diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 8175b91f..b8b25216 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -473,12 +473,14 @@ export default function Home() { const handleDelete = async () => { if (!client || !selectedEmail) return; - // Check if we're currently in the trash folder + // Check if we're currently in the trash or junk folder const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); const isInTrash = currentMailbox?.role === 'trash'; + const isInJunk = currentMailbox?.role === 'junk'; + const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk; - if (isInTrash) { - // In trash: confirm before permanently deleting + if (isInTrash || (isInJunk && permanentlyDeleteJunk)) { + // In trash or junk with permanent delete enabled: confirm before permanently deleting const confirmed = await confirmDialog({ title: t('email_list.permanent_delete_confirm_title'), message: t('email_list.permanent_delete_confirm_message'), diff --git a/components/email/email-list.tsx b/components/email/email-list.tsx index b1b2ebab..5c02d802 100644 --- a/components/email/email-list.tsx +++ b/components/email/email-list.tsx @@ -4,7 +4,7 @@ import { Email, ThreadGroup } from "@/lib/jmap/types"; import { ThreadListItem } from "./thread-list-item"; import { EmailContextMenu } from "./email-context-menu"; import { cn } from "@/lib/utils"; -import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX } from "lucide-react"; +import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX, AlertTriangle } from "lucide-react"; import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; @@ -74,6 +74,7 @@ export function EmailList({ isLoadingMore, mailboxes, selectedMailbox, + emptyMailbox, expandedThreadIds, threadEmailsCache, isLoadingThread, @@ -172,6 +173,28 @@ export function EmailList({ } }; + const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); + const isEmptyableFolder = currentMailbox?.role === 'trash' || currentMailbox?.role === 'junk'; + + const handleEmptyFolder = async () => { + if (!client || isProcessing || !currentMailbox) return; + + const confirmed = await confirmDialog({ + title: t('empty_folder.confirm_title'), + message: t('empty_folder.confirm_message'), + confirmText: t('empty_folder.confirm_button'), + variant: "destructive", + }); + if (!confirmed) return; + + setIsProcessing(true); + try { + await emptyMailbox(client, currentMailbox.id); + } finally { + setTimeout(() => setIsProcessing(false), 500); + } + }; + const handleLoadMore = useCallback(() => { if (client && hasMoreEmails && !isLoadingMore && !isLoading) { loadMoreEmails(client); @@ -308,7 +331,29 @@ export function EmailList({ /> )} - + {/* Empty Folder Banner for Junk/Trash */} + {isEmptyableFolder && emails.length > 0 && !hasSelection && ( +
+
+ + {currentMailbox?.role === 'junk' ? t('empty_folder.junk_hint') : t('empty_folder.trash_hint')} +
+ +
+ )} {/* Email List */}
diff --git a/components/settings/email-settings.tsx b/components/settings/email-settings.tsx index ee13b5e9..e3dfe3d1 100644 --- a/components/settings/email-settings.tsx +++ b/components/settings/email-settings.tsx @@ -14,6 +14,7 @@ export function EmailSettings() { const { markAsReadDelay, deleteAction, + permanentlyDeleteJunk, showPreview, emailsPerPage, externalContentPolicy, @@ -65,6 +66,14 @@ export function EmailSettings() {
+ {/* Permanently Delete Junk */} + + updateSetting('permanentlyDeleteJunk', checked)} + /> + + {/* Show Preview */} updateSetting('showPreview', checked)} /> diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index cd63f3d5..10f8b9c5 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -808,6 +808,34 @@ export class JMAPClient { } } + async emptyMailbox(mailboxId: string): Promise { + let totalDestroyed = 0; + let hasMore = true; + + while (hasMore) { + const response = await this.request([ + ["Email/query", { + accountId: this.accountId, + filter: { inMailbox: mailboxId }, + limit: 500, + }, "0"], + ["Email/set", { + accountId: this.accountId, + "#destroy": { resultOf: "0", name: "Email/query", path: "/ids" }, + }, "1"], + ]); + + const queryResult = response.methodResponses?.[0]?.[1]; + const setResult = response.methodResponses?.[1]?.[1]; + const destroyed = setResult?.destroyed?.length || 0; + totalDestroyed += destroyed; + + hasMore = destroyed > 0 && (queryResult?.total || 0) > destroyed; + } + + return totalDestroyed; + } + async markAsSpam(emailId: string, accountId?: string): Promise { const targetAccountId = accountId || this.accountId; diff --git a/locales/de/common.json b/locales/de/common.json index 59a38fdc..5ded4634 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -130,7 +130,15 @@ "permanent_delete": "Endgültig löschen", "permanent_delete_confirm_title": "Endgültig löschen", "permanent_delete_confirm_message": "Diese E-Mail wird endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", - "permanent_delete_confirm_batch_message": "Diese {count, plural, one {1 E-Mail} other {# E-Mails}} werden endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden." + "permanent_delete_confirm_batch_message": "Diese {count, plural, one {1 E-Mail} other {# E-Mails}} werden endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", + "empty_folder": { + "button": "Ordner leeren", + "confirm_title": "Ordner leeren", + "confirm_message": "Alle E-Mails in diesem Ordner werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", + "confirm_button": "Ordner leeren", + "junk_hint": "Sie können den Spam-Ordner leeren, um alle Nachrichten dauerhaft zu entfernen.", + "trash_hint": "Sie können den Papierkorb leeren, um alle Nachrichten dauerhaft zu entfernen." + } }, "email_viewer": { "no_email_selected": "Keine E-Mail ausgewählt", @@ -594,6 +602,10 @@ "permanent": "Dauerhaft löschen", "warning": "E-Mails werden dauerhaft gelöscht und können nicht wiederhergestellt werden. Diese Aktion ist unwiderruflich." }, + "permanently_delete_junk": { + "label": "Spam dauerhaft löschen", + "description": "E-Mails aus dem Spam-Ordner dauerhaft löschen, anstatt sie in den Papierkorb zu verschieben" + }, "show_preview": { "label": "Vorschautext anzeigen", "description": "E-Mail-Vorschau in der Liste anzeigen" diff --git a/locales/en/common.json b/locales/en/common.json index 8f8d4f28..50852f7e 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -130,7 +130,15 @@ "permanent_delete": "Delete permanently", "permanent_delete_confirm_title": "Permanently delete", "permanent_delete_confirm_message": "This email will be permanently deleted. This action cannot be undone.", - "permanent_delete_confirm_batch_message": "These {count, plural, one {1 email} other {# emails}} will be permanently deleted. This action cannot be undone." + "permanent_delete_confirm_batch_message": "These {count, plural, one {1 email} other {# emails}} will be permanently deleted. This action cannot be undone.", + "empty_folder": { + "button": "Empty folder", + "confirm_title": "Empty folder", + "confirm_message": "All emails in this folder will be permanently deleted. This action cannot be undone.", + "confirm_button": "Empty folder", + "junk_hint": "You can empty the Junk folder to permanently remove all messages.", + "trash_hint": "You can empty the Trash folder to permanently remove all messages." + } }, "email_viewer": { "no_email_selected": "No email selected", @@ -598,6 +606,10 @@ "permanent": "Delete Permanently", "warning": "Emails will be permanently deleted and cannot be recovered. This action is irreversible." }, + "permanently_delete_junk": { + "label": "Permanently Delete Junk", + "description": "Permanently delete emails from the Junk/Spam folder instead of moving them to Trash" + }, "show_preview": { "label": "Show Preview Text", "description": "Display email preview in the list" diff --git a/locales/es/common.json b/locales/es/common.json index ec28e783..eebde46f 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -130,7 +130,15 @@ "permanent_delete": "Eliminar permanentemente", "permanent_delete_confirm_title": "Eliminar permanentemente", "permanent_delete_confirm_message": "Este correo se eliminará permanentemente. Esta acción no se puede deshacer.", - "permanent_delete_confirm_batch_message": "Estos {count, plural, one {1 correo} other {# correos}} se eliminarán permanentemente. Esta acción no se puede deshacer." + "permanent_delete_confirm_batch_message": "Estos {count, plural, one {1 correo} other {# correos}} se eliminarán permanentemente. Esta acción no se puede deshacer.", + "empty_folder": { + "button": "Vaciar carpeta", + "confirm_title": "Vaciar carpeta", + "confirm_message": "Todos los correos de esta carpeta se eliminarán permanentemente. Esta acción no se puede deshacer.", + "confirm_button": "Vaciar carpeta", + "junk_hint": "Puede vaciar la carpeta de Spam para eliminar permanentemente todos los mensajes.", + "trash_hint": "Puede vaciar la Papelera para eliminar permanentemente todos los mensajes." + } }, "email_viewer": { "no_email_selected": "Ningún correo seleccionado", @@ -594,6 +602,10 @@ "permanent": "Eliminar Permanentemente", "warning": "Los correos se eliminarán permanentemente y no se podrán recuperar. Esta acción es irreversible." }, + "permanently_delete_junk": { + "label": "Eliminar spam permanentemente", + "description": "Eliminar permanentemente los correos de la carpeta Spam en lugar de moverlos a la Papelera" + }, "show_preview": { "label": "Mostrar Vista Previa", "description": "Mostrar vista previa del correo en la lista" diff --git a/locales/fr/common.json b/locales/fr/common.json index 0c920d32..ff01e6ce 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -130,7 +130,15 @@ "permanent_delete": "Supprimer définitivement", "permanent_delete_confirm_title": "Supprimer définitivement", "permanent_delete_confirm_message": "Cet e-mail sera définitivement supprimé. Cette action est irréversible.", - "permanent_delete_confirm_batch_message": "Ces {count, plural, one {1 e-mail} other {# e-mails}} seront définitivement supprimés. Cette action est irréversible." + "permanent_delete_confirm_batch_message": "Ces {count, plural, one {1 e-mail} other {# e-mails}} seront définitivement supprimés. Cette action est irréversible.", + "empty_folder": { + "button": "Vider le dossier", + "confirm_title": "Vider le dossier", + "confirm_message": "Tous les e-mails de ce dossier seront définitivement supprimés. Cette action est irréversible.", + "confirm_button": "Vider le dossier", + "junk_hint": "Vous pouvez vider le dossier Indésirables pour supprimer définitivement tous les messages.", + "trash_hint": "Vous pouvez vider la corbeille pour supprimer définitivement tous les messages." + } }, "email_viewer": { "no_email_selected": "Aucun email sélectionné", @@ -594,6 +602,10 @@ "permanent": "Supprimer définitivement", "warning": "Les emails seront supprimés définitivement et ne pourront pas être récupérés. Cette action est irréversible." }, + "permanently_delete_junk": { + "label": "Supprimer définitivement les indésirables", + "description": "Supprimer définitivement les e-mails du dossier Indésirables au lieu de les déplacer vers la corbeille" + }, "show_preview": { "label": "Afficher l'aperçu", "description": "Afficher l'aperçu de l'email dans la liste" diff --git a/locales/it/common.json b/locales/it/common.json index 21599c9d..5fab4799 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -130,7 +130,15 @@ "permanent_delete": "Elimina definitivamente", "permanent_delete_confirm_title": "Elimina definitivamente", "permanent_delete_confirm_message": "Questa e-mail verrà eliminata definitivamente. Questa azione non può essere annullata.", - "permanent_delete_confirm_batch_message": "Queste {count, plural, one {1 e-mail} other {# e-mail}} verranno eliminate definitivamente. Questa azione non può essere annullata." + "permanent_delete_confirm_batch_message": "Queste {count, plural, one {1 e-mail} other {# e-mail}} verranno eliminate definitivamente. Questa azione non può essere annullata.", + "empty_folder": { + "button": "Svuota cartella", + "confirm_title": "Svuota cartella", + "confirm_message": "Tutti i messaggi in questa cartella verranno eliminati definitivamente. Questa azione non può essere annullata.", + "confirm_button": "Svuota cartella", + "junk_hint": "Puoi svuotare la cartella Spam per rimuovere definitivamente tutti i messaggi.", + "trash_hint": "Puoi svuotare il Cestino per rimuovere definitivamente tutti i messaggi." + } }, "email_viewer": { "no_email_selected": "Nessun messaggio selezionato", @@ -594,6 +602,10 @@ "permanent": "Elimina definitivamente", "warning": "I messaggi verranno eliminati definitivamente e non potranno essere recuperati. Questa azione è irreversibile." }, + "permanently_delete_junk": { + "label": "Elimina spam definitivamente", + "description": "Elimina definitivamente i messaggi dalla cartella Spam invece di spostarli nel cestino" + }, "show_preview": { "label": "Mostra anteprima testo", "description": "Visualizza l'anteprima del messaggio nell'elenco" diff --git a/locales/ja/common.json b/locales/ja/common.json index 481eccb6..19202061 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -130,7 +130,15 @@ "permanent_delete": "完全に削除", "permanent_delete_confirm_title": "完全に削除", "permanent_delete_confirm_message": "このメールは完全に削除されます。この操作は取り消せません。", - "permanent_delete_confirm_batch_message": "{count, plural, one {1件} other {#件}}のメールが完全に削除されます。この操作は取り消せません。" + "permanent_delete_confirm_batch_message": "{count, plural, one {1件} other {#件}}のメールが完全に削除されます。この操作は取り消せません。", + "empty_folder": { + "button": "フォルダを空にする", + "confirm_title": "フォルダを空にする", + "confirm_message": "このフォルダ内のすべてのメールが完全に削除されます。この操作は取り消せません。", + "confirm_button": "フォルダを空にする", + "junk_hint": "迷惑メールフォルダを空にして、すべてのメールを完全に削除できます。", + "trash_hint": "ゴミ箱を空にして、すべてのメールを完全に削除できます。" + } }, "email_viewer": { "no_email_selected": "メールが選択されていません", @@ -594,6 +602,10 @@ "permanent": "完全に削除", "warning": "メールは完全に削除され、復元できません。この操作は元に戻せません。" }, + "permanently_delete_junk": { + "label": "迷惑メールを完全に削除", + "description": "迷惑メールフォルダのメールをゴミ箱に移動せずに完全に削除する" + }, "show_preview": { "label": "プレビューテキストを表示", "description": "リストにメールのプレビューを表示" diff --git a/locales/nl/common.json b/locales/nl/common.json index c18bf510..5b457d50 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -130,7 +130,15 @@ "permanent_delete": "Definitief verwijderen", "permanent_delete_confirm_title": "Definitief verwijderen", "permanent_delete_confirm_message": "Deze e-mail wordt definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.", - "permanent_delete_confirm_batch_message": "Deze {count, plural, one {1 e-mail} other {# e-mails}} worden definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt." + "permanent_delete_confirm_batch_message": "Deze {count, plural, one {1 e-mail} other {# e-mails}} worden definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.", + "empty_folder": { + "button": "Map legen", + "confirm_title": "Map legen", + "confirm_message": "Alle e-mails in deze map worden permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt.", + "confirm_button": "Map legen", + "junk_hint": "U kunt de map Spam legen om alle berichten permanent te verwijderen.", + "trash_hint": "U kunt de prullenbak legen om alle berichten permanent te verwijderen." + } }, "email_viewer": { "no_email_selected": "Geen e-mail geselecteerd", @@ -594,6 +602,10 @@ "permanent": "Permanent verwijderen", "warning": "E-mails worden permanent verwijderd en kunnen niet worden hersteld. Deze actie is onomkeerbaar." }, + "permanently_delete_junk": { + "label": "Spam permanent verwijderen", + "description": "E-mails uit de map Spam permanent verwijderen in plaats van naar de prullenbak te verplaatsen" + }, "show_preview": { "label": "Voorbeeldtekst tonen", "description": "E-mailvoorbeeld weergeven in de lijst" diff --git a/locales/pt/common.json b/locales/pt/common.json index e299ee26..3c18c23a 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -130,7 +130,15 @@ "permanent_delete": "Excluir permanentemente", "permanent_delete_confirm_title": "Excluir permanentemente", "permanent_delete_confirm_message": "Este e-mail será permanentemente excluído. Esta ação não pode ser desfeita.", - "permanent_delete_confirm_batch_message": "Estes {count, plural, one {1 e-mail} other {# e-mails}} serão permanentemente excluídos. Esta ação não pode ser desfeita." + "permanent_delete_confirm_batch_message": "Estes {count, plural, one {1 e-mail} other {# e-mails}} serão permanentemente excluídos. Esta ação não pode ser desfeita.", + "empty_folder": { + "button": "Esvaziar pasta", + "confirm_title": "Esvaziar pasta", + "confirm_message": "Todos os e-mails nesta pasta serão permanentemente excluídos. Esta ação não pode ser desfeita.", + "confirm_button": "Esvaziar pasta", + "junk_hint": "Você pode esvaziar a pasta de Spam para remover permanentemente todas as mensagens.", + "trash_hint": "Você pode esvaziar a Lixeira para remover permanentemente todas as mensagens." + } }, "email_viewer": { "no_email_selected": "Nenhum e-mail selecionado", @@ -594,6 +602,10 @@ "permanent": "Excluir Permanentemente", "warning": "Os e-mails serão excluídos permanentemente e não poderão ser recuperados. Esta ação é irreversível." }, + "permanently_delete_junk": { + "label": "Excluir spam permanentemente", + "description": "Excluir permanentemente e-mails da pasta Spam em vez de movê-los para a Lixeira" + }, "show_preview": { "label": "Mostrar Texto de Visualização", "description": "Exibir visualização do e-mail na lista" diff --git a/stores/email-store.ts b/stores/email-store.ts index a6c198da..ed52cb4c 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -102,6 +102,7 @@ interface EmailStore { renameMailbox: (client: JMAPClient, mailboxId: string, name: string) => Promise; deleteMailbox: (client: JMAPClient, mailboxId: string) => Promise; setMailboxRole: (client: JMAPClient, mailboxId: string, role: string | null) => Promise; + emptyMailbox: (client: JMAPClient, mailboxId: string) => Promise; // Mock data for demo loadMockData: () => void; @@ -411,6 +412,7 @@ export const useEmailStore = create((set, get) => ({ // Get delete action preference from settings const deleteAction = useSettingsStore.getState().deleteAction; + const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk; // Determine accountId for shared folders const selectedMailboxId = get().selectedMailbox; @@ -418,6 +420,12 @@ export const useEmailStore = create((set, get) => ({ const currentMailbox = mailboxes.find(mb => mb.id === selectedMailboxId); const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined; + // If in junk folder and setting is enabled, permanently delete + const isInJunk = currentMailbox?.role === 'junk'; + if (isInJunk && permanentlyDeleteJunk) { + forceDelete = true; + } + // If deleteAction is 'trash' and not forced permanent delete, try to move to trash mailbox if (deleteAction === 'trash' && !forceDelete) { // Find trash mailbox for the correct account @@ -1287,6 +1295,35 @@ export const useEmailStore = create((set, get) => ({ } }, + emptyMailbox: async (client, mailboxId) => { + try { + set({ isLoading: true, error: null }); + await client.emptyMailbox(mailboxId); + + // Clear emails from local state if we're viewing this mailbox + const currentMailbox = get().selectedMailbox; + if (currentMailbox === mailboxId) { + set({ emails: [], selectedEmail: null }); + } + + // Update mailbox counters + set({ + mailboxes: get().mailboxes.map(mb => + mb.id === mailboxId + ? { ...mb, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0 } + : mb + ), + isLoading: false, + }); + } catch (error) { + set({ + error: error instanceof Error ? error.message : 'Failed to empty folder', + isLoading: false, + }); + throw error; + } + }, + loadMockData: () => { const mockEmails: Email[] = [ { diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 3ba2435f..b90792cc 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -77,6 +77,7 @@ interface SettingsState { // Email Behavior markAsReadDelay: number; // milliseconds (0 = instant, -1 = never) deleteAction: DeleteAction; + permanentlyDeleteJunk: boolean; // Permanently delete emails from junk/spam instead of moving to trash showPreview: boolean; emailsPerPage: number; externalContentPolicy: ExternalContentPolicy; @@ -154,6 +155,7 @@ const DEFAULT_SETTINGS = { // Email Behavior markAsReadDelay: 0, // Instant deleteAction: 'trash' as DeleteAction, + permanentlyDeleteJunk: false, showPreview: true, emailsPerPage: 50, externalContentPolicy: 'ask' as ExternalContentPolicy,