feat(server): add fallback smtp config (#13377)
fix AF-2749 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for configuring a fallback SMTP server for outgoing emails. * Introduced the ability to specify email domains that will always use the fallback SMTP server. * Enhanced email sending to automatically route messages to the appropriate SMTP server based on recipient domain. * **Documentation** * Updated configuration options and descriptions in the admin interface to reflect new fallback SMTP settings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -219,6 +219,41 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
|
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
|
||||||
"default": false
|
"default": false
|
||||||
|
},
|
||||||
|
"fallbackDomains": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []",
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
"fallbackSMTP.host": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"fallbackSMTP.port": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Port of the email server (they commonly are 25, 465 or 587)\n@default 465",
|
||||||
|
"default": 465
|
||||||
|
},
|
||||||
|
"fallbackSMTP.username": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Username used to authenticate the email server\n@default \"\"",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"fallbackSMTP.password": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Password used to authenticate the email server\n@default \"\"",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"fallbackSMTP.sender": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Sender of all the emails (e.g. \"AFFiNE Team <noreply@affine.pro>\")\n@default \"\"",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"fallbackSMTP.ignoreTLS": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
import { defineModuleConfig } from '../../base';
|
import { defineModuleConfig } from '../../base';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -11,6 +13,16 @@ declare global {
|
|||||||
ignoreTLS: boolean;
|
ignoreTLS: boolean;
|
||||||
sender: string;
|
sender: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fallbackDomains: ConfigItem<string[]>;
|
||||||
|
fallbackSMTP: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
ignoreTLS: boolean;
|
||||||
|
sender: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,4 +58,34 @@ defineModuleConfig('mailer', {
|
|||||||
default: false,
|
default: false,
|
||||||
env: ['MAILER_IGNORE_TLS', 'boolean'],
|
env: ['MAILER_IGNORE_TLS', 'boolean'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fallbackDomains: {
|
||||||
|
desc: 'The emails from these domains are always sent using the fallback SMTP server.',
|
||||||
|
default: [],
|
||||||
|
shape: z.array(z.string()),
|
||||||
|
},
|
||||||
|
'fallbackSMTP.host': {
|
||||||
|
desc: 'Host of the email server (e.g. smtp.gmail.com)',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
'fallbackSMTP.port': {
|
||||||
|
desc: 'Port of the email server (they commonly are 25, 465 or 587)',
|
||||||
|
default: 465,
|
||||||
|
},
|
||||||
|
'fallbackSMTP.username': {
|
||||||
|
desc: 'Username used to authenticate the email server',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
'fallbackSMTP.password': {
|
||||||
|
desc: 'Password used to authenticate the email server',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
'fallbackSMTP.sender': {
|
||||||
|
desc: 'Sender of all the emails (e.g. "AFFiNE Team <noreply@affine.pro>")',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
'fallbackSMTP.ignoreTLS': {
|
||||||
|
desc: "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ function configToSMTPOptions(
|
|||||||
export class MailSender {
|
export class MailSender {
|
||||||
private readonly logger = new Logger(MailSender.name);
|
private readonly logger = new Logger(MailSender.name);
|
||||||
private smtp: Transporter<SMTPTransport.SentMessageInfo> | null = null;
|
private smtp: Transporter<SMTPTransport.SentMessageInfo> | null = null;
|
||||||
|
private fallbackSMTP: Transporter<SMTPTransport.SentMessageInfo> | null =
|
||||||
|
null;
|
||||||
private usingTestAccount = false;
|
private usingTestAccount = false;
|
||||||
constructor(private readonly config: Config) {}
|
constructor(private readonly config: Config) {}
|
||||||
|
|
||||||
@@ -61,11 +63,17 @@ export class MailSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setup() {
|
private setup() {
|
||||||
const { SMTP } = this.config.mailer;
|
const { SMTP, fallbackDomains, fallbackSMTP } = this.config.mailer;
|
||||||
const opts = configToSMTPOptions(SMTP);
|
const opts = configToSMTPOptions(SMTP);
|
||||||
|
|
||||||
if (SMTP.host) {
|
if (SMTP.host) {
|
||||||
this.smtp = createTransport(opts);
|
this.smtp = createTransport(opts);
|
||||||
|
if (fallbackDomains.length > 0 && fallbackSMTP?.host) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Fallback SMTP is configured for domains: ${fallbackDomains.join(', ')}`
|
||||||
|
);
|
||||||
|
this.fallbackSMTP = createTransport(configToSMTPOptions(fallbackSMTP));
|
||||||
|
}
|
||||||
} else if (env.dev) {
|
} else if (env.dev) {
|
||||||
createTestAccount((err, account) => {
|
createTestAccount((err, account) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
@@ -83,21 +91,34 @@ export class MailSender {
|
|||||||
} else {
|
} else {
|
||||||
this.logger.warn('Mailer SMTP transport is not configured.');
|
this.logger.warn('Mailer SMTP transport is not configured.');
|
||||||
this.smtp = null;
|
this.smtp = null;
|
||||||
|
this.fallbackSMTP = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSender(domain: string) {
|
||||||
|
const { SMTP, fallbackSMTP, fallbackDomains } = this.config.mailer;
|
||||||
|
if (this.fallbackSMTP && fallbackDomains.includes(domain)) {
|
||||||
|
return [this.fallbackSMTP, fallbackSMTP.sender] as const;
|
||||||
|
}
|
||||||
|
return [this.smtp, SMTP.sender] as const;
|
||||||
|
}
|
||||||
|
|
||||||
async send(name: string, options: SendOptions) {
|
async send(name: string, options: SendOptions) {
|
||||||
if (!this.smtp) {
|
const [, domain, ...rest] = options.to.split('@');
|
||||||
|
if (rest.length || !domain) {
|
||||||
|
this.logger.error(`Invalid email address: ${options.to}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [smtpClient, from] = this.getSender(domain);
|
||||||
|
if (!smtpClient) {
|
||||||
this.logger.warn(`Mailer SMTP transport is not configured to send mail.`);
|
this.logger.warn(`Mailer SMTP transport is not configured to send mail.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.mail.counter('send_total').add(1, { name });
|
metrics.mail.counter('send_total').add(1, { name });
|
||||||
try {
|
try {
|
||||||
const result = await this.smtp.sendMail({
|
const result = await smtpClient.sendMail({ from, ...options });
|
||||||
from: this.config.mailer.SMTP.sender,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.rejected.length > 0) {
|
if (result.rejected.length > 0) {
|
||||||
metrics.mail.counter('rejected_total').add(1, { name });
|
metrics.mail.counter('rejected_total').add(1, { name });
|
||||||
|
|||||||
@@ -114,6 +114,34 @@
|
|||||||
"type": "Boolean",
|
"type": "Boolean",
|
||||||
"desc": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.",
|
"desc": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.",
|
||||||
"env": "MAILER_IGNORE_TLS"
|
"env": "MAILER_IGNORE_TLS"
|
||||||
|
},
|
||||||
|
"fallbackDomains": {
|
||||||
|
"type": "Array",
|
||||||
|
"desc": "The emails from these domains are always sent using the fallback SMTP server."
|
||||||
|
},
|
||||||
|
"fallbackSMTP.host": {
|
||||||
|
"type": "String",
|
||||||
|
"desc": "Host of the email server (e.g. smtp.gmail.com)"
|
||||||
|
},
|
||||||
|
"fallbackSMTP.port": {
|
||||||
|
"type": "Number",
|
||||||
|
"desc": "Port of the email server (they commonly are 25, 465 or 587)"
|
||||||
|
},
|
||||||
|
"fallbackSMTP.username": {
|
||||||
|
"type": "String",
|
||||||
|
"desc": "Username used to authenticate the email server"
|
||||||
|
},
|
||||||
|
"fallbackSMTP.password": {
|
||||||
|
"type": "String",
|
||||||
|
"desc": "Password used to authenticate the email server"
|
||||||
|
},
|
||||||
|
"fallbackSMTP.sender": {
|
||||||
|
"type": "String",
|
||||||
|
"desc": "Sender of all the emails (e.g. \"AFFiNE Team <noreply@affine.pro>\")"
|
||||||
|
},
|
||||||
|
"fallbackSMTP.ignoreTLS": {
|
||||||
|
"type": "Boolean",
|
||||||
|
"desc": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"doc": {
|
"doc": {
|
||||||
|
|||||||
Reference in New Issue
Block a user