diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index 589d5315..77af5192 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -3084,6 +3084,11 @@ export default function Home() { { if (!autoSelectReplyIdentity) return; if (selectedIdentityId || initialData?.selectedIdentityId) return; + + // New message started from a specific mailbox/account: default the From to + // that mailbox's identity instead of the primary one, so composing while + // viewing info@ sends as info@. Reply/forward fall through to the + // recipient-based resolution below. + if (mode === 'compose') { + const composeIdentityId = findComposeIdentityId(identities, composeFromAccountEmail); + if (composeIdentityId) { + setSelectedIdentityId(composeIdentityId); + } + return; + } + if (mode !== 'reply' && mode !== 'replyAll') return; const resolved = resolveReplyFrom(identities, { @@ -708,6 +731,7 @@ export function EmailComposer({ } }, [ autoSelectReplyIdentity, + composeFromAccountEmail, fromOverrideEnabled, identities, initialData?.selectedIdentityId, diff --git a/lib/__tests__/reply-identity.test.ts b/lib/__tests__/reply-identity.test.ts index 4cb3bdf8..c883d1d1 100644 --- a/lib/__tests__/reply-identity.test.ts +++ b/lib/__tests__/reply-identity.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { findReplyIdentityId, resolveReplyFrom } from '../reply-identity'; +import { findComposeIdentityId, findReplyIdentityId, resolveReplyFrom } from '../reply-identity'; import type { Identity } from '../jmap/types'; const identities: Identity[] = [ @@ -51,6 +51,29 @@ describe('findReplyIdentityId', () => { }); }); +describe('findComposeIdentityId', () => { + it('matches the identity of the active mailbox', () => { + expect(findComposeIdentityId(identities, 'harry@secondary.com')).toBe('secondary'); + }); + + it('matches case-insensitively', () => { + expect(findComposeIdentityId(identities, 'HARRY@PRIMARY.COM')).toBe('primary'); + }); + + it('strips +tag before matching', () => { + expect(findComposeIdentityId(identities, 'harry+news@secondary.com')).toBe('secondary'); + }); + + it('returns null when the active mailbox has no matching identity', () => { + expect(findComposeIdentityId(identities, 'other@example.com')).toBeNull(); + }); + + it('returns null when no active mailbox email is given', () => { + expect(findComposeIdentityId(identities, undefined)).toBeNull(); + expect(findComposeIdentityId(identities, '')).toBeNull(); + }); +}); + describe('resolveReplyFrom', () => { it('returns the matching identity with no override when exact match', () => { expect(resolveReplyFrom(identities, { to: [{ email: 'harry@secondary.com' }] })) diff --git a/lib/reply-identity.ts b/lib/reply-identity.ts index bd5b9dee..1040fbe1 100644 --- a/lib/reply-identity.ts +++ b/lib/reply-identity.ts @@ -67,6 +67,34 @@ export function findReplyIdentityId( return baseIdentity?.id ?? null; } +/** + * Pick the identity to use for a NEW message started while viewing a specific + * mailbox/account. Matches the active mailbox's address to a configured + * identity (exact, then `+tag`-stripped) so composing from info@ defaults its + * From to info@. Returns `null` when no address is given or none matches, so + * the caller keeps the primary identity. + */ +export function findComposeIdentityId( + identities: Identity[], + accountEmail?: string | null, +): string | null { + const email = accountEmail?.trim(); + if (identities.length === 0 || !email) { + return null; + } + + const exact = normalizeEmailAddress(email); + const exactIdentity = identities.find((identity) => normalizeEmailAddress(identity.email) === exact); + if (exactIdentity) { + return exactIdentity.id; + } + + const base = normalizeBaseEmailAddress(email); + const baseIdentity = identities.find((identity) => normalizeBaseEmailAddress(identity.email) === base); + + return baseIdentity?.id ?? null; +} + export interface ReplyFromResolution { /** Identity to use for JMAP `identityId` and the SMTP envelope MAIL FROM. */ identityId: string;