diff --git a/.env.self-host.example b/.env.self-host.example index 32d5ab2..3da8174 100644 --- a/.env.self-host.example +++ b/.env.self-host.example @@ -52,6 +52,13 @@ SES_CONFIGURATION_SET=plunk-configuration-set # When set, projects can choose to disable email tracking SES_CONFIGURATION_SET_NO_TRACKING=plunk-no-tracking-configuration-set +# Custom MAIL FROM subdomain — used to construct `.` +# when a domain is added. Defaults to `plunk`. Override when `plunk.` +# 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. +# MAIL_FROM_SUBDOMAIN= + # ======================================== # OPTIONAL: OAuth Login # ======================================== diff --git a/apps/api/src/app/constants.ts b/apps/api/src/app/constants.ts index 9ce2962..face47b 100644 --- a/apps/api/src/app/constants.ts +++ b/apps/api/src/app/constants.ts @@ -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_SECRET_ACCESS_KEY = validateEnv('AWS_SES_SECRET_ACCESS_KEY'); +// Custom MAIL FROM subdomain used to construct `.` +// when a domain is added. Defaults to `plunk`. Override when `plunk.` +// 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) // If not set, will automatically fetch from AWS SES account quota // Set this to override AWS quota (useful for setting lower limits or testing) diff --git a/apps/api/src/controllers/Config.ts b/apps/api/src/controllers/Config.ts index 89d458c..99e45cb 100644 --- a/apps/api/src/controllers/Config.ts +++ b/apps/api/src/controllers/Config.ts @@ -8,6 +8,7 @@ import { GITHUB_OAUTH_ENABLED, GOOGLE_OAUTH_ENABLED, LANDING_URI, + MAIL_FROM_SUBDOMAIN, NODE_ENV, S3_ENABLED, SMTP_DOMAIN, @@ -62,6 +63,7 @@ export class Config { }, aws: { sesRegion: AWS_SES_REGION, + mailFromSubdomain: MAIL_FROM_SUBDOMAIN, }, }); } diff --git a/apps/api/src/services/SESService.ts b/apps/api/src/services/SESService.ts index 2a8eacd..8a91b19 100644 --- a/apps/api/src/services/SESService.ts +++ b/apps/api/src/services/SESService.ts @@ -6,6 +6,7 @@ import { AWS_SES_REGION, AWS_SES_SECRET_ACCESS_KEY, DASHBOARD_URI, + MAIL_FROM_SUBDOMAIN, SES_CONFIGURATION_SET, SES_CONFIGURATION_SET_NO_TRACKING, TRACKING_TOGGLE_ENABLED, @@ -250,10 +251,13 @@ export const verifyDomain = async (domain: string): Promise => { // Verify DKIM for the 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.` + // 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({ Identity: domain, - MailFromDomain: `plunk.${domain}`, + MailFromDomain: `${MAIL_FROM_SUBDOMAIN}.${domain}`, }); return DKIM.DkimTokens ?? []; diff --git a/apps/web/src/components/DomainsSettings.tsx b/apps/web/src/components/DomainsSettings.tsx index a2bc5cb..765043a 100644 --- a/apps/web/src/components/DomainsSettings.tsx +++ b/apps/web/src/components/DomainsSettings.tsx @@ -335,6 +335,8 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
{domains.map(domain => { const status = getDomainStatus(domain); + const mailFromSubdomain = config?.aws?.mailFromSubdomain ?? 'plunk'; + const mailFromHost = `${mailFromSubdomain}.${domain.domain}`; return (
@@ -494,8 +496,8 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {

- Set up a custom MAIL FROM domain (plunk.{domain.domain}) to improve deliverability and - handle bounces/complaints. + Set up a custom MAIL FROM domain ({mailFromHost}) to improve deliverability and handle + bounces/complaints.

@@ -522,16 +524,16 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
- plunk.{domain.domain} + {mailFromHost}
@@ -571,16 +573,16 @@ export function DomainsSettings({projectId}: DomainsSettingsProps) {
- plunk.{domain.domain} + {mailFromHost}
diff --git a/apps/web/src/lib/hooks/useConfig.ts b/apps/web/src/lib/hooks/useConfig.ts index c4198df..85f870d 100644 --- a/apps/web/src/lib/hooks/useConfig.ts +++ b/apps/web/src/lib/hooks/useConfig.ts @@ -21,6 +21,7 @@ export interface ConfigResponse { }; aws: { sesRegion: string; + mailFromSubdomain: string; }; } diff --git a/docker-compose.yml b/docker-compose.yml index 66f5da8..18c0524 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -154,6 +154,10 @@ services: SES_CONFIGURATION_SET: ${SES_CONFIGURATION_SET} SES_CONFIGURATION_SET_NO_TRACKING: ${SES_CONFIGURATION_SET_NO_TRACKING:-} + # Custom MAIL FROM subdomain (defaults to 'plunk'; override when + # plunk. is already used for something else, e.g. a CDN) + MAIL_FROM_SUBDOMAIN: ${MAIL_FROM_SUBDOMAIN:-} + # Optional: OAuth GITHUB_OAUTH_CLIENT: ${GITHUB_OAUTH_CLIENT:-} GITHUB_OAUTH_SECRET: ${GITHUB_OAUTH_SECRET:-}