feat(email): configurable reply-to address (#3140)

This commit is contained in:
Hampus
2026-10-02 23:26:01 +02:00
committed by GitHub
parent 98fa41dcf0
commit 811341bc2f
16 changed files with 138 additions and 1 deletions
+8
View File
@@ -163,6 +163,7 @@ function defaultConfig(): MasterConfig {
provider: 'none',
from_email: '',
from_name: 'Fluxer',
reply_to_email: '',
app_base_url: '',
},
voice: {
@@ -292,6 +293,12 @@ function requireString(value: string | undefined, envName: string): void {
}
}
function validateReplyToEmail(value: string): void {
if (value !== '' && !/^[^\s@<>,;"]+@[^\s@<>,;"]+$/.test(value)) {
throw new Error('FLUXER_EMAIL_REPLY_TO_EMAIL must be a single email address such as [email protected]');
}
}
function validateUploadRelaySecret(value: string): void {
const trimmed = value.trim();
if (trimmed.length === 0) {
@@ -566,6 +573,7 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
validateStorageChangeFeedConfig(config);
validateCachePurgeConfig(config);
validateStoreBillingConfig(config);
validateReplyToEmail(config.integrations.email.reply_to_email);
normalizeAppOriginAliases(config);
assertIntegerInRange(config.services.api.max_inflight_requests, 'FLUXER_API_MAX_INFLIGHT_REQUESTS', 1, 100_000);
assertIntegerInRange(config.services.api.headers_timeout_ms, 'FLUXER_API_HEADERS_TIMEOUT_MS', 1_000, 3_600_000);
+1
View File
@@ -169,6 +169,7 @@ export interface MasterConfig {
provider: 'smtp' | 'none';
from_email: string;
from_name: string;
reply_to_email: string;
app_base_url: string;
webhook_secret?: string;
smtp?: {
@@ -290,6 +290,30 @@ describe('ConfigLoader', () => {
expect(config.services.api.storage_change_feed?.skip_buckets).toBeUndefined();
});
test('reads the email reply-to address', async () => {
stubMinimalEnv({FLUXER_EMAIL_REPLY_TO_EMAIL: '[email protected]'});
const config = await loadConfig();
expect(config.integrations.email.reply_to_email).toBe('[email protected]');
});
test('leaves the email reply-to address empty when unset or blank', async () => {
stubMinimalEnv({FLUXER_EMAIL_REPLY_TO_EMAIL: ' '});
const config = await loadConfig();
expect(config.integrations.email.reply_to_email).toBe('');
});
test.each(['support', 'Support <[email protected]>', '[email protected],[email protected]', ' [email protected]'])(
'rejects %j as the email reply-to address',
async (value) => {
stubMinimalEnv({FLUXER_EMAIL_REPLY_TO_EMAIL: value});
await expect(loadConfig()).rejects.toThrow('FLUXER_EMAIL_REPLY_TO_EMAIL must be a single email address');
},
);
test('keeps explicit passkey relying party values', async () => {
stubMinimalEnv({
FLUXER_BASE_DOMAIN: 'chat.example.com',
@@ -52,6 +52,7 @@ describe('buildNamedFluxerEnvOverrides', () => {
FLUXER_S3_FORCE_PATH_STYLE: 'true',
FLUXER_AUTH_BLUESKY_KEYS: '[{"kid":"key-1","private_key_path":"/etc/fluxer/keys/bluesky.pem"}]',
FLUXER_STRIPE_PRICE_MONTHLY_USD: 'price_monthly_usd',
FLUXER_EMAIL_REPLY_TO_EMAIL: '[email protected]',
});
expect(overrides).toMatchObject({
@@ -62,7 +63,10 @@ describe('buildNamedFluxerEnvOverrides', () => {
bluesky: {keys: [{kid: 'key-1', private_key_path: '/etc/fluxer/keys/bluesky.pem'}]},
},
s3: {force_path_style: true},
integrations: {stripe: {prices: {monthly_usd: 'price_monthly_usd'}}},
integrations: {
stripe: {prices: {monthly_usd: 'price_monthly_usd'}},
email: {reply_to_email: '[email protected]'},
},
});
});
@@ -77,6 +81,7 @@ describe('buildNamedFluxerEnvOverrides', () => {
FLUXER_LIVEKIT_DEFAULT_REGION: blank,
FLUXER_AUTH_BLUESKY_KEYS: blank,
FLUXER_EMAIL_FROM_NAME: blank,
FLUXER_EMAIL_REPLY_TO_EMAIL: blank,
}),
).toEqual({});
});
@@ -148,6 +148,7 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
FLUXER_EMAIL_PROVIDER: {path: ['integrations', 'email', 'provider']},
FLUXER_EMAIL_FROM_EMAIL: {path: ['integrations', 'email', 'from_email']},
FLUXER_EMAIL_FROM_NAME: {path: ['integrations', 'email', 'from_name']},
FLUXER_EMAIL_REPLY_TO_EMAIL: {path: ['integrations', 'email', 'reply_to_email']},
FLUXER_EMAIL_APP_BASE_URL: {path: ['integrations', 'email', 'app_base_url']},
FLUXER_EMAIL_WEBHOOK_SECRET: {path: ['integrations', 'email', 'webhook_secret']},
FLUXER_EMAIL_SMTP_HOST: {path: ['integrations', 'email', 'smtp', 'host']},