fix: report a missing archive mailbox instead of failing silently #578

This commit is contained in:
Linus Rath
2026-09-07 22:34:47 +02:00
parent e095805851
commit 2b32a0110b
30 changed files with 146 additions and 3 deletions
+10 -2
View File
@@ -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();
+1
View File
@@ -292,6 +292,7 @@
"forward": "إعادة توجيه",
"delete": "حذف",
"archive": "أرشفة",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "تمييز بنجمة",
"unstar": "إزالة النجمة",
"mark_unread": "تمييز كغير مقروءة",
+1
View File
@@ -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",
+1
View File
@@ -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é",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -292,6 +292,7 @@
"forward": "ارسال مجدد",
"delete": "حذف",
"archive": "بایگانی",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "ستاره‌دار کردن",
"unstar": "حذف ستاره",
"mark_unread": "علامت‌گذاری خوانده نشده",
+1
View File
@@ -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",
+1
View File
@@ -244,6 +244,7 @@
"forward": "העבר",
"delete": "מחיקה",
"archive": "ארכיון",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "כוכב",
"unstar": "הסר כוכב",
"mark_unread": "סמן כלא נקרא",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -292,6 +292,7 @@
"forward": "転送",
"delete": "削除",
"archive": "アーカイブ",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "スターを付ける",
"unstar": "スターを外す",
"mark_unread": "未読にする",
+1
View File
@@ -292,6 +292,7 @@
"forward": "전달",
"delete": "삭제",
"archive": "보관",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "별표 달기",
"unstar": "별표 해제",
"mark_unread": "읽지 않은 상태로 표시",
+1
View File
@@ -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",
+1
View File
@@ -292,6 +292,7 @@
"forward": "Урагшаа",
"delete": "Устгах",
"archive": "Архив",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "од",
"unstar": "Одгүй болгох",
"mark_unread": "Уншаагүй гэж тэмдэглэх",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -292,6 +292,7 @@
"forward": "Переслать",
"delete": "Удалить",
"archive": "Архивировать",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "Пометить",
"unstar": "Снять пометку",
"mark_unread": "Отметить непрочитанным",
+1
View File
@@ -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é",
+1
View File
@@ -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",
+1
View File
@@ -292,6 +292,7 @@
"forward": "вперед",
"delete": "Видалити",
"archive": "Архів",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "зірка",
"unstar": "Зняти зірочку",
"mark_unread": "Позначити як непрочитане",
+1
View File
@@ -289,6 +289,7 @@
"forward": "轉寄",
"delete": "刪除",
"archive": "封存",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "加上星號",
"unstar": "移除星號",
"mark_unread": "標示為未讀",
+1
View File
@@ -292,6 +292,7 @@
"forward": "转发",
"delete": "删除",
"archive": "归档",
"archive_mailbox_not_found": "Archive mailbox not found - cannot archive email",
"star": "加星标",
"unstar": "取消星标",
"mark_unread": "标记为未读",
@@ -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> = {}): 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> = {}): 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);
});
});
+18 -1
View File
@@ -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<EmailStore>((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;