feat: enhance email deletion and spam handling with improved parameterization

This commit is contained in:
Linus Rath
2026-04-16 16:50:42 +02:00
parent f22699fe20
commit ad175d20e3
4 changed files with 62 additions and 31 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"WebFetch(domain:github.com)"
]
}
}
+13 -16
View File
@@ -758,8 +758,8 @@ export default function Home() {
if (isMobile) setActiveView('viewer'); if (isMobile) setActiveView('viewer');
}; };
const handleDelete = async () => { const handleDelete = async (emailToDelete: Email | null = selectedEmail) => {
if (!client || !selectedEmail) return; if (!client || !emailToDelete) return;
// Check if we're currently in the trash or junk folder // Check if we're currently in the trash or junk folder
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
@@ -778,7 +778,7 @@ export default function Home() {
if (!confirmed) return; if (!confirmed) return;
try { try {
await deleteEmail(client, selectedEmail.id, true); await deleteEmail(client, emailToDelete.id, true);
} catch (error) { } catch (error) {
console.error("Failed to permanently delete email:", 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); const trashMailbox = mailboxes.find(m => m.role === 'trash' && !m.isShared);
if (trashMailbox) { if (trashMailbox) {
try { try {
await moveToMailbox(client, selectedEmail.id, trashMailbox.id); await moveToMailbox(client, emailToDelete.id, trashMailbox.id);
} catch (error) { } catch (error) {
console.error("Failed to move email to trash:", error); console.error("Failed to move email to trash:", error);
} }
@@ -860,10 +860,10 @@ export default function Home() {
} }
}; };
const handleMarkAsSpam = async () => { const handleMarkAsSpam = async (emailToMark: Email | null = selectedEmail) => {
if (!client || !selectedEmail) return; if (!client || !emailToMark) return;
const emailId = selectedEmail.id; const emailId = emailToMark.id;
try { try {
await markAsSpam(client, emailId); await markAsSpam(client, emailId);
@@ -891,11 +891,11 @@ export default function Home() {
} }
}; };
const handleUndoSpam = async () => { const handleUndoSpam = async (emailToRestore: Email | null = selectedEmail) => {
if (!client || !selectedEmail) return; if (!client || !emailToRestore) return;
try { try {
await undoSpam(client, selectedEmail.id); await undoSpam(client, emailToRestore.id);
const toastInstance = (await import('sonner')).toast; const toastInstance = (await import('sonner')).toast;
toastInstance.success(t('email_viewer.spam.toast_not_spam_success')); toastInstance.success(t('email_viewer.spam.toast_not_spam_success'));
@@ -1715,8 +1715,7 @@ export default function Home() {
} }
}} }}
onDelete={async (email) => { onDelete={async (email) => {
selectEmail(email); await handleDelete(email);
await handleDelete();
}} }}
onArchive={async (email) => { onArchive={async (email) => {
await handleArchive(email); await handleArchive(email);
@@ -1730,12 +1729,10 @@ export default function Home() {
} }
}} }}
onMarkAsSpam={async (email) => { onMarkAsSpam={async (email) => {
selectEmail(email); await handleMarkAsSpam(email);
await handleMarkAsSpam();
}} }}
onUndoSpam={async (email) => { onUndoSpam={async (email) => {
selectEmail(email); await handleUndoSpam(email);
await handleUndoSpam();
}} }}
onEditDraft={(email) => { onEditDraft={(email) => {
handleEditDraft(email); handleEditDraft(email);
+1 -1
View File
@@ -176,7 +176,7 @@ export function EmailList({
setIsProcessing(true); setIsProcessing(true);
try { try {
await batchDelete(client); await batchDelete(client, isInTrash);
} finally { } finally {
setTimeout(() => setIsProcessing(false), 500); setTimeout(() => setIsProcessing(false), 500);
} }
+41 -14
View File
@@ -87,7 +87,7 @@ interface EmailStore {
// Batch operations // Batch operations
batchMarkAsRead: (client: IJMAPClient, read: boolean) => Promise<void>; batchMarkAsRead: (client: IJMAPClient, read: boolean) => Promise<void>;
batchDelete: (client: IJMAPClient) => Promise<void>; batchDelete: (client: IJMAPClient, permanent?: boolean) => Promise<void>;
batchMoveToMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>; batchMoveToMailbox: (client: IJMAPClient, mailboxId: string) => Promise<void>;
// Spam operations // Spam operations
@@ -1044,32 +1044,59 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
} }
}, },
batchDelete: async (client) => { batchDelete: async (client, permanent = false) => {
const { selectedEmailIds, emails, mailboxes } = get(); const { selectedEmailIds, emails, mailboxes, selectedMailbox } = get();
if (selectedEmailIds.size === 0) return; if (selectedEmailIds.size === 0) return;
set({ isLoading: true, error: null }); set({ isLoading: true, error: null });
try { try {
const emailIdsArray = Array.from(selectedEmailIds); const emailIdsArray = Array.from(selectedEmailIds);
if (get().isUnifiedView) { // Determine if the current folder forces permanent deletion.
// Group emails by accountId for cross-account operations const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
const emailsByAccount = new Map<string, string[]>(); const isInTrash = currentMailbox?.role === 'trash';
for (const emailId of emailIdsArray) { const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk;
const email = emails.find(e => e.id === emailId); const isInJunk = currentMailbox?.role === 'junk';
const acctId = email?.accountId || '__default__'; const forceDestroy = permanent || isInTrash || (isInJunk && permanentlyDeleteJunk);
if (!emailsByAccount.has(acctId)) emailsByAccount.set(acctId, []);
emailsByAccount.get(acctId)!.push(emailId);
}
// Group emails by accountId (handles unified view and search results spanning accounts).
const emailsByAccount = new Map<string, string[]>();
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 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; if (!acctClient) return;
await acctClient.batchDeleteEmails(ids); await acctClient.batchDeleteEmails(ids);
}); });
await Promise.allSettled(promises); await Promise.allSettled(promises);
} else { } 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 // Remove deleted emails from local state