feat: show the sender's BIMI logo in the list, the unified inbox and the reader

This commit is contained in:
Linus Rath
2026-10-31 15:50:02 +01:00
parent ff01246c92
commit 6b7e752c4b
5 changed files with 49 additions and 1 deletions
@@ -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(
/<SenderAvatar [^>]*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/);
});
});
+2
View File
@@ -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)}
/>
</Pressable>
<View style={styles.info}>
+2 -1
View File
@@ -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 (
<Pressable onPress={onToggleExpanded} style={({ pressed }) => [styles.collapsed, pressed && styles.pressed]}>
<SenderAvatar name={from?.name} email={from?.email} size={componentSizes.avatarSm} senderWarning={senderWarning} />
<SenderAvatar name={from?.name} email={from?.email} size={componentSizes.avatarSm} senderWarning={senderWarning} brandDomain={brandIndicatorDomain(email)} />
<View style={styles.collapsedInfo}>
<View style={styles.collapsedTop}>
<Text style={[styles.collapsedName, unread && styles.unread]} numberOfLines={1}>
+4
View File
@@ -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 (
<Pressable
@@ -272,6 +275,7 @@ const EmailRow = React.memo(function EmailRow({
size={componentSizes.avatarMd}
disableImages={disableAvatarImages}
senderWarning={senderWarning}
brandDomain={brandDomain}
/>
</Pressable>
)}
+2
View File
@@ -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 && (
<View style={[styles.accountDot, { backgroundColor: acc.avatarColor }]} />