fix(api): add custom headers to email fallback SMTP (#3507)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-17 02:38:59 +02:00
1 parent 0b1ddc2695
commit 557efe1c7d
13 files changed
+132 -4

No files matched your search

@@ -25,4 +25,33 @@ describe('SmtpEmailProvider', () => {
new SmtpEmailProvider({host: 'smtp.example.com', port, username: 'user', password: 'pass', tlsMode});
expect(createTransport).toHaveBeenCalledWith(expect.objectContaining(expected));
});
const message = {
to: '[email protected]',
from: {email: '[email protected]', name: 'Example'},
subject: 'Hi',
text: 'Body',
};
it('sends the configured headers verbatim', async () => {
const provider = new SmtpEmailProvider({
host: 'smtp.example.com',
port: 587,
username: 'user',
password: 'pass',
headers: {'X-Tracking': '{"enable":0}'},
});
await provider.sendEmail(message);
const {sendMail} = createTransport.mock.results[0].value;
expect(sendMail).toHaveBeenCalledWith(
expect.objectContaining({headers: {'X-Tracking': {prepared: true, value: '{"enable":0}'}}}),
);
});
it('sends no extra headers when none are configured', async () => {
const provider = new SmtpEmailProvider({host: 'smtp.example.com', port: 587, username: 'user', password: 'pass'});
await provider.sendEmail(message);
const {sendMail} = createTransport.mock.results[0].value;
expect(sendMail.mock.calls[0][0]).not.toHaveProperty('headers');
});
});
@@ -13,6 +13,7 @@ interface SmtpEmailConfig {
username: string;
password: string;
tlsMode?: SmtpTlsMode | null;
headers?: Record<string, string>;
connectionTimeoutMs?: number;
greetingTimeoutMs?: number;
socketTimeoutMs?: number;
@@ -23,11 +24,17 @@ export class SmtpEmailProvider implements IEmailProvider {
private readonly host: string;
private readonly port: number;
private readonly tlsMode: SmtpTlsMode;
private readonly headers: Record<string, {prepared: true; value: string}> | undefined;
constructor(config: SmtpEmailConfig) {
this.host = config.host;
this.port = config.port;
this.tlsMode = resolveSmtpTlsMode(config.tlsMode, config.port);
this.headers = config.headers
? Object.fromEntries(
Object.entries(config.headers).map(([name, value]) => [name, {prepared: true as const, value}]),
)
: undefined;
this.transporter = nodemailer.createTransport({
host: config.host,
port: config.port,
@@ -55,6 +62,7 @@ export class SmtpEmailProvider implements IEmailProvider {
to: message.to,
from: `${message.from.name} <${message.from.email}>`,
...(message.replyTo ? {replyTo: message.replyTo} : {}),
...(this.headers ? {headers: this.headers} : {}),
subject: message.subject,
text: message.text,
});
+4 -1
View File
@@ -307,7 +307,10 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
fallback: master.integrations.email.fallback
? {
domains: master.integrations.email.fallback.domains,
smtp: toSmtpConfig(master.integrations.email.fallback.smtp),
smtp: {
...toSmtpConfig(master.integrations.email.fallback.smtp),
headers: master.integrations.email.fallback.smtp.headers,
},
}
: undefined,
},
+1 -1
View File
@@ -173,7 +173,7 @@ export interface APIConfig {
smtp?: SmtpConfig;
fallback?: {
domains: Array<string>;
smtp: SmtpConfig;
smtp: SmtpConfig & {headers?: Record<string, string>};
};
};
blocklistFeeds: {
@@ -4,7 +4,8 @@ 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';
import nodemailer, {type Transporter} from 'nodemailer';
import {describe, expect, it, vi} from 'vitest';
const smtp = {host: 'smtp.example.com', port: 587, username: 'user', password: 'pass', tlsMode: null};
@@ -33,6 +34,18 @@ describe('createEmailProvider', () => {
expect(provider).toBeInstanceOf(DomainRoutedEmailProvider);
});
it('adds the fallback headers only to mail through the fallback', async () => {
const sendMail = vi.fn();
vi.spyOn(nodemailer, 'createTransport').mockReturnValue({sendMail} as unknown as Transporter);
const headers = {'X-Tracking': '{"enable":0}'};
const provider = createEmailProvider(emailConfig({fallback: {domains: ['gmail.com'], smtp: {...smtp, headers}}}));
const from = {email: '[email protected]', name: 'Example'};
await provider?.sendEmail({to: '[email protected]', from, subject: 'Hi', text: 'Body'});
await provider?.sendEmail({to: '[email protected]', from, subject: 'Hi', text: 'Body'});
expect(sendMail.mock.calls[0][0].headers).toEqual({'X-Tracking': {prepared: true, value: '{"enable":0}'}});
expect(sendMail.mock.calls[1][0]).not.toHaveProperty('headers');
});
it.each([{enabled: false}, {provider: 'none' as const}, {smtp: undefined}])(
'returns no provider for %j even with a fallback',
(overrides) => {