From 836dcba66ee6a288afdc3c8ac1c94d3f5f8a9895 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:20:26 +0100 Subject: [PATCH] fix: don't count senders whose images the user allowed as contacts for the external recipient confirm --- .../composer-external-recipients.test.tsx | 2 ++ lib/__tests__/external-mail.test.ts | 21 ++++++++++++++++++- lib/external-mail.ts | 15 ++++++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/components/email/__tests__/composer-external-recipients.test.tsx b/components/email/__tests__/composer-external-recipients.test.tsx index 4cdf10cd4..b2a9ddd76 100644 --- a/components/email/__tests__/composer-external-recipients.test.tsx +++ b/components/email/__tests__/composer-external-recipients.test.tsx @@ -118,6 +118,8 @@ vi.mock('@/stores/settings-store', () => { vi.mock('@/stores/contact-store', () => { const state = { contacts: [], + addressBooks: [], + trustedSendersBookId: null, getAutocomplete: async () => [], addToTrustedSendersBook: async () => {}, }; diff --git a/lib/__tests__/external-mail.test.ts b/lib/__tests__/external-mail.test.ts index be3449906..57ba06cfd 100644 --- a/lib/__tests__/external-mail.test.ts +++ b/lib/__tests__/external-mail.test.ts @@ -126,7 +126,7 @@ describe('the active account', () => { beforeEach(() => { setExternalMail({}); useAuthStore.setState({ identities: [] }); - useContactStore.setState({ contacts: [] }); + useContactStore.setState({ contacts: [], addressBooks: [], trustedSendersBookId: null }); }); it('reads the switches from the effective policy', () => { @@ -152,4 +152,23 @@ describe('the active account', () => { }); expect([...contactAddresses()].sort()).toEqual(['dave@org-two.test', 'second@x.test']); }); + + it('leaves out cards that are only in the Trusted Senders book', () => { + useContactStore.setState({ + addressBooks: [ + { id: 'book-main', name: 'Contacts' }, + { id: 'acc1::book-trusted', name: 'Trusted Senders' }, + ] as never, + trustedSendersBookId: 'book-trusted-raw', + contacts: [ + // Only "always load images" for this sender: not a contact. + { id: 'c1', addressBookIds: { 'acc1::book-trusted': true }, emails: { e: { address: 'newsletter@shop.example' } } }, + { id: 'c2', addressBookIds: { 'book-trusted-raw': true }, emails: { e: { address: 'promo@shop.example' } } }, + // In a real book too: a contact. + { id: 'c3', addressBookIds: { 'book-main': true, 'acc1::book-trusted': true }, emails: { e: { address: 'dave@org-two.test' } } }, + { id: 'c4', addressBookIds: { 'book-main': true }, emails: { e: { address: 'carol@partner.example' } } }, + ] as unknown as ContactCard[], + }); + expect([...contactAddresses()].sort()).toEqual(['carol@partner.example', 'dave@org-two.test']); + }); }); diff --git a/lib/external-mail.ts b/lib/external-mail.ts index 0fe756a9b..f3ecc101f 100644 --- a/lib/external-mail.ts +++ b/lib/external-mail.ts @@ -18,7 +18,7 @@ import { DEFAULT_ORGANISATION_POLICY, normalizeDomainList, type OrganisationPoli import { toAsciiDomain, toAsciiEmail } from '@/lib/idn'; import { usePolicyStore } from '@/stores/policy-store'; import { useAuthStore } from '@/stores/auth-store'; -import { useContactStore } from '@/stores/contact-store'; +import { TRUSTED_SENDERS_BOOK_NAME, useContactStore } from '@/stores/contact-store'; export type ExternalMailPolicy = OrganisationPolicy['externalMail']; @@ -125,10 +125,19 @@ export function currentInternalDomains(policy: ExternalMailPolicy = currentExter return internalDomains(identities.map((identity) => identity.email), policy.domains); } -/** The addresses of every loaded contact (all address books), as addressKey makes them. */ +/** + * The addresses of the loaded contacts, as addressKey makes them. A card + * that sits only in the "Trusted Senders" book is left out: that book is + * the list of senders whose images load, not people the user writes to. + */ export function contactAddresses(): Set { + const { contacts, addressBooks, trustedSendersBookId } = useContactStore.getState(); + const trustedBooks = new Set(addressBooks.filter((book) => book.name === TRUSTED_SENDERS_BOOK_NAME).map((book) => book.id)); + if (trustedSendersBookId) trustedBooks.add(trustedSendersBookId); const out = new Set(); - for (const contact of useContactStore.getState().contacts) { + for (const contact of contacts) { + const books = Object.keys(contact.addressBookIds ?? {}).filter((id) => contact.addressBookIds?.[id]); + if (books.length > 0 && books.every((id) => trustedBooks.has(id))) continue; for (const entry of Object.values(contact.emails ?? {})) { const address = entry?.address?.trim(); if (address) out.add(addressKey(address));