From ad175d20e34a86dc4a5b4c86ab073494ab41c18a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:50:42 +0200 Subject: [PATCH] feat: enhance email deletion and spam handling with improved parameterization --- .claude/settings.local.json | 7 +++++ app/[locale]/page.tsx | 29 ++++++++--------- components/email/email-list.tsx | 2 +- stores/email-store.ts | 55 ++++++++++++++++++++++++--------- 4 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..e56d14eb --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:github.com)" + ] + } +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 34e61cad..af6e1f57 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -758,8 +758,8 @@ export default function Home() { if (isMobile) setActiveView('viewer'); }; - const handleDelete = async () => { - if (!client || !selectedEmail) return; + const handleDelete = async (emailToDelete: Email | null = selectedEmail) => { + if (!client || !emailToDelete) return; // Check if we're currently in the trash or junk folder const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); @@ -778,7 +778,7 @@ export default function Home() { if (!confirmed) return; try { - await deleteEmail(client, selectedEmail.id, true); + await deleteEmail(client, emailToDelete.id, true); } catch (error) { console.error("Failed to permanently delete email:", error); } @@ -787,7 +787,7 @@ export default function Home() { const trashMailbox = mailboxes.find(m => m.role === 'trash' && !m.isShared); if (trashMailbox) { try { - await moveToMailbox(client, selectedEmail.id, trashMailbox.id); + await moveToMailbox(client, emailToDelete.id, trashMailbox.id); } catch (error) { console.error("Failed to move email to trash:", error); } @@ -860,10 +860,10 @@ export default function Home() { } }; - const handleMarkAsSpam = async () => { - if (!client || !selectedEmail) return; + const handleMarkAsSpam = async (emailToMark: Email | null = selectedEmail) => { + if (!client || !emailToMark) return; - const emailId = selectedEmail.id; + const emailId = emailToMark.id; try { await markAsSpam(client, emailId); @@ -891,11 +891,11 @@ export default function Home() { } }; - const handleUndoSpam = async () => { - if (!client || !selectedEmail) return; + const handleUndoSpam = async (emailToRestore: Email | null = selectedEmail) => { + if (!client || !emailToRestore) return; try { - await undoSpam(client, selectedEmail.id); + await undoSpam(client, emailToRestore.id); const toastInstance = (await import('sonner')).toast; toastInstance.success(t('email_viewer.spam.toast_not_spam_success')); @@ -1715,8 +1715,7 @@ export default function Home() { } }} onDelete={async (email) => { - selectEmail(email); - await handleDelete(); + await handleDelete(email); }} onArchive={async (email) => { await handleArchive(email); @@ -1730,12 +1729,10 @@ export default function Home() { } }} onMarkAsSpam={async (email) => { - selectEmail(email); - await handleMarkAsSpam(); + await handleMarkAsSpam(email); }} onUndoSpam={async (email) => { - selectEmail(email); - await handleUndoSpam(); + await handleUndoSpam(email); }} onEditDraft={(email) => { handleEditDraft(email); diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index cf220411..7de11036 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -176,7 +176,7 @@ export function EmailList({ setIsProcessing(true); try { - await batchDelete(client); + await batchDelete(client, isInTrash); } finally { setTimeout(() => setIsProcessing(false), 500); } diff --git a/stores/email-store.ts b/stores/email-store.ts index 2ab1ddb3..572912ff 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -87,7 +87,7 @@ interface EmailStore { // Batch operations batchMarkAsRead: (client: IJMAPClient, read: boolean) => Promise; - batchDelete: (client: IJMAPClient) => Promise; + batchDelete: (client: IJMAPClient, permanent?: boolean) => Promise; batchMoveToMailbox: (client: IJMAPClient, mailboxId: string) => Promise; // Spam operations @@ -1044,32 +1044,59 @@ export const useEmailStore = create((set, get) => ({ } }, - batchDelete: async (client) => { - const { selectedEmailIds, emails, mailboxes } = get(); + batchDelete: async (client, permanent = false) => { + const { selectedEmailIds, emails, mailboxes, selectedMailbox } = get(); if (selectedEmailIds.size === 0) return; set({ isLoading: true, error: null }); try { const emailIdsArray = Array.from(selectedEmailIds); - if (get().isUnifiedView) { - // Group emails by accountId for cross-account operations - const emailsByAccount = new Map(); - for (const emailId of emailIdsArray) { - const email = emails.find(e => e.id === emailId); - const acctId = email?.accountId || '__default__'; - if (!emailsByAccount.has(acctId)) emailsByAccount.set(acctId, []); - emailsByAccount.get(acctId)!.push(emailId); - } + // Determine if the current folder forces permanent deletion. + const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); + const isInTrash = currentMailbox?.role === 'trash'; + const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk; + const isInJunk = currentMailbox?.role === 'junk'; + const forceDestroy = permanent || isInTrash || (isInJunk && permanentlyDeleteJunk); + // Group emails by accountId (handles unified view and search results spanning accounts). + const emailsByAccount = new Map(); + for (const emailId of emailIdsArray) { + const email = emails.find(e => e.id === emailId); + const acctId = email?.accountId || '__default__'; + if (!emailsByAccount.has(acctId)) emailsByAccount.set(acctId, []); + emailsByAccount.get(acctId)!.push(emailId); + } + + const getClient = (acctId: string) => + acctId === '__default__' ? client : useAuthStore.getState().getClientForAccount(acctId); + + if (forceDestroy) { const promises = Array.from(emailsByAccount.entries()).map(async ([acctId, ids]) => { - const acctClient = acctId === '__default__' ? client : useAuthStore.getState().getClientForAccount(acctId); + const acctClient = getClient(acctId); if (!acctClient) return; await acctClient.batchDeleteEmails(ids); }); await Promise.allSettled(promises); } else { - await client.batchDeleteEmails(emailIdsArray); + // Move to trash per account. + const promises = Array.from(emailsByAccount.entries()).map(async ([acctId, ids]) => { + const acctClient = getClient(acctId); + if (!acctClient) return; + const trashMailbox = mailboxes.find(mb => { + if (mb.role !== 'trash') return false; + if (acctId === '__default__') return !mb.isShared; + return mb.accountId === acctId; + }); + if (!trashMailbox) { + // No trash available for this account — fall back to destroy so the action isn't silently dropped. + await acctClient.batchDeleteEmails(ids); + return; + } + const trashId = trashMailbox.originalId || trashMailbox.id; + await acctClient.batchMoveEmails(ids, trashId, trashMailbox.accountId); + }); + await Promise.allSettled(promises); } // Remove deleted emails from local state