feat(compose): From override + catch-all auto-reply (fixes #246)

Adds an Override toggle in the composer's From row. When enabled, name
and address become free-text inputs. Mail is still submitted through the
selected identity, but the outgoing message's From: header — and the
SMTP envelope MAIL FROM when different — is set from the override.

The existing "Auto-select Reply Address" setting is extended: if the
incoming message was addressed to an alias on a domain that matches one
of your identities but isn't itself an identity (classic domain catch-
all), it now auto-enables Override and pre-fills the alias. Quick reply
honors the same resolution. The setting is relabeled to reflect the
broader behavior.

JMAP: client.sendEmail gains an optional envelopeMailFrom; when set, the
EmailSubmission includes an explicit envelope with that mailFrom and the
to/cc/bcc as rcptTo so header-From and envelope can diverge (JMAP §7.3).

S/MIME: override is incompatible with sign/encrypt and is refused with a
clear error — signing a different visible From from the identity's
certificate Subject would produce messages clients reject.

Tests: resolveReplyFrom covers exact match, sub-address stripping,
catch-all detection, identity preference, and foreign-domain null.
This commit is contained in:
Augustin Marcin
2026-05-11 12:13:47 +02:00
committed by Linus Rath
parent 2d7e24b513
commit b0640c9ecc
8 changed files with 297 additions and 40 deletions
+1
View File
@@ -146,6 +146,7 @@ export interface IJMAPClient {
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>,
inReplyTo?: string[],
references?: string[],
envelopeMailFrom?: string,
): Promise<void>;
sendImipReply(opts: {
+19 -3
View File
@@ -2089,7 +2089,8 @@ export class JMAPClient implements IJMAPClient {
htmlBody?: string,
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>,
inReplyTo?: string[],
references?: string[]
references?: string[],
envelopeMailFrom?: string
): Promise<void> {
const emailId = `send-${Date.now()}`;
const mailboxes = await this.getMailboxes();
@@ -2185,6 +2186,21 @@ export class JMAPClient implements IJMAPClient {
},
};
// When an explicit envelope MAIL FROM is provided (header From ≠ envelope,
// e.g. sending from a domain-catch-all alias without a dedicated Identity),
// set the EmailSubmission envelope explicitly. JMAP §7.3: when `envelope`
// is omitted the server derives mailFrom from the Identity.
const submissionCreate = (submissionId: string): Record<string, unknown> => {
const create: Record<string, unknown> = { emailId: `#${emailId}`, identityId: finalIdentityId };
if (envelopeMailFrom) {
create.envelope = {
mailFrom: { email: envelopeMailFrom },
rcptTo: [...to, ...(cc || []), ...(bcc || [])].map((email) => ({ email })),
};
}
return { [submissionId]: create };
};
if (draftId) {
// Destroy the old draft and create a new email with the final body
methodCalls.push(["Email/set", {
@@ -2197,7 +2213,7 @@ export class JMAPClient implements IJMAPClient {
}, "1"]);
methodCalls.push(["EmailSubmission/set", {
accountId: this.accountId,
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
create: submissionCreate("1"),
onSuccessUpdateEmail,
}, "2"]);
} else {
@@ -2207,7 +2223,7 @@ export class JMAPClient implements IJMAPClient {
}, "0"]);
methodCalls.push(["EmailSubmission/set", {
accountId: this.accountId,
create: { "1": { emailId: `#${emailId}`, identityId: finalIdentityId } },
create: submissionCreate("1"),
onSuccessUpdateEmail,
}, "1"]);
}