domma-cms 1.8.15 → 1.8.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Where this server may open a mail connection (SMTP or IMAP) to a host a site
3
+ * user typed - and the address it then connects to.
4
+ *
5
+ * A mail host is a TCP target the admin chooses, and a failed connection says
6
+ * what is listening there, so an unchecked host is a scanner of the server's
7
+ * own network. The address rules are services/outbound.js's (`resolvePublic`,
8
+ * `isBlockedAddress`): every address the name resolves to must be public,
9
+ * IPv4-mapped / NAT64 / decimal and octal spellings included. This module adds
10
+ * what is particular to mail:
11
+ *
12
+ * - Signed-only sites (codePolicy.signedOnly() - every site a Manager hosts)
13
+ * may name `localhost`, `127.0.0.1` or `::1` (hosted sites send through the
14
+ * box's own port 25) or a public mail server, on the mail ports only
15
+ * (SMTP 25/465/587/2525, IMAP 143/993). Nothing the site's admin sets lifts
16
+ * that - not `outboundAllowPrivate`, not a mail plugin's
17
+ * `allowPrivateMailHosts`, not the site's own SMTP host.
18
+ * - This server's own PUBLIC address is not refused here (outbound.js refuses
19
+ * it for web requests): a hosted mailbox at mail.<domain> on the same box is
20
+ * normal, and on a signed-only site the port list keeps it to mail.
21
+ * - The connection goes to the address that was checked (`address`), with the
22
+ * name kept for TLS (`servername`), so a second DNS answer cannot swap in an
23
+ * internal one (DNS rebinding).
24
+ *
25
+ * Self-hosted sites keep their freedom: core's site SMTP is not checked at all
26
+ * there (an internal relay is normal), and the mail plugins' own rules apply
27
+ * (plugins/_lib/mail/hostGuard.js).
28
+ *
29
+ * @module services/mailHost
30
+ */
31
+ import dns from 'node:dns';
32
+ import net from 'node:net';
33
+ import {FetchRefused, resolvePublic} from './outbound.js';
34
+ import {signedOnly} from './codePolicy.js';
35
+
36
+ export const SMTP_PORTS = Object.freeze([25, 465, 587, 2525]);
37
+ export const IMAP_PORTS = Object.freeze([143, 993]);
38
+
39
+ /** The loopback names a signed-only site may use, and the address each one is. */
40
+ const LOOPBACK = Object.freeze({localhost: '127.0.0.1', '127.0.0.1': '127.0.0.1', '::1': '::1'});
41
+
42
+ export const REFUSED_PRIVATE = 'That mail server is on a private network, so this site will not connect to it. '
43
+ + 'Use the mail server\'s public name.';
44
+
45
+ export class MailHostRefused extends Error {
46
+ constructor(message) { super(message); this.code = 'EMAILHOST_REFUSED'; this.mailHostRefused = true; }
47
+ }
48
+
49
+ /**
50
+ * Normalise a typed host for comparison: case, spaces, trailing dot, IPv6 brackets.
51
+ *
52
+ * @param {string} host
53
+ * @returns {string}
54
+ */
55
+ export function normaliseMailHost(host) {
56
+ return String(host ?? '').trim().toLowerCase().replace(/\.$/, '').replace(/^\[(.*)\]$/, '$1');
57
+ }
58
+
59
+ /**
60
+ * Check a mail host and port, and say which address to connect to.
61
+ *
62
+ * @param {string} host
63
+ * @param {number} port
64
+ * @param {object} [opts]
65
+ * @param {'smtp'|'imap'} [opts.kind='smtp']
66
+ * @param {boolean} [opts.signed] - codePolicy.signedOnly() when omitted
67
+ * @param {boolean} [opts.checkAddresses] - refuse private addresses (default: on when signed)
68
+ * @param {boolean} [opts.anyPort] - skip the port rule (a save route that has no port to hand; the connection checks it)
69
+ * @param {Function} [opts.lookup] - dns.promises.lookup, swappable in tests
70
+ * @returns {Promise<{address: string, family: number, servername: string|undefined}>}
71
+ * @throws {MailHostRefused} the host or port is not allowed
72
+ * @throws {Error} code ENOTFOUND - the name did not resolve (never connect by name instead)
73
+ */
74
+ export async function resolveMailHost(host, port, {kind = 'smtp', signed, checkAddresses, anyPort = false, lookup = dns.promises.lookup} = {}) {
75
+ const isSigned = signed ?? signedOnly();
76
+ const check = checkAddresses ?? isSigned;
77
+ const name = normaliseMailHost(host);
78
+ if (!name) throw new MailHostRefused('A mail server is required.');
79
+ if (isSigned && !anyPort) {
80
+ const ports = kind === 'imap' ? IMAP_PORTS : SMTP_PORTS;
81
+ const n = Number(port);
82
+ if (!ports.includes(n)) throw new MailHostRefused(`Port ${Number.isFinite(n) ? n : port} is not used for mail here - use ${ports.join(', ')}.`);
83
+ }
84
+ const servername = net.isIP(name) ? undefined : name;
85
+ if (isSigned && Object.hasOwn(LOOPBACK, name)) {
86
+ const address = LOOPBACK[name];
87
+ return {address, family: net.isIP(address), servername};
88
+ }
89
+ try {
90
+ // own: an empty set - see the module note on this server's public address.
91
+ const r = await resolvePublic(name, {allowPrivate: !check, lookup, own: new Set()});
92
+ return {address: r.address, family: r.family, servername};
93
+ } catch (err) {
94
+ if (!(err instanceof FetchRefused)) throw err;
95
+ if (/could not be found/.test(err.message)) {
96
+ throw Object.assign(new Error(`getaddrinfo ENOTFOUND ${name}`), {code: 'ENOTFOUND'});
97
+ }
98
+ throw new MailHostRefused(REFUSED_PRIVATE);
99
+ }
100
+ }
@@ -317,7 +317,8 @@ export async function registerCoreSources() {
317
317
  export function emailFailed(message) {
318
318
  return notify({source: 'core:email', severity: 'warning', dedupeKey: `email-failed:${day()}`, link: '#/settings',
319
319
  title: 'An email could not be sent',
320
- body: `The mail server refused or could not be reached: ${String(message || '').slice(0, 300)}\n\nPassword resets and form emails may not be arriving. Check Settings > Email.`});
320
+ // `message` is email.js's plain text, never the mail server's own words (those are in the server log).
321
+ body: `${String(message || 'The mail server refused the message or could not be reached.').slice(0, 300)}\n\nPassword resets and form emails may not be arriving. Check Settings > Email; the server log has the mail server's reply.`});
321
322
  }
322
323
 
323
324
  /** email.js: no SMTP host, so mail goes to a test inbox. */