feat: make MAIL FROM subdomain configurable via MAIL_FROM_SUBDOMAIN env var
This commit is contained in:
@@ -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
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,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 ?? [];
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface ConfigResponse {
|
|||||||
};
|
};
|
||||||
aws: {
|
aws: {
|
||||||
sesRegion: string;
|
sesRegion: string;
|
||||||
|
mailFromSubdomain: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:-}
|
||||||
|
|||||||
Reference in New Issue
Block a user