From 2b32a0110b4afeb66600d81aeb7d0d03d19f0aff Mon Sep 17 00:00:00 2001 From: Linus Rath Date: Mon, 7 Sep 2026 22:34:47 +0200 Subject: [PATCH] fix: report a missing archive mailbox instead of failing silently #578 --- components/mail/mail-app.tsx | 12 ++- locales/ar/common.json | 1 + locales/ca/common.json | 1 + locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/fa/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/mn/common.json | 1 + locales/nb/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ro/common.json | 1 + locales/ru/common.json | 1 + locales/sk/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/zh-TW/common.json | 1 + locales/zh/common.json | 1 + .../email-store-archive-missing.test.ts | 91 +++++++++++++++++++ stores/email-store.ts | 19 +++- 30 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 stores/__tests__/email-store-archive-missing.test.ts diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index 77c6385bb..7d81f830f 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -31,7 +31,7 @@ import { usePolicyStore } from "@/stores/policy-store"; import type { UnifiedAccountClient } from "@/lib/unified-mailbox"; import { connectedAccountsGrew } from "@/lib/unified-mailbox"; import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal"; -import { useEmailStore, buildUnifiedAccountClients } from "@/stores/email-store"; +import { useEmailStore, buildUnifiedAccountClients, ArchiveMailboxNotFoundError } from "@/stores/email-store"; import { toast } from "@/stores/toast-store"; import { MailboxShareDialog } from "@/components/layout/mailbox-share-dialog"; import { ShareNotificationToaster } from "@/components/layout/share-notification-toaster"; @@ -704,6 +704,10 @@ export function MailApp({ linkSegments }: MailAppProps = {}) { await batchArchive(client); } catch (error) { console.error("Failed to batch archive:", error); + if (error instanceof ArchiveMailboxNotFoundError) { + const { toast } = await import('sonner'); + toast.error(t('email_viewer.archive_mailbox_not_found')); + } } } else if (selectedEmail) { handleArchive(); @@ -2205,7 +2209,11 @@ export function MailApp({ linkSegments }: MailAppProps = {}) { const currentMailboxes = readMailboxes(); const archiveMailbox = currentMailboxes.find(m => m.role === "archive" || m.name.toLowerCase() === "archive"); - if (!archiveMailbox) return; + if (!archiveMailbox) { + const { toast } = await import('sonner'); + toast.error(t('email_viewer.archive_mailbox_not_found')); + return; + } const { archiveMode } = useSettingsStore.getState(); diff --git a/locales/ar/common.json b/locales/ar/common.json index 9f0ae9ee9..7801f5eb9 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -292,6 +292,7 @@ "forward": "إعادة توجيه", "delete": "حذف", "archive": "أرشفة", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "تمييز بنجمة", "unstar": "إزالة النجمة", "mark_unread": "تمييز كغير مقروءة", diff --git a/locales/ca/common.json b/locales/ca/common.json index a5ba6c6b8..b7790be89 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -292,6 +292,7 @@ "forward": "Reenvia", "delete": "Suprimeix", "archive": "Arxiva", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Marca amb estrella", "unstar": "Treu l'estrella", "mark_unread": "Marca com a no llegit", diff --git a/locales/cs/common.json b/locales/cs/common.json index 2e305ceb5..23a5ad7ff 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -292,6 +292,7 @@ "forward": "Přeposlat", "delete": "Odstranit", "archive": "Archivovat", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Označit hvězdičkou", "unstar": "Odebrat hvězdičku", "mark_unread": "Označit jako nepřečtené", diff --git a/locales/da/common.json b/locales/da/common.json index cdd9bdfdd..e7fb1e671 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -292,6 +292,7 @@ "forward": "Videresend", "delete": "Slet", "archive": "Arkivér", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Stjernemarkér", "unstar": "Fjern stjernemarkering", "mark_unread": "Markér som ulæst", diff --git a/locales/de/common.json b/locales/de/common.json index dbf925f6e..89bca19ac 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -292,6 +292,7 @@ "forward": "Weiterleiten", "delete": "Löschen", "archive": "Archivieren", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Stern hinzufügen", "unstar": "Stern entfernen", "mark_unread": "Als ungelesen markieren", diff --git a/locales/en/common.json b/locales/en/common.json index 0d2c1177c..f79d8ed16 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -292,6 +292,7 @@ "forward": "Forward", "delete": "Delete", "archive": "Archive", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Star", "unstar": "Unstar", "mark_unread": "Mark as unread", diff --git a/locales/es/common.json b/locales/es/common.json index 97ab1052a..5e3799f5d 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -292,6 +292,7 @@ "forward": "Reenviar", "delete": "Eliminar", "archive": "Archivar", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Destacar", "unstar": "Quitar destacado", "mark_unread": "Marcar como no leído", diff --git a/locales/fa/common.json b/locales/fa/common.json index 3ef14c7a6..2a0bbf4ae 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -292,6 +292,7 @@ "forward": "ارسال مجدد", "delete": "حذف", "archive": "بایگانی", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "ستاره‌دار کردن", "unstar": "حذف ستاره", "mark_unread": "علامت‌گذاری خوانده نشده", diff --git a/locales/fr/common.json b/locales/fr/common.json index dc4116756..6c3289873 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -292,6 +292,7 @@ "forward": "Transférer", "delete": "Supprimer", "archive": "Archiver", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Marquer comme favori", "unstar": "Retirer des favoris", "mark_unread": "Marquer comme non lu", diff --git a/locales/he/common.json b/locales/he/common.json index 41c6fda09..13399843a 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -244,6 +244,7 @@ "forward": "העבר", "delete": "מחיקה", "archive": "ארכיון", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "כוכב", "unstar": "הסר כוכב", "mark_unread": "סמן כלא נקרא", diff --git a/locales/hu/common.json b/locales/hu/common.json index 7091ba8c5..dccdae7ce 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -292,6 +292,7 @@ "forward": "Továbbítás", "delete": "Törlés", "archive": "Archiválás", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Csillagozás", "unstar": "Csillagozás megszüntetése", "mark_unread": "Megjelölés olvasatlanként", diff --git a/locales/it/common.json b/locales/it/common.json index d59335b14..e5635fa74 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -292,6 +292,7 @@ "forward": "Inoltra", "delete": "Elimina", "archive": "Archivia", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Aggiungi stella", "unstar": "Rimuovi stella", "mark_unread": "Segna come non letto", diff --git a/locales/ja/common.json b/locales/ja/common.json index 80f154e58..85a7bb34c 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -292,6 +292,7 @@ "forward": "転送", "delete": "削除", "archive": "アーカイブ", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "スターを付ける", "unstar": "スターを外す", "mark_unread": "未読にする", diff --git a/locales/ko/common.json b/locales/ko/common.json index c29c8a570..eb41784f7 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -292,6 +292,7 @@ "forward": "전달", "delete": "삭제", "archive": "보관", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "별표 달기", "unstar": "별표 해제", "mark_unread": "읽지 않은 상태로 표시", diff --git a/locales/lv/common.json b/locales/lv/common.json index a51796997..f8fffebea 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -292,6 +292,7 @@ "forward": "Pārsūtīt", "delete": "Dzēst", "archive": "Arhivēt", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Pievienot zvaigznīti", "unstar": "Noņemt zvaigznīti", "mark_unread": "Atzīmēt kā nelasītu", diff --git a/locales/mn/common.json b/locales/mn/common.json index 638cd1555..235b458a7 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -292,6 +292,7 @@ "forward": "Урагшаа", "delete": "Устгах", "archive": "Архив", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "од", "unstar": "Одгүй болгох", "mark_unread": "Уншаагүй гэж тэмдэглэх", diff --git a/locales/nb/common.json b/locales/nb/common.json index 68f220ea2..2796da166 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -292,6 +292,7 @@ "forward": "Videresend", "delete": "Slett", "archive": "Arkiver", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Stjernemerk", "unstar": "Fjern stjerne", "mark_unread": "Merk som ulest", diff --git a/locales/nl/common.json b/locales/nl/common.json index 2c78d5afa..e77e89f2c 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -292,6 +292,7 @@ "forward": "Doorsturen", "delete": "Verwijderen", "archive": "Archiveren", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Ster toevoegen", "unstar": "Ster verwijderen", "mark_unread": "Markeren als ongelezen", diff --git a/locales/pl/common.json b/locales/pl/common.json index 805c56ff2..76b3b131e 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -292,6 +292,7 @@ "forward": "Przekaż dalej", "delete": "Usuń", "archive": "Archiwizuj", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Oznacz gwiazdką", "unstar": "Usuń gwiazdkę", "mark_unread": "Oznacz jako nieprzeczytane", diff --git a/locales/pt/common.json b/locales/pt/common.json index 3d5208c45..c055f7bca 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -292,6 +292,7 @@ "forward": "Encaminhar", "delete": "Excluir", "archive": "Arquivar", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Adicionar Estrela", "unstar": "Remover Estrela", "mark_unread": "Marcar como não lido", diff --git a/locales/ro/common.json b/locales/ro/common.json index d76697c6e..b9856296f 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -292,6 +292,7 @@ "forward": "Redirecționează", "delete": "Șterge", "archive": "Arhivează", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Stea", "unstar": "Anulează marcarea cu stea", "mark_unread": "Marcați ca necitit", diff --git a/locales/ru/common.json b/locales/ru/common.json index 2f7877bd7..227ba48db 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -292,6 +292,7 @@ "forward": "Переслать", "delete": "Удалить", "archive": "Архивировать", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Пометить", "unstar": "Снять пометку", "mark_unread": "Отметить непрочитанным", diff --git a/locales/sk/common.json b/locales/sk/common.json index 9e13b0b46..5992abcbb 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -292,6 +292,7 @@ "forward": "Preposlať", "delete": "Odstrániť", "archive": "Archivovať", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Označiť hviezdičkou", "unstar": "Odstrániť hviezdičku", "mark_unread": "Označiť ako neprečítané", diff --git a/locales/tr/common.json b/locales/tr/common.json index 655cdedfa..b8b0b9374 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -292,6 +292,7 @@ "forward": "İlet", "delete": "Sil", "archive": "Arşivle", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "Yıldızla", "unstar": "Yıldızı Kaldır", "mark_unread": "Okunmadı olarak işaretle", diff --git a/locales/uk/common.json b/locales/uk/common.json index b64bea073..b3ceca7fa 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -292,6 +292,7 @@ "forward": "вперед", "delete": "Видалити", "archive": "Архів", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "зірка", "unstar": "Зняти зірочку", "mark_unread": "Позначити як непрочитане", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index cea9bddf4..a6c827772 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -289,6 +289,7 @@ "forward": "轉寄", "delete": "刪除", "archive": "封存", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "加上星號", "unstar": "移除星號", "mark_unread": "標示為未讀", diff --git a/locales/zh/common.json b/locales/zh/common.json index 78c3391d9..96ea3a4c9 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -292,6 +292,7 @@ "forward": "转发", "delete": "删除", "archive": "归档", + "archive_mailbox_not_found": "Archive mailbox not found - cannot archive email", "star": "加星标", "unstar": "取消星标", "mark_unread": "标记为未读", diff --git a/stores/__tests__/email-store-archive-missing.test.ts b/stores/__tests__/email-store-archive-missing.test.ts new file mode 100644 index 000000000..cce2d45f5 --- /dev/null +++ b/stores/__tests__/email-store-archive-missing.test.ts @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useEmailStore, ArchiveMailboxNotFoundError } from '../email-store'; +import { useAuthStore } from '../auth-store'; +import { useSettingsStore } from '@/stores/settings-store'; +import type { Email, Mailbox } from '@/lib/jmap/types'; +import type { IJMAPClient } from '@/lib/jmap/client-interface'; + +// #578: archiving with no archive folder used to return silently, leaving the +// user with a shortcut/button that did nothing. The store must now report it. + +function makeMailbox(overrides: Partial = {}): Mailbox { + return { + id: 'inbox', + name: 'Inbox', + sortOrder: 0, + totalEmails: 0, + unreadEmails: 0, + totalThreads: 0, + unreadThreads: 0, + myRights: { + mayReadItems: true, + mayAddItems: true, + mayRemoveItems: true, + maySetSeen: true, + maySetKeywords: true, + mayCreateChild: true, + mayRename: true, + mayDelete: true, + maySubmit: true, + }, + isSubscribed: true, + isShared: false, + ...overrides, + } as Mailbox; +} + +function makeEmail(overrides: Partial = {}): Email { + return { + id: 'email-1', + threadId: 'thread-1', + subject: 'Hi', + receivedAt: new Date().toISOString(), + keywords: {}, + mailboxIds: {}, + ...overrides, + } as Email; +} + +describe('batchArchive without an archive mailbox (#578)', () => { + let client: IJMAPClient; + + beforeEach(() => { + client = { + batchArchiveEmails: vi.fn().mockResolvedValue(undefined), + getEmails: vi.fn().mockResolvedValue({ emails: [], hasMore: false, total: 0 }), + getMailboxes: vi.fn().mockResolvedValue([]), + } as unknown as IJMAPClient; + + useAuthStore.setState({ + activeAccountId: 'account-a', + getClientForAccount: (id: string) => (id === 'account-a' ? client : undefined) as never, + } as never); + useSettingsStore.setState({ archiveMode: 'single' } as never); + + useEmailStore.setState({ + isUnifiedView: false, + unifiedRole: null, + viewingAccountId: null, + selectedMailbox: 'a-inbox', + error: null, + mailboxes: [ + makeMailbox({ id: 'a-inbox', role: 'inbox' }), + makeMailbox({ id: 'a-trash', name: 'Trash', role: 'trash' }), + ], + accountMailboxes: {}, + emails: [makeEmail({ id: 'e1', mailboxIds: { 'a-inbox': true } })], + selectedEmailIds: new Set(['e1']), + }); + }); + + it('rejects with ArchiveMailboxNotFoundError and records the error', async () => { + await expect(useEmailStore.getState().batchArchive(client)).rejects.toBeInstanceOf( + ArchiveMailboxNotFoundError, + ); + + expect(useEmailStore.getState().error).toMatch(/archive mailbox not found/i); + expect(client.batchArchiveEmails).not.toHaveBeenCalled(); + // Nothing was archived, so the selection must survive. + expect(useEmailStore.getState().selectedEmailIds.has('e1')).toBe(true); + }); +}); diff --git a/stores/email-store.ts b/stores/email-store.ts index dba739d88..4d6391d61 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -566,6 +566,19 @@ function resolveActionClient(passedClient: IJMAPClient): IJMAPClient { return c ?? passedClient; } +/** + * Thrown by archive actions when no archive folder exists in the target + * account. Callers map it to the translated + * `email_viewer.archive_mailbox_not_found` toast; a silent return left the + * user with a shortcut/button that did nothing. (#578) + */ +export class ArchiveMailboxNotFoundError extends Error { + constructor() { + super('Archive mailbox not found - cannot archive email'); + this.name = 'ArchiveMailboxNotFoundError'; + } +} + function resolveActionMailboxes(): Mailbox[] { const state = useEmailStore.getState(); if (state.viewingAccountId) { @@ -3087,7 +3100,11 @@ export const useEmailStore = create((set, get) => ({ const archiveMailbox = mailboxes.find(m => isArchive(m) && (viewAccountId ? m.accountId === viewAccountId : !m.isShared) ); - if (!archiveMailbox) return; + if (!archiveMailbox) { + const error = new ArchiveMailboxNotFoundError(); + set({ error: error.message }); + throw error; + } const mode = useSettingsStore.getState().archiveMode; const archiveId = archiveMailbox.originalId || archiveMailbox.id;