feat: make MAIL FROM subdomain configurable via MAIL_FROM_SUBDOMAIN env var

This commit is contained in:
Tania Sanz
2026-05-12 20:35:53 +02:00
parent 715961c007
commit e0bf0f628a
7 changed files with 36 additions and 10 deletions
+7
View File
@@ -52,6 +52,13 @@ SES_CONFIGURATION_SET=plunk-configuration-set
# When set, projects can choose to disable email tracking # When set, projects can choose to disable email tracking
SES_CONFIGURATION_SET_NO_TRACKING=plunk-no-tracking-configuration-set SES_CONFIGURATION_SET_NO_TRACKING=plunk-no-tracking-configuration-set
# Custom MAIL FROM subdomain — used to construct `<subdomain>.<your-domain>`
# when a domain is added. Defaults to `plunk`. Override when `plunk.<your-domain>`
# is already used for something else (e.g. an R2/CDN custom domain), since the
# MAIL FROM hostname needs MX + TXT records that can't coexist with a CNAME.
# Example: MAIL_FROM_SUBDOMAIN=emails → emails.<your-domain>
# MAIL_FROM_SUBDOMAIN=
# ======================================== # ========================================
# OPTIONAL: OAuth Login # OPTIONAL: OAuth Login
# ======================================== # ========================================
+6
View File
@@ -45,6 +45,12 @@ export const AWS_SES_REGION = validateEnv('AWS_SES_REGION');
export const AWS_SES_ACCESS_KEY_ID = validateEnv('AWS_SES_ACCESS_KEY_ID'); export const AWS_SES_ACCESS_KEY_ID = validateEnv('AWS_SES_ACCESS_KEY_ID');
export const AWS_SES_SECRET_ACCESS_KEY = validateEnv('AWS_SES_SECRET_ACCESS_KEY'); export const AWS_SES_SECRET_ACCESS_KEY = validateEnv('AWS_SES_SECRET_ACCESS_KEY');
// Custom MAIL FROM subdomain used to construct `<subdomain>.<your-domain>`
// when a domain is added. Defaults to `plunk`. Override when `plunk.<your-domain>`
// is already used for something else (e.g. a CDN), since the MAIL FROM hostname
// needs MX + TXT records that can't coexist with a CNAME.
export const MAIL_FROM_SUBDOMAIN = validateEnv('MAIL_FROM_SUBDOMAIN', '').trim() || 'plunk';
// Email Processing Rate Limit (optional override) // Email Processing Rate Limit (optional override)
// If not set, will automatically fetch from AWS SES account quota // If not set, will automatically fetch from AWS SES account quota
// Set this to override AWS quota (useful for setting lower limits or testing) // Set this to override AWS quota (useful for setting lower limits or testing)
+2
View File
@@ -8,6 +8,7 @@ import {
GITHUB_OAUTH_ENABLED, GITHUB_OAUTH_ENABLED,
GOOGLE_OAUTH_ENABLED, GOOGLE_OAUTH_ENABLED,
LANDING_URI, LANDING_URI,
MAIL_FROM_SUBDOMAIN,
NODE_ENV, NODE_ENV,
S3_ENABLED, S3_ENABLED,
SMTP_DOMAIN, SMTP_DOMAIN,
@@ -62,6 +63,7 @@ export class Config {
}, },
aws: { aws: {
sesRegion: AWS_SES_REGION, sesRegion: AWS_SES_REGION,
mailFromSubdomain: MAIL_FROM_SUBDOMAIN,
}, },
}); });
} }
+6 -2
View File
@@ -6,6 +6,7 @@ import {
AWS_SES_REGION, AWS_SES_REGION,
AWS_SES_SECRET_ACCESS_KEY, AWS_SES_SECRET_ACCESS_KEY,
DASHBOARD_URI, DASHBOARD_URI,
MAIL_FROM_SUBDOMAIN,
SES_CONFIGURATION_SET, SES_CONFIGURATION_SET,
SES_CONFIGURATION_SET_NO_TRACKING, SES_CONFIGURATION_SET_NO_TRACKING,
TRACKING_TOGGLE_ENABLED, TRACKING_TOGGLE_ENABLED,
@@ -250,10 +251,13 @@ export const verifyDomain = async (domain: string): Promise<string[]> => {
// Verify DKIM for the domain // Verify DKIM for the domain
const DKIM = await ses.verifyDomainDkim({Domain: domain}); const DKIM = await ses.verifyDomainDkim({Domain: domain});
// Set custom MAIL FROM domain (plunk.yourdomain.com) // Set custom MAIL FROM domain. The subdomain defaults to `plunk` and can be
// overridden via the MAIL_FROM_SUBDOMAIN env var — useful when `plunk.<domain>`
// is already in use for something else (e.g., a CNAME to a CDN), since the
// MAIL FROM subdomain needs MX + TXT records that conflict with a CNAME.
await ses.setIdentityMailFromDomain({ await ses.setIdentityMailFromDomain({
Identity: domain, Identity: domain,
MailFromDomain: `plunk.${domain}`, MailFromDomain: `${MAIL_FROM_SUBDOMAIN}.${domain}`,
}); });
return DKIM.DkimTokens ?? []; return DKIM.DkimTokens ?? [];
+10 -8
View File
@@ -335,6 +335,8 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
<div className="space-y-4"> <div className="space-y-4">
{domains.map(domain => { {domains.map(domain => {
const status = getDomainStatus(domain); const status = getDomainStatus(domain);
const mailFromSubdomain = config?.aws?.mailFromSubdomain ?? 'plunk';
const mailFromHost = `${mailFromSubdomain}.${domain.domain}`;
return ( return (
<div key={domain.id} className="border border-neutral-200 rounded-lg p-4"> <div key={domain.id} className="border border-neutral-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
@@ -494,8 +496,8 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
</Badge> </Badge>
</div> </div>
<p className="text-xs text-neutral-600 mb-2"> <p className="text-xs text-neutral-600 mb-2">
Set up a custom MAIL FROM domain (plunk.{domain.domain}) to improve deliverability and Set up a custom MAIL FROM domain ({mailFromHost}) to improve deliverability and handle
handle bounces/complaints. bounces/complaints.
</p> </p>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -522,16 +524,16 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
<td className="py-3 px-3"> <td className="py-3 px-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="text-xs font-mono text-neutral-700 break-all flex-1"> <code className="text-xs font-mono text-neutral-700 break-all flex-1">
plunk.{domain.domain} {mailFromHost}
</code> </code>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleCopyToken(`plunk.${domain.domain}`, 3000)} onClick={() => handleCopyToken(mailFromHost, 3000)}
className="shrink-0 h-6 w-6 p-0 overflow-hidden" className="shrink-0 h-6 w-6 p-0 overflow-hidden"
> >
<AnimatedCopyIcon <AnimatedCopyIcon
isCopied={copiedToken === `plunk.${domain.domain}-3000`} isCopied={copiedToken === `${mailFromHost}-3000`}
/> />
</Button> </Button>
</div> </div>
@@ -571,16 +573,16 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
<td className="py-3 px-3"> <td className="py-3 px-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="text-xs font-mono text-neutral-700 break-all flex-1"> <code className="text-xs font-mono text-neutral-700 break-all flex-1">
plunk.{domain.domain} {mailFromHost}
</code> </code>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleCopyToken(`plunk.${domain.domain}`, 3001)} onClick={() => handleCopyToken(mailFromHost, 3001)}
className="shrink-0 h-6 w-6 p-0 overflow-hidden" className="shrink-0 h-6 w-6 p-0 overflow-hidden"
> >
<AnimatedCopyIcon <AnimatedCopyIcon
isCopied={copiedToken === `plunk.${domain.domain}-3001`} isCopied={copiedToken === `${mailFromHost}-3001`}
/> />
</Button> </Button>
</div> </div>
+1
View File
@@ -21,6 +21,7 @@ export interface ConfigResponse {
}; };
aws: { aws: {
sesRegion: string; sesRegion: string;
mailFromSubdomain: string;
}; };
} }
+4
View File
@@ -154,6 +154,10 @@ services:
SES_CONFIGURATION_SET: ${SES_CONFIGURATION_SET} SES_CONFIGURATION_SET: ${SES_CONFIGURATION_SET}
SES_CONFIGURATION_SET_NO_TRACKING: ${SES_CONFIGURATION_SET_NO_TRACKING:-} SES_CONFIGURATION_SET_NO_TRACKING: ${SES_CONFIGURATION_SET_NO_TRACKING:-}
# Custom MAIL FROM subdomain (defaults to 'plunk'; override when
# plunk.<your-domain> is already used for something else, e.g. a CDN)
MAIL_FROM_SUBDOMAIN: ${MAIL_FROM_SUBDOMAIN:-}
# Optional: OAuth # Optional: OAuth
GITHUB_OAUTH_CLIENT: ${GITHUB_OAUTH_CLIENT:-} GITHUB_OAUTH_CLIENT: ${GITHUB_OAUTH_CLIENT:-}
GITHUB_OAUTH_SECRET: ${GITHUB_OAUTH_SECRET:-} GITHUB_OAUTH_SECRET: ${GITHUB_OAUTH_SECRET:-}