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": {
|
||||
"SMTP.name": {
|
||||
"type": "string",
|
||||
"description": "Name of the email server (e.g. your domain name)\n@default \"AFFiNE Server\"\n@environment `MAILER_SERVERNAME`",
|
||||
"default": "AFFiNE Server"
|
||||
"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": ""
|
||||
},
|
||||
"SMTP.host": {
|
||||
"type": "string",
|
||||
@@ -237,8 +237,8 @@
|
||||
},
|
||||
"fallbackSMTP.name": {
|
||||
"type": "string",
|
||||
"description": "Name of the fallback email server (e.g. your domain name)\n@default \"AFFiNE Server\"",
|
||||
"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": ""
|
||||
},
|
||||
"fallbackSMTP.host": {
|
||||
"type": "string",
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -48,6 +48,7 @@ testem.log
|
||||
/typings
|
||||
tsconfig.tsbuildinfo
|
||||
.context
|
||||
/*.md
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
# MAILER_HOST=127.0.0.1
|
||||
# MAILER_PORT=1025
|
||||
# MAILER_SERVERNAME="mail.example.com"
|
||||
# MAILER_SENDER="noreply@toeverything.info"
|
||||
# MAILER_USER="noreply@toeverything.info"
|
||||
# MAILER_PASSWORD="affine"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test from 'ava';
|
||||
|
||||
import { normalizeSMTPHeloHostname } from '../core/mail/utils';
|
||||
import { Renderers } from '../mails';
|
||||
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);
|
||||
});
|
||||
|
||||
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', {
|
||||
'SMTP.name': {
|
||||
desc: 'Name of the email server (e.g. your domain name)',
|
||||
default: 'AFFiNE Server',
|
||||
desc: 'Hostname used for SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.',
|
||||
default: '',
|
||||
env: 'MAILER_SERVERNAME',
|
||||
},
|
||||
'SMTP.host': {
|
||||
@@ -72,8 +72,8 @@ defineModuleConfig('mailer', {
|
||||
shape: z.array(z.string()),
|
||||
},
|
||||
'fallbackSMTP.name': {
|
||||
desc: 'Name of the fallback email server (e.g. your domain name)',
|
||||
default: 'AFFiNE Server',
|
||||
desc: 'Hostname used for fallback SMTP HELO/EHLO (e.g. mail.example.com). Leave empty to use the system hostname.',
|
||||
default: '',
|
||||
},
|
||||
'fallbackSMTP.host': {
|
||||
desc: 'Host of the email server (e.g. smtp.gmail.com)',
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
|
||||
import { Config, metrics, OnEvent } from '../../base';
|
||||
import { resolveSMTPHeloHostname } from './utils';
|
||||
|
||||
export type SendOptions = Omit<SendMailOptions, 'to' | 'subject' | 'html'> & {
|
||||
to: string;
|
||||
@@ -19,8 +20,10 @@ export type SendOptions = Omit<SendMailOptions, 'to' | 'subject' | 'html'> & {
|
||||
function configToSMTPOptions(
|
||||
config: AppConfig['mailer']['SMTP']
|
||||
): SMTPTransport.Options {
|
||||
const name = resolveSMTPHeloHostname(config.name);
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
...(name ? { name } : {}),
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
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": {
|
||||
"SMTP.name": {
|
||||
"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"
|
||||
},
|
||||
"SMTP.host": {
|
||||
@@ -130,7 +130,7 @@
|
||||
},
|
||||
"fallbackSMTP.name": {
|
||||
"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": {
|
||||
"type": "String",
|
||||
|
||||
Reference in New Issue
Block a user