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.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +6 -0
- package/admin/css/admin.css +1 -1
- package/admin/js/lib/block-tags.js +1 -0
- package/admin/js/lib/editor-hop-arrange.js +1 -0
- package/admin/js/lib/editor-hop.js +19 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/shortcode-form.js +3 -3
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/block-editor.html +2 -5
- package/admin/js/templates/page-editor.html +5 -7
- package/admin/js/views/block-editor.js +5 -5
- package/admin/js/views/page-editor.js +8 -8
- package/docs/markdown-shortcodes.md +17 -0
- package/package.json +1 -1
- package/plugins/_lib/mail/hostGuard.js +96 -12
- package/plugins/_lib/mail/imapPool.js +26 -4
- package/plugins/_lib/mail/poller.js +2 -1
- package/plugins/_lib/mail/readRoutes.js +4 -4
- package/plugins/_lib/mail/scheduler.js +7 -4
- package/plugins/_lib/mail/send.js +67 -18
- package/plugins/free-tier.lock.json +6 -5
- package/plugins/security/CLAUDE.md +9 -1
- package/plugins/security/plugin.json +2 -2
- package/plugins/security/server/facts.js +105 -29
- package/plugins/security/tests/facts.test.js +109 -0
- package/server/routes/api/blocks.js +57 -2
- package/server/routes/api/forms.js +4 -2
- package/server/routes/api/settings.js +22 -6
- package/server/services/email.js +98 -8
- package/server/services/mailHost.js +100 -0
- package/server/services/notification-sources.js +2 -1
|
@@ -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
|
-
|
|
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. */
|