diff --git a/src/components/email/MessageHeader.tsx b/src/components/email/MessageHeader.tsx index ad236b1b..123e0e67 100644 --- a/src/components/email/MessageHeader.tsx +++ b/src/components/email/MessageHeader.tsx @@ -12,7 +12,7 @@ import { useLocaleStore } from '../../stores/locale-store'; import { useAccountKeywordDefs } from '../../stores/keywords-store'; import { emailDisplayDate, formatHeaderDate, formatHeaderTime, formatFullDateTime } from '../../lib/email-date'; import { - deriveHeaderInfo, deliveryDeltaMs, formatDelta, isAuthenticationSpoofed, findReceivingIdentity, + deriveHeaderInfo, deliveryDeltaMs, formatDelta, isAuthenticationSpoofed, findReceivingIdentity, viaIdentityNameShown, getSenderVerification, describeSenderVerification, type AuthenticationResults, type EmailHeaderInfo, } from '../../lib/email-headers'; @@ -138,7 +138,7 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA if (spoofed || identities.length === 0) return null; const fromEmail = from?.email?.toLowerCase(); const sentAs = fromEmail ? identities.find((i) => i.email?.toLowerCase() === fromEmail) : undefined; - if (sentAs) return { identity: sentAs, direction: 'from' as const }; + if (sentAs) return viaIdentityNameShown(sentAs, from) ? { identity: sentAs, direction: 'from' as const } : null; const received = findReceivingIdentity(identities, email); if (received && identities.length > 1 && received.id !== identities[0].id) { return { identity: received, direction: 'to' as const }; @@ -258,7 +258,7 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA {viaIdentity.direction === 'from' - ? t('identities.badge.identity_short', 'via {name}', { name: viaIdentity.identity.email }) + ? t('identities.badge.identity_short', 'via {name}', { name: viaIdentity.identity.name }) : t('email_viewer.received_via', 'to {name}', { name: viaIdentity.identity.email })} diff --git a/src/lib/__tests__/via-badge.test.ts b/src/lib/__tests__/via-badge.test.ts new file mode 100644 index 00000000..e9d77b24 --- /dev/null +++ b/src/lib/__tests__/via-badge.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { viaIdentityNameShown } from '../email-headers'; + +// Mail sent as one of the user's identities read "Ann " with a +// "via ann@x" badge: the sender's address a second time (webmail +// email-identity-badge.tsx shows the identity's name, and only when it adds +// something). + +describe('"via" badge', () => { + it("shows the identity's name only when it differs from what the sender line says", () => { + const from = { name: 'Ann Smith', email: 'ann@example.org' }; + expect(viaIdentityNameShown({ name: 'Support', email: 'ann@example.org' }, from)).toBe(true); + expect(viaIdentityNameShown({ name: 'Ann Smith', email: 'ann@example.org' }, from)).toBe(false); + expect(viaIdentityNameShown({ name: 'ann@example.org', email: 'ann@example.org' }, from)).toBe(false); + expect(viaIdentityNameShown({ name: '', email: 'ann@example.org' }, from)).toBe(false); + }); + + it("names the identity, not its address, in the reader's badge", () => { + const header = fs.readFileSync(path.resolve(__dirname, '../../components/email/MessageHeader.tsx'), 'utf8'); + expect(header).toMatch(/'via \{name\}', \{ name: viaIdentity\.identity\.name \}/); + expect(header).toMatch(/viaIdentityNameShown\(sentAs, from\)/); + }); +}); diff --git a/src/lib/email-headers.ts b/src/lib/email-headers.ts index 767e5007..a7bf2688 100644 --- a/src/lib/email-headers.ts +++ b/src/lib/email-headers.ts @@ -97,6 +97,21 @@ const SPF_SEVERITY: Record = { pass: 0, }; +/** + * Whether "via " on a message sent as one of the user's identities + * tells the reader anything: only when the identity's name differs from its + * address, the From address and the name already shown as the sender, so it + * no longer reads "ann@x via ann@x" (webmail email-identity-badge.tsx). + */ +export function viaIdentityNameShown( + identity: Pick, + from: { name?: string | null; email?: string | null } | undefined, +): boolean { + const name = identity.name?.trim(); + if (!name) return false; + return name !== identity.email && name !== from?.email && name !== from?.name; +} + /** * Whether the authentication results indicate the visible From identity can't * be trusted (i.e. the message is likely spoofed). Used to suppress UI that