mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(api): add custom headers to email fallback SMTP (#3507)
This commit is contained in:
1 parent
0b1ddc2695
commit
557efe1c7d
13 files changed
+132
-4
No files matched your search
@@ -330,6 +330,41 @@ function validateReplyToEmail(value: string): void {
|
||||
const EMAIL_DOMAIN_PATTERN =
|
||||
/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
|
||||
const DEFAULT_SMTP_PORT = 587;
|
||||
const SMTP_HEADER_NAME_PATTERN = /^[\x21-\x39\x3b-\x7e]+$/u;
|
||||
const SMTP_HEADER_VALUE_PATTERN = /^[\x20-\x7e\t]*$/u;
|
||||
const SMTP_HEADER_MAX_LINE_LENGTH = 998;
|
||||
const CORE_SMTP_HEADER_NAMES = new Set([
|
||||
'from',
|
||||
'to',
|
||||
'cc',
|
||||
'bcc',
|
||||
'subject',
|
||||
'reply-to',
|
||||
'message-id',
|
||||
'date',
|
||||
'content-type',
|
||||
'mime-version',
|
||||
]);
|
||||
|
||||
function validateSmtpHeaders(headers: unknown, envName: string): void {
|
||||
if (headers === undefined) return;
|
||||
if (!isConfigObject(headers)) throw new Error(`${envName} must be a JSON object of header names to strings`);
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
if (!SMTP_HEADER_NAME_PATTERN.test(name)) {
|
||||
throw new Error(`${envName} header name ${JSON.stringify(name)} is not a valid header field name`);
|
||||
}
|
||||
if (CORE_SMTP_HEADER_NAMES.has(name.toLowerCase())) {
|
||||
throw new Error(`${envName} must not set the ${name} header`);
|
||||
}
|
||||
if (typeof value !== 'string') throw new Error(`${envName} value of ${name} must be a string`);
|
||||
if (!SMTP_HEADER_VALUE_PATTERN.test(value)) {
|
||||
throw new Error(`${envName} value of ${name} must be printable ASCII without line breaks`);
|
||||
}
|
||||
if (name.length + 2 + value.length > SMTP_HEADER_MAX_LINE_LENGTH) {
|
||||
throw new Error(`${envName} header ${name} must fit on one ${SMTP_HEADER_MAX_LINE_LENGTH} character line`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeEmailFallbackConfig(config: MasterConfig): void {
|
||||
const email = config.integrations.email;
|
||||
@@ -342,6 +377,7 @@ function normalizeEmailFallbackConfig(config: MasterConfig): void {
|
||||
if (!smtp.password?.trim()) throw missing('FLUXER_EMAIL_FALLBACK_SMTP_PASSWORD');
|
||||
const port: unknown = smtp.port ?? DEFAULT_SMTP_PORT;
|
||||
assertIntegerInRange(port, 'FLUXER_EMAIL_FALLBACK_SMTP_PORT', 1, 65_535);
|
||||
validateSmtpHeaders(smtp.headers, 'FLUXER_EMAIL_FALLBACK_SMTP_HEADERS');
|
||||
const normalizedDomains = domains.map((domain) => domain.trim().toLowerCase());
|
||||
for (const domain of normalizedDomains) {
|
||||
if (!EMAIL_DOMAIN_PATTERN.test(domain)) {
|
||||
|
||||
@@ -197,7 +197,7 @@ export interface MasterConfig {
|
||||
smtp?: SmtpConfig;
|
||||
fallback?: {
|
||||
domains: Array<string>;
|
||||
smtp: SmtpConfig;
|
||||
smtp: SmtpConfig & {headers?: Record<string, string>};
|
||||
};
|
||||
};
|
||||
voice: {
|
||||
|
||||
@@ -540,6 +540,30 @@ describe('ConfigLoader', () => {
|
||||
},
|
||||
);
|
||||
|
||||
test('loads the email fallback headers', async () => {
|
||||
stubMinimalEnv({...emailFallbackEnv, FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: '{"X-Tracking":"{\\"enable\\":0}"}'});
|
||||
const config = await loadConfig();
|
||||
expect(config.integrations.email.fallback?.smtp.headers).toEqual({'X-Tracking': '{"enable":0}'});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{headers: '["X-Tracking"]', error: 'must be a JSON object'},
|
||||
{headers: '{"X-Tracking":{"enable":0}}', error: 'value of X-Tracking must be a string'},
|
||||
{headers: '{"X-Tracking":0}', error: 'value of X-Tracking must be a string'},
|
||||
{headers: '{"X Tracking":"off"}', error: 'header name "X Tracking" is not a valid header field name'},
|
||||
{headers: '{"X-Tracking:":"off"}', error: 'header name "X-Tracking:" is not a valid header field name'},
|
||||
{headers: '{"X-Tracking\\r\\nBcc":"off"}', error: 'is not a valid header field name'},
|
||||
{headers: '{"X-Tracking":"off\\r\\nBcc: [email protected]"}', error: 'must be printable ASCII without line breaks'},
|
||||
{headers: '{"X-Tracking":"off\\n"}', error: 'must be printable ASCII without line breaks'},
|
||||
{headers: `{"X-Tracking":"${'a'.repeat(990)}"}`, error: 'must fit on one 998 character line'},
|
||||
...['From', 'to', 'Cc', 'BCC', 'Subject', 'Reply-To', 'Message-ID', 'Date', 'content-type', 'MIME-Version'].map(
|
||||
(name) => ({headers: JSON.stringify({[name]: 'x'}), error: `must not set the ${name} header`}),
|
||||
),
|
||||
])('rejects the email fallback headers $headers', async ({headers, error}) => {
|
||||
stubMinimalEnv({...emailFallbackEnv, FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: headers});
|
||||
await expect(loadConfig()).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
test('rejects an out of range email fallback port', async () => {
|
||||
stubMinimalEnv({...emailFallbackEnv, FLUXER_EMAIL_FALLBACK_SMTP_PORT: '70000'});
|
||||
await expect(loadConfig()).rejects.toThrow(
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('buildNamedFluxerEnvOverrides', () => {
|
||||
FLUXER_EMAIL_REPLY_TO_EMAIL: blank,
|
||||
FLUXER_EMAIL_FALLBACK_DOMAINS: blank,
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_HOST: blank,
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: blank,
|
||||
}),
|
||||
).toEqual({});
|
||||
});
|
||||
@@ -61,6 +62,7 @@ describe('buildNamedFluxerEnvOverrides', () => {
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_USERNAME: 'apikey',
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_PASSWORD: 'secret',
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_TLS_MODE: 'STARTTLS',
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: '{"X-Tracking":"off"}',
|
||||
}),
|
||||
).toEqual({
|
||||
integrations: {
|
||||
@@ -73,6 +75,7 @@ describe('buildNamedFluxerEnvOverrides', () => {
|
||||
username: 'apikey',
|
||||
password: 'secret',
|
||||
tls_mode: 'starttls',
|
||||
headers: {'X-Tracking': 'off'},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -174,6 +174,10 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
|
||||
path: ['integrations', 'email', 'fallback', 'smtp', 'tls_mode'],
|
||||
parse: parseSmtpTlsMode,
|
||||
},
|
||||
FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: {
|
||||
path: ['integrations', 'email', 'fallback', 'smtp', 'headers'],
|
||||
parse: parseJsonObject,
|
||||
},
|
||||
FLUXER_LIVEKIT_ENABLED: {path: ['integrations', 'voice', 'enabled'], parse: parseBoolean},
|
||||
FLUXER_LIVEKIT_API_KEY: {path: ['integrations', 'voice', 'api_key']},
|
||||
FLUXER_LIVEKIT_API_SECRET: {path: ['integrations', 'voice', 'api_secret']},
|
||||
|
||||
Reference in new issue
Block a user