mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-22 23:30:36 +08:00
fix(server): normalize mail server name (#14564)
fix #14562 fix #14226 fix #14192 #### PR Dependency Tree * **PR #14564** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * SMTP and fallback SMTP name now default to empty and will use the system hostname when not set. * HELO hostname resolution includes stricter normalization/validation for more reliable mail handshakes. * **Documentation** * Updated admin and config descriptions to explain hostname/HELO behavior and fallback. * **Tests** * Added tests covering hostname normalization and rejection of invalid HELO values. * **Chores** * Updated example env and ignore rules. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -197,8 +197,8 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"SMTP.name": {
|
"SMTP.name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
|
"description": "Hostname used for SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"\n@environment `MAILER_SERVERNAME`",
|
||||||
"default": "AFFiNE Server"
|
"default": ""
|
||||||
},
|
},
|
||||||
"SMTP.host": {
|
"SMTP.host": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -237,8 +237,8 @@
|
|||||||
},
|
},
|
||||||
"fallbackSMTP.name": {
|
"fallbackSMTP.name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
|
"description": "Hostname used for fallback SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.\n@default \"\"",
|
||||||
"default": "AFFiNE Server"
|
"default": ""
|
||||||
},
|
},
|
||||||
"fallbackSMTP.host": {
|
"fallbackSMTP.host": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -48,6 +48,7 @@ testem.log
|
|||||||
/typings
|
/typings
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
.context
|
.context
|
||||||
|
/*.md
|
||||||
|
|
||||||
# System Files
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
# MAILER_HOST=127.0.0.1
|
# MAILER_HOST=127.0.0.1
|
||||||
# MAILER_PORT=1025
|
# MAILER_PORT=1025
|
||||||
|
# MAILER_SERVERNAME="mail.example.com"
|
||||||
# MAILER_SENDER="noreply@toeverything.info"
|
# MAILER_SENDER="noreply@toeverything.info"
|
||||||
# MAILER_USER="noreply@toeverything.info"
|
# MAILER_USER="noreply@toeverything.info"
|
||||||
# MAILER_PASSWORD="affine"
|
# MAILER_PASSWORD="affine"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
|
|
||||||
|
import { normalizeSMTPHeloHostname } from '../core/mail/utils';
|
||||||
import { Renderers } from '../mails';
|
import { Renderers } from '../mails';
|
||||||
import { TEST_DOC, TEST_USER } from '../mails/common';
|
import { TEST_DOC, TEST_USER } from '../mails/common';
|
||||||
|
|
||||||
@@ -21,3 +22,22 @@ test('should render mention email with empty doc title', async t => {
|
|||||||
});
|
});
|
||||||
t.snapshot(content.html, content.subject);
|
t.snapshot(content.html, content.subject);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should normalize valid SMTP HELO hostnames', t => {
|
||||||
|
t.is(normalizeSMTPHeloHostname('mail.example.com'), 'mail.example.com');
|
||||||
|
t.is(normalizeSMTPHeloHostname(' localhost '), 'localhost');
|
||||||
|
t.is(normalizeSMTPHeloHostname('[127.0.0.1]'), '[127.0.0.1]');
|
||||||
|
t.is(normalizeSMTPHeloHostname('[IPv6:2001:db8::1]'), '[IPv6:2001:db8::1]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject invalid SMTP HELO hostnames', t => {
|
||||||
|
t.is(normalizeSMTPHeloHostname(''), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname(' '), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname('AFFiNE Server'), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname('-example.com'), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname('example-.com'), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname('example..com'), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname('[bad host]'), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname('[foo]'), undefined);
|
||||||
|
t.is(normalizeSMTPHeloHostname('[IPv6:foo]'), undefined);
|
||||||
|
});
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ declare global {
|
|||||||
|
|
||||||
defineModuleConfig('mailer', {
|
defineModuleConfig('mailer', {
|
||||||
'SMTP.name': {
|
'SMTP.name': {
|
||||||
desc: 'Name of the email server (e.g. your domain name)',
|
desc: 'Hostname used for SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.',
|
||||||
default: 'AFFiNE Server',
|
default: '',
|
||||||
env: 'MAILER_SERVERNAME',
|
env: 'MAILER_SERVERNAME',
|
||||||
},
|
},
|
||||||
'SMTP.host': {
|
'SMTP.host': {
|
||||||
@@ -72,8 +72,8 @@ defineModuleConfig('mailer', {
|
|||||||
shape: z.array(z.string()),
|
shape: z.array(z.string()),
|
||||||
},
|
},
|
||||||
'fallbackSMTP.name': {
|
'fallbackSMTP.name': {
|
||||||
desc: 'Name of the fallback email server (e.g. your domain name)',
|
desc: 'Hostname used for fallback SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.',
|
||||||
default: 'AFFiNE Server',
|
default: '',
|
||||||
},
|
},
|
||||||
'fallbackSMTP.host': {
|
'fallbackSMTP.host': {
|
||||||
desc: 'Host of the email server (e.g. smtp.gmail.com)',
|
desc: 'Host of the email server (e.g. smtp.gmail.com)',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||||
|
|
||||||
import { Config, metrics, OnEvent } from '../../base';
|
import { Config, metrics, OnEvent } from '../../base';
|
||||||
|
import { resolveSMTPHeloHostname } from './utils';
|
||||||
|
|
||||||
export type SendOptions = Omit<SendMailOptions, 'to' | 'subject' | 'html'> & {
|
export type SendOptions = Omit<SendMailOptions, 'to' | 'subject' | 'html'> & {
|
||||||
to: string;
|
to: string;
|
||||||
@@ -19,8 +20,10 @@ export type SendOptions = Omit<SendMailOptions, 'to' | 'subject' | 'html'> & {
|
|||||||
function configToSMTPOptions(
|
function configToSMTPOptions(
|
||||||
config: AppConfig['mailer']['SMTP']
|
config: AppConfig['mailer']['SMTP']
|
||||||
): SMTPTransport.Options {
|
): SMTPTransport.Options {
|
||||||
|
const name = resolveSMTPHeloHostname(config.name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: config.name,
|
...(name ? { name } : {}),
|
||||||
host: config.host,
|
host: config.host,
|
||||||
port: config.port,
|
port: config.port,
|
||||||
tls: {
|
tls: {
|
||||||
|
|||||||
53
packages/backend/server/src/core/mail/utils.ts
Normal file
53
packages/backend/server/src/core/mail/utils.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { isIP } from 'node:net';
|
||||||
|
import { hostname as getHostname } from 'node:os';
|
||||||
|
|
||||||
|
const hostnameLabelRegexp = /^[A-Za-z0-9-]+$/;
|
||||||
|
|
||||||
|
function isValidSMTPAddressLiteral(hostname: string) {
|
||||||
|
if (!hostname.startsWith('[') || !hostname.endsWith(']')) return false;
|
||||||
|
|
||||||
|
const literal = hostname.slice(1, -1);
|
||||||
|
if (!literal || literal.includes(' ')) return false;
|
||||||
|
if (isIP(literal) === 4) return true;
|
||||||
|
|
||||||
|
if (literal.startsWith('IPv6:')) {
|
||||||
|
return isIP(literal.slice('IPv6:'.length)) === 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSMTPHeloHostname(hostname: string) {
|
||||||
|
const normalized = hostname.trim().replace(/\.$/, '');
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
if (isValidSMTPAddressLiteral(normalized)) return normalized;
|
||||||
|
if (normalized.length > 253) return undefined;
|
||||||
|
|
||||||
|
const labels = normalized.split('.');
|
||||||
|
for (const label of labels) {
|
||||||
|
if (!label || label.length > 63) return undefined;
|
||||||
|
if (
|
||||||
|
!hostnameLabelRegexp.test(label) ||
|
||||||
|
label.startsWith('-') ||
|
||||||
|
label.endsWith('-')
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSystemHostname() {
|
||||||
|
try {
|
||||||
|
return getHostname();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSMTPHeloHostname(configuredName: string) {
|
||||||
|
const normalizedConfiguredName = normalizeSMTPHeloHostname(configuredName);
|
||||||
|
if (normalizedConfiguredName) return normalizedConfiguredName;
|
||||||
|
return normalizeSMTPHeloHostname(readSystemHostname());
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"mailer": {
|
"mailer": {
|
||||||
"SMTP.name": {
|
"SMTP.name": {
|
||||||
"type": "String",
|
"type": "String",
|
||||||
"desc": "Name of the email server (e.g. your domain name)",
|
"desc": "Hostname used for SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.",
|
||||||
"env": "MAILER_SERVERNAME"
|
"env": "MAILER_SERVERNAME"
|
||||||
},
|
},
|
||||||
"SMTP.host": {
|
"SMTP.host": {
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
},
|
},
|
||||||
"fallbackSMTP.name": {
|
"fallbackSMTP.name": {
|
||||||
"type": "String",
|
"type": "String",
|
||||||
"desc": "Name of the fallback email server (e.g. your domain name)"
|
"desc": "Hostname used for fallback SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname."
|
||||||
},
|
},
|
||||||
"fallbackSMTP.host": {
|
"fallbackSMTP.host": {
|
||||||
"type": "String",
|
"type": "String",
|
||||||
|
|||||||
Reference in New Issue
Block a user