feat(api): allow routing all email through fallback (#3509)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-17 04:16:08 +02:00
1 parent d564e01518
commit 2d8d130be1
5 files changed
+27 -3

No files matched your search

@@ -30,6 +30,16 @@ describe('DomainRoutedEmailProvider', () => {
expect(result).toBe(route === 'primary'); expect(result).toBe(route === 'primary');
}); });
it('sends every address through the fallback provider when the domains are *', async () => {
const primary = {sendEmail: vi.fn().mockResolvedValue(true)};
const fallback = {sendEmail: vi.fn().mockResolvedValue(true)};
const provider = new DomainRoutedEmailProvider(primary, fallback, ['*']);
await provider.sendEmail(message('[email protected]'));
await provider.sendEmail(message('[email protected]'));
expect(fallback.sendEmail).toHaveBeenCalledTimes(2);
expect(primary.sendEmail).not.toHaveBeenCalled();
});
it('never reaches either provider for a bounced address', async () => { it('never reaches either provider for a bounced address', async () => {
const primary = {sendEmail: vi.fn().mockResolvedValue(true)}; const primary = {sendEmail: vi.fn().mockResolvedValue(true)};
const fallback = {sendEmail: vi.fn().mockResolvedValue(true)}; const fallback = {sendEmail: vi.fn().mockResolvedValue(true)};
@@ -21,7 +21,7 @@ export class DomainRoutedEmailProvider implements IEmailProvider {
.slice(message.to.lastIndexOf('@') + 1) .slice(message.to.lastIndexOf('@') + 1)
.trim() .trim()
.toLowerCase(); .toLowerCase();
const useFallback = this.fallbackDomains.has(domain); const useFallback = this.fallbackDomains.has('*') || this.fallbackDomains.has(domain);
logger.debug({domain, provider: useFallback ? 'fallback' : 'primary'}, 'Routing email'); logger.debug({domain, provider: useFallback ? 'fallback' : 'primary'}, 'Routing email');
return (useFallback ? this.fallback : this.primary).sendEmail(message); return (useFallback ? this.fallback : this.primary).sendEmail(message);
} }
@@ -843,7 +843,7 @@ A second SMTP provider can take the mail for chosen recipient domains while ever
#### `FLUXER_EMAIL_FALLBACK_DOMAINS` #### `FLUXER_EMAIL_FALLBACK_DOMAINS`
Default empty. The recipient domains that go through the fallback provider, comma separated, such as `gmail.com,googlemail.com`. Matching ignores case and takes the part of the address after the last `@`. Only an exact match counts, so `gmail.com` leaves `mail.gmail.com` on the main provider. An entry that is not a bare domain fails startup. `.env.example` lists it commented out. Default empty. The recipient domains that go through the fallback provider, comma separated, such as `gmail.com,googlemail.com`. Matching ignores case and takes the part of the address after the last `@`. Only an exact match counts, so `gmail.com` leaves `mail.gmail.com` on the main provider. Set it to `*` alone to send every email through the fallback provider. An entry that is not a bare domain or `*` fails startup. `.env.example` lists it commented out.
#### `FLUXER_EMAIL_FALLBACK_SMTP_HOST` #### `FLUXER_EMAIL_FALLBACK_SMTP_HOST`
+4 -1
View File
@@ -379,8 +379,11 @@ function normalizeEmailFallbackConfig(config: MasterConfig): void {
assertIntegerInRange(port, 'FLUXER_EMAIL_FALLBACK_SMTP_PORT', 1, 65_535); assertIntegerInRange(port, 'FLUXER_EMAIL_FALLBACK_SMTP_PORT', 1, 65_535);
validateSmtpHeaders(smtp.headers, 'FLUXER_EMAIL_FALLBACK_SMTP_HEADERS'); validateSmtpHeaders(smtp.headers, 'FLUXER_EMAIL_FALLBACK_SMTP_HEADERS');
const normalizedDomains = domains.map((domain) => domain.trim().toLowerCase()); const normalizedDomains = domains.map((domain) => domain.trim().toLowerCase());
if (normalizedDomains.includes('*') && new Set(normalizedDomains).size > 1) {
throw new Error('FLUXER_EMAIL_FALLBACK_DOMAINS must be either * or a list of domains');
}
for (const domain of normalizedDomains) { for (const domain of normalizedDomains) {
if (!EMAIL_DOMAIN_PATTERN.test(domain)) { if (domain !== '*' && !EMAIL_DOMAIN_PATTERN.test(domain)) {
throw new Error( throw new Error(
`FLUXER_EMAIL_FALLBACK_DOMAINS entry ${JSON.stringify(domain)} must be a domain such as gmail.com`, `FLUXER_EMAIL_FALLBACK_DOMAINS entry ${JSON.stringify(domain)} must be a domain such as gmail.com`,
); );
@@ -540,6 +540,17 @@ describe('ConfigLoader', () => {
}, },
); );
test('loads * as the email fallback domains', async () => {
stubMinimalEnv({...emailFallbackEnv, FLUXER_EMAIL_FALLBACK_DOMAINS: ' * '});
const config = await loadConfig();
expect(config.integrations.email.fallback?.domains).toEqual(['*']);
});
test('rejects * mixed with email fallback domains', async () => {
stubMinimalEnv({...emailFallbackEnv, FLUXER_EMAIL_FALLBACK_DOMAINS: '*,gmail.com'});
await expect(loadConfig()).rejects.toThrow('FLUXER_EMAIL_FALLBACK_DOMAINS must be either * or a list of domains');
});
test('loads the email fallback headers', async () => { test('loads the email fallback headers', async () => {
stubMinimalEnv({...emailFallbackEnv, FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: '{"X-Tracking":"{\\"enable\\":0}"}'}); stubMinimalEnv({...emailFallbackEnv, FLUXER_EMAIL_FALLBACK_SMTP_HEADERS: '{"X-Tracking":"{\\"enable\\":0}"}'});
const config = await loadConfig(); const config = await loadConfig();