fix: Generate Message-ID client-side using the sender's domain
Bulwark currently sends Email/set create without a messageId property, leaving Message-ID generation to the JMAP server. Servers typically fall back to their OS hostname for this (Stalwart, via mail-builder's `gethostname()`), which produces IDs like: ``` <175234...abc@ip-10-0-12-97.ec2.internal> ``` This is bad for every deployment, in three escalating ways: 1. Information disclosure: the Message-ID travels in every outgoing message and permanently into archives, quoting, and In-Reply-To / References of replies. An internal hostname (container name, private DNS, k8s pod name) is infrastructure detail no recipient should see. 2. Deliverability: spam filters score Message-IDs whose domain part is not a plausible FQDN or is unrelated to the sender (SpamAssassin MSGID_FROM_MTA_HEADER and friends). Internal names like *.ec2.internal or bare container ids read as botnet-ish. 3. Correctness of intent: RFC 5322 §3.6.4 recommends the originator generate the Message-ID, using a domain it controls, so the id is meaningful and plausibly unique under that domain's authority. The sender's own domain is exactly that; the mail server's transient runtime hostname is exactly not. Generate the id in `sendEmail()` as `<epoch36>.<uuid>@<sender-domain>`, taken from the From address (falling back to the login username). The timestamp prefix keeps ids roughly sortable and adds entropy across UUID reuse concerns; crypto.randomUUID() is available in every runtime Bulwark supports (browsers and Node 19+). Per RFC 8621 §4.1.2.3 the JMAP messageId property carries bare msg-ids (no angle brackets), so none are added. Clients that never set messageId also can't thread their own sent mail reliably until the server echoes the message back; setting it at create time makes the id known and stable from the start. No behavior change for servers that honored client-provided ids all along; servers that previously synthesized an id now simply don't need to.
This commit is contained in:
@@ -432,6 +432,17 @@ function stripMessageIdBrackets(id: string): string {
|
||||
return id.trim().replace(/^<+/, '').replace(/>+$/, '').trim();
|
||||
}
|
||||
|
||||
// Generate a Message-ID for outgoing mail (bare msg-id, no angle brackets, per
|
||||
// RFC 8621 §4.1.2.3). Without one the server synthesizes it from its OS
|
||||
// hostname, which leaks internal names (e.g. @ip-10-0-12-97.ec2.internal) into
|
||||
// headers — an anti-spam signal and an information disclosure. Use the sender's
|
||||
// domain instead, matching what receivers expect a Message-ID to look like.
|
||||
function generateMessageId(fromEmail: string): string {
|
||||
const at = fromEmail.lastIndexOf('@');
|
||||
const domain = at > 0 ? fromEmail.slice(at + 1) : 'localhost';
|
||||
return `${Date.now().toString(36)}.${crypto.randomUUID()}@${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a CalendarEvent/query filter restricting results to the given
|
||||
* calendars. Stalwart implements the singular `inCalendar` condition (one
|
||||
@@ -2467,6 +2478,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
cc: cc?.length ? cc.map(parseRecipientString) : undefined,
|
||||
bcc: bcc?.length ? bcc.map(parseRecipientString) : undefined,
|
||||
subject,
|
||||
messageId: [generateMessageId(fromEmail || this.username)],
|
||||
inReplyTo: normalizedInReplyTo?.length ? normalizedInReplyTo : undefined,
|
||||
references: normalizedReferences?.length ? normalizedReferences : undefined,
|
||||
keywords: { "$seen": true, "$draft": true },
|
||||
|
||||
Reference in New Issue
Block a user