Add tagging of outgoing emails (#12570)

* Add tagging of outgoing emails

* Detect SES configured via well-known service key

The isSES check only matched "amazonaws" in the host, so SES configured
through SMTP_SERVICE (e.g. "SES" or "SES-US-EAST-1") was not detected and
tagging headers were not applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-06-03 23:51:09 -04:00
committed by GitHub
parent 0d50f0d60a
commit a21ddc4999
2 changed files with 85 additions and 0 deletions
+84
View File
@@ -12,17 +12,37 @@ import { baseStyles } from "./templates/components/EmailLayout";
const useTestEmailService = env.isDevelopment && !env.SMTP_USERNAME;
type SendMailOptions = {
/** The email address being sent to. */
to: string;
/** The address the email is sent from. */
from: EmailAddress;
/** An address to set as the reply-to for the email. */
replyTo?: string;
/** A unique identifier for the message, used for threading. */
messageId?: string;
/** Message IDs this email is a reply to, used for threading. */
references?: string[];
/** The subject line of the email. */
subject: string;
/** Preview text shown in email client list views. */
previewText?: string;
/** The plain-text version of the email body. */
text: string;
/** The React element rendered to produce the HTML body. */
component: JSX.Element;
/** Additional CSS to inject into the head of the email. */
headCSS?: string;
/** The URL used to unsubscribe from these emails. */
unsubscribeUrl?: string;
/** Tags used for reporting, where supported by the email provider. */
tags?: EmailTags;
};
type EmailTags = {
/** The broad category of the email, e.g. "notification". */
category: string;
/** The specific template name, e.g. "InviteEmail". */
template: string;
};
/**
@@ -167,6 +187,7 @@ export class Mailer {
references: data.references,
inReplyTo: data.references?.at(-1),
subject: data.subject,
headers: this.tagHeaders(data.tags),
html,
text: data.text,
list: data.unsubscribeUrl
@@ -200,6 +221,69 @@ export class Mailer {
}
};
/**
* Builds the provider-specific headers used to tag a message for reporting.
* Each supported provider expects a different header name and format; for
* providers that do not support tagging, or when no tags are given, no
* headers are returned.
*
* @param tags The tags to apply to the message.
* @returns A map of headers to set on the message, or undefined.
*/
private tagHeaders(
tags?: EmailTags
): Record<string, string | string[]> | undefined {
if (!tags) {
return undefined;
}
// Mailgun: up to three tags via repeated X-Mailgun-Tag headers.
// https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#tagging
if (this.isMailgun) {
return { "X-Mailgun-Tag": Object.values(tags).slice(0, 3) };
}
// SES: comma-separated name=value pairs via X-SES-MESSAGE-TAGS.
// https://docs.aws.amazon.com/ses/latest/dg/event-publishing-send-email.html
if (this.isSES) {
return {
"X-SES-MESSAGE-TAGS": Object.entries(tags)
.map(([name, value]) => `${name}=${value}`)
.join(", "),
};
}
// Postmark: a single tag per message via X-PM-Tag.
// https://postmarkapp.com/support/article/1117-add-link-tracking-to-a-message
if (this.isPostmark) {
return { "X-PM-Tag": tags.template };
}
return undefined;
}
/** The configured SMTP host and service name, for provider detection. */
private get provider(): string {
return `${env.SMTP_HOST ?? ""} ${env.SMTP_SERVICE ?? ""}`;
}
/** Whether the configured SMTP provider is Mailgun. */
private get isMailgun(): boolean {
return /mailgun/i.test(this.provider);
}
/** Whether the configured SMTP provider is Amazon SES. */
private get isSES(): boolean {
// Detected by the SES SMTP host (email-smtp.<region>.amazonaws.com) or a
// well-known Nodemailer service key (SES, SES-US-EAST-1, etc.).
return /amazonaws|(?:^|\s)ses\b/i.test(this.provider);
}
/** Whether the configured SMTP provider is Postmark. */
private get isPostmark(): boolean {
return /postmark/i.test(this.provider);
}
private getOptions(): SMTPTransport.Options {
// nodemailer will use the service config to determine host/port
if (env.SMTP_SERVICE) {
+1
View File
@@ -177,6 +177,7 @@ export default abstract class BaseEmail<
text: this.renderAsText(data),
headCSS: this.headCSS?.(data),
unsubscribeUrl: this.unsubscribeUrl?.(data),
tags: { category: this.category, template: templateName },
});
Metrics.increment("email.sent", {
templateName,