diff --git a/src/lib/__tests__/push-background-task.test.ts b/src/lib/__tests__/push-background-task.test.ts index 7f9e384b..d2f707a8 100644 --- a/src/lib/__tests__/push-background-task.test.ts +++ b/src/lib/__tests__/push-background-task.test.ts @@ -288,6 +288,40 @@ describe('pushBackgroundTask notifications', () => { ]); }); + it.each([ + ['trash-and-read', { mailboxIds: { 'trash-box': true }, 'keywords/$seen': true }], + ['trash', { mailboxIds: { 'trash-box': true } }], + // Never destroyed from a notification (#68), but not marked read either. + ['permanent', { mailboxIds: { 'trash-box': true } }], + ])('follows the delete action setting "%s" (#86)', async (deleteAction, patch) => { + await AsyncStorage.setItem('webmail:settings:v1', JSON.stringify({ deleteAction })); + const postCalls: any[] = []; + (secureFetch as ReturnType).mockImplementation(async (url: string, opts?: any) => { + if (opts?.body) postCalls.push(JSON.parse(opts.body)); + return { + ok: true, + json: async () => (url.endsWith('/.well-known/jmap') + ? { + apiUrl: 'https://mail.example.com/jmap/', + primaryAccounts: { 'urn:ietf:params:jmap:mail': 'jmap-primary' }, + accounts: { 'jmap-primary': {} }, + } + : { + methodResponses: [ + ['Mailbox/get', { list: [{ id: 'trash-box', role: 'trash' }] }, '0'], + ['Email/set', { updated: { m1: {} } }, '0'], + ], + }), + }; + }); + + await handleNotificationAction({ action: 'delete', emailId: 'm1', accountId: LOCAL }); + + expect(postCalls[1].methodCalls).toEqual([ + ['Email/set', { accountId: 'jmap-primary', update: { m1: patch } }, '0'], + ]); + }); + it.each([ ['has no trash mailbox', ['Mailbox/get', { list: [{ id: 'inbox', role: 'inbox' }] }, '0']], ['cannot list mailboxes', ['error', { type: 'accountNotFound' }, '0']], diff --git a/src/lib/push-background-task.ts b/src/lib/push-background-task.ts index 985a76e0..e5d1bd68 100644 --- a/src/lib/push-background-task.ts +++ b/src/lib/push-background-task.ts @@ -51,6 +51,7 @@ const EMAIL_PROPERTIES = [ interface PushPersistedSettings { emailNotificationsEnabled?: boolean; senderFavicons?: boolean; + deleteAction?: string; } async function emailNotificationsAllowed(): Promise { @@ -78,6 +79,20 @@ export async function senderFaviconsAllowed(): Promise { } } +// The "Delete Action" setting, as far as a notification's Delete honours it: +// "Move to Trash and mark as read" marks the message read on the way (#86). +// "Delete Permanently" still only moves to Trash: a notification never +// destroys mail (#68). +async function notificationDeleteMarksRead(): Promise { + try { + const raw = await AsyncStorage.getItem(SETTINGS_STORAGE_KEY); + if (!raw) return false; + return (JSON.parse(raw) as PushPersistedSettings).deleteAction === 'trash-and-read'; + } catch { + return false; + } +} + /** The app's language: the in-app override, else the device's. */ export async function notificationLocale(): Promise { try { @@ -665,13 +680,15 @@ export async function handleNotificationAction(data: unknown): Promise { console.warn('[push] no trash mailbox for notification delete', emailAccountId); return; } + const patch: Record = { mailboxIds: { [trash.id]: true } }; + if (await notificationDeleteMarksRead()) patch['keywords/$seen'] = true; const res = await jmapPost(session, [ [ 'Email/set', { accountId: emailAccountId, update: { - [emailId]: { mailboxIds: { [trash.id]: true } }, + [emailId]: patch, }, }, '0',