feat(api): add domain-routed fallback email provider (#3506)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-17 01:45:19 +02:00
1 parent c195e2b5de
commit 0b1ddc2695
15 files changed
+346 -30

No files matched your search

+16 -7
View File
@@ -39,6 +39,18 @@ function resolveEmailAppBaseUrl(master: MasterConfig): string {
}
}
function toSmtpConfig(
smtp: NonNullable<MasterConfig['integrations']['email']['smtp']>,
): NonNullable<APIConfig['email']['smtp']> {
return {
host: smtp.host,
port: smtp.port,
username: smtp.username,
password: smtp.password,
tlsMode: smtp.tls_mode ?? smtpTlsModeFromSecure(smtp.secure),
};
}
function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean';
}
@@ -291,14 +303,11 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
fromName: master.integrations.email.from_name,
replyToEmail: master.integrations.email.reply_to_email,
appBaseUrl: resolveEmailAppBaseUrl(master),
smtp: master.integrations.email.smtp
smtp: master.integrations.email.smtp ? toSmtpConfig(master.integrations.email.smtp) : undefined,
fallback: master.integrations.email.fallback
? {
host: master.integrations.email.smtp.host,
port: master.integrations.email.smtp.port,
username: master.integrations.email.smtp.username,
password: master.integrations.email.smtp.password,
tlsMode:
master.integrations.email.smtp.tls_mode ?? smtpTlsModeFromSecure(master.integrations.email.smtp.secure),
domains: master.integrations.email.fallback.domains,
smtp: toSmtpConfig(master.integrations.email.fallback.smtp),
}
: undefined,
},
+12 -6
View File
@@ -19,6 +19,14 @@ export interface TrustedCallerConfig {
buckets: Array<string>;
}
interface SmtpConfig {
host: string;
port: number;
username: string;
password: string;
tlsMode: SmtpTlsMode | null;
}
interface APICachePurgeTarget {
endpoint: string;
token: string;
@@ -162,12 +170,10 @@ export interface APIConfig {
fromName: string;
replyToEmail: string;
appBaseUrl: string;
smtp?: {
host: string;
port: number;
username: string;
password: string;
tlsMode: SmtpTlsMode | null;
smtp?: SmtpConfig;
fallback?: {
domains: Array<string>;
smtp: SmtpConfig;
};
};
blocklistFeeds: {
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {APIConfig} from '@app/api/config/APIConfig';
import {createEmailProvider} from '@app/api/email/EmailProviderFactory';
import {DomainRoutedEmailProvider} from '@pkgs/email/src/DomainRoutedEmailProvider';
import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider';
import {describe, expect, it} from 'vitest';
const smtp = {host: 'smtp.example.com', port: 587, username: 'user', password: 'pass', tlsMode: null};
function emailConfig(overrides: Partial<APIConfig['email']> = {}): APIConfig['email'] {
return {
enabled: true,
provider: 'smtp',
fromEmail: '[email protected]',
fromName: 'Example',
replyToEmail: '',
appBaseUrl: 'https://example.com',
smtp,
...overrides,
};
}
describe('createEmailProvider', () => {
it('returns the plain SMTP provider without a fallback', () => {
expect(createEmailProvider(emailConfig())).toBeInstanceOf(SmtpEmailProvider);
});
it('routes by domain when a fallback is set', () => {
const provider = createEmailProvider(
emailConfig({fallback: {domains: ['gmail.com'], smtp: {...smtp, host: 'smtp.sendgrid.net'}}}),
);
expect(provider).toBeInstanceOf(DomainRoutedEmailProvider);
});
it.each([{enabled: false}, {provider: 'none' as const}, {smtp: undefined}])(
'returns no provider for %j even with a fallback',
(overrides) => {
const fallback = {domains: ['gmail.com'], smtp};
expect(createEmailProvider(emailConfig({fallback, ...overrides}))).toBeNull();
},
);
});
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {APIConfig} from '@app/api/config/APIConfig';
import {DomainRoutedEmailProvider} from '@pkgs/email/src/DomainRoutedEmailProvider';
import type {IEmailProvider} from '@pkgs/email/src/EmailProviderTypes';
import {SmtpEmailProvider} from '@pkgs/email/src/SmtpEmailProvider';
@@ -9,16 +10,14 @@ export function createEmailProvider(emailConfig: APIConfig['email']): IEmailProv
return null;
}
switch (emailConfig.provider) {
case 'smtp':
return emailConfig.smtp
? new SmtpEmailProvider({
host: emailConfig.smtp.host,
port: emailConfig.smtp.port,
username: emailConfig.smtp.username,
password: emailConfig.smtp.password,
tlsMode: emailConfig.smtp.tlsMode,
})
: null;
case 'smtp': {
if (!emailConfig.smtp) return null;
const primary = new SmtpEmailProvider(emailConfig.smtp);
const {fallback} = emailConfig;
return fallback
? new DomainRoutedEmailProvider(primary, new SmtpEmailProvider(fallback.smtp), fallback.domains)
: primary;
}
default:
return null;
}