From 6b7e752c4b4dea0a06f09c31f69c3313de378b43 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:50:02 +0100 Subject: [PATCH] feat: show the sender's BIMI logo in the list, the unified inbox and the reader --- src/components/__tests__/bimi-wiring.test.ts | 39 ++++++++++++++++++++ src/components/email/MessageHeader.tsx | 2 + src/components/email/ThreadMessageCard.tsx | 3 +- src/screens/EmailListScreen.tsx | 4 ++ src/screens/UnifiedInboxScreen.tsx | 2 + 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/components/__tests__/bimi-wiring.test.ts diff --git a/src/components/__tests__/bimi-wiring.test.ts b/src/components/__tests__/bimi-wiring.test.ts new file mode 100644 index 00000000..2557b868 --- /dev/null +++ b/src/components/__tests__/bimi-wiring.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +// BIMI sender logos (webmail components/ui/avatar.tsx brandDomain, used by +// email-list-item, thread-list-item, email-viewer and thread-conversation-view). +// No React Native renderer in the test env, so this checks that every avatar +// of a received message hands SenderAvatar its brand domain; the helper has +// unit tests in lib/__tests__/avatar-bimi.test.ts. + +const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8'); + +describe('BIMI logo wiring', () => { + it('gives list rows the brand domain, none on rows that show the recipient', () => { + const src = read('screens/EmailListScreen.tsx'); + expect(src).toMatch(/const brandDomain = React\.useMemo\(\(\) => \(showRecipient \? undefined : brandIndicatorDomain\(item\)\), \[showRecipient, item\]\);/); + expect(src).toMatch(/senderWarning=\{senderWarning\}\s+brandDomain=\{brandDomain\}/); + }); + + it('gives unified inbox rows the brand domain', () => { + const src = read('screens/UnifiedInboxScreen.tsx'); + expect(src).toMatch(/brandDomain=\{showRecipient \? undefined : brandIndicatorDomain\(item\)\}/); + }); + + it('gives the reader header and collapsed thread cards the brand domain', () => { + expect(read('components/email/MessageHeader.tsx')).toMatch(/brandDomain=\{brandIndicatorDomain\(email\)\}/); + expect(read('components/email/ThreadMessageCard.tsx')).toMatch( + /]*brandDomain=\{brandIndicatorDomain\(email\)\}/, + ); + }); + + it('fetches the Authentication-Results the helper reads', async () => { + const { EMAIL_FULL_PROPERTIES, EMAIL_LIST_PROPERTIES, THREAD_HEADER_PROPERTIES } = await import('../../api/email'); + expect(EMAIL_FULL_PROPERTIES).toContain('headers'); + expect(EMAIL_LIST_PROPERTIES).toContain('header:Authentication-Results:all'); + expect(THREAD_HEADER_PROPERTIES).toContain('header:Authentication-Results:all'); + expect(read('api/unified-inbox.ts')).toMatch(/const EMAIL_LIST_PROPERTIES = \[[^\]]*LIST_AUTH_RESULTS_PROPERTY/); + }); +}); diff --git a/src/components/email/MessageHeader.tsx b/src/components/email/MessageHeader.tsx index fa5101af..c8864877 100644 --- a/src/components/email/MessageHeader.tsx +++ b/src/components/email/MessageHeader.tsx @@ -25,6 +25,7 @@ import { resolveEmailTags } from '../../lib/thread-utils'; import { findVerificationCode, verificationCodeBodyText } from '../../lib/verification-code'; import { VerificationCodeChip } from './VerificationCodeChip'; import { useOwnDomainAddress } from '../../lib/use-own-domain-address'; +import { brandIndicatorDomain } from '../../lib/avatar-bimi'; interface Props { email: Email; @@ -185,6 +186,7 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA email={from?.email} size={compact ? componentSizes.avatarSm : componentSizes.avatarMd} senderWarning={verificationText?.label} + brandDomain={brandIndicatorDomain(email)} /> diff --git a/src/components/email/ThreadMessageCard.tsx b/src/components/email/ThreadMessageCard.tsx index 564dd9d4..63b9a8cd 100644 --- a/src/components/email/ThreadMessageCard.tsx +++ b/src/components/email/ThreadMessageCard.tsx @@ -15,6 +15,7 @@ import { emailDisplayDate, formatHeaderDate, formatHeaderTime } from '../../lib/ import { singleLine } from '../../lib/single-line'; import { cleanPreview } from '../../lib/preview-text'; import { useSenderWarning } from './use-sender-warning'; +import { brandIndicatorDomain } from '../../lib/avatar-bimi'; import { useOwnDomainAddress } from '../../lib/use-own-domain-address'; import type { CardReplyActions } from '../../lib/thread-card-actions'; @@ -61,7 +62,7 @@ export function ThreadMessageCard({ if (!expanded) { return ( [styles.collapsed, pressed && styles.pressed]}> - + diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx index ebbc5682..ae61ec09 100644 --- a/src/screens/EmailListScreen.tsx +++ b/src/screens/EmailListScreen.tsx @@ -32,6 +32,7 @@ import { import { showsPaperclip } from '../lib/list-attachments'; import { VerificationCodeChip, useListVerificationCode } from '../components/email/VerificationCodeChip'; import { useSenderWarning } from '../components/email/use-sender-warning'; +import { brandIndicatorDomain } from '../lib/avatar-bimi'; import type { LoadListAttachments } from '../lib/list-attachments'; import { useNetworkStore } from '../stores/network-store'; import { @@ -219,6 +220,8 @@ const EmailRow = React.memo(function EmailRow({ const attachments = useListAttachments(item, onOpenAttachment ? loadAttachments : undefined); // Sent and Drafts rows show the recipient: the check says nothing about them. const senderWarning = useSenderWarning(showRecipient ? undefined : item); + // The sender's BIMI logo, for a message that passed DMARC for its From domain. + const brandDomain = React.useMemo(() => (showRecipient ? undefined : brandIndicatorDomain(item)), [showRecipient, item]); return ( )} diff --git a/src/screens/UnifiedInboxScreen.tsx b/src/screens/UnifiedInboxScreen.tsx index 553e4c9b..274bcfbf 100644 --- a/src/screens/UnifiedInboxScreen.tsx +++ b/src/screens/UnifiedInboxScreen.tsx @@ -33,6 +33,7 @@ import { isPermanentDelete, confirmPermanentDelete } from '../lib/delete-confirm import { confirmMailRemoval } from '../lib/mail-removal-confirm'; import { onStateChangeType } from '../lib/state-change-bus'; import { describeSenderVerification, senderWarningOf } from '../lib/email-headers'; +import { brandIndicatorDomain } from '../lib/avatar-bimi'; import { createUnifiedReloadTracker } from '../lib/unified-reload'; import { spacing, typography, componentSizes, radius, type ThemePalette } from '../theme/tokens'; import { useColors } from '../theme/colors'; @@ -380,6 +381,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) { size={componentSizes.avatarMd} disableImages={role === 'junk' && !showAvatarsInJunk} senderWarning={verification ? describeSenderVerification(verification, t).label : undefined} + brandDomain={showRecipient ? undefined : brandIndicatorDomain(item)} /> {acc && accountIds.length > 1 && (