domma-cms 0.116.1 → 0.116.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.116.1",
3
+ "version": "0.116.2",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -206,7 +206,7 @@ export function createPool(config) {
206
206
  } catch {
207
207
  // Already down - nothing to close.
208
208
  }
209
- return {ok: false, error: describeError(err)};
209
+ return {ok: false, error: describeError(err, connection)};
210
210
  }
211
211
  }
212
212
 
@@ -261,19 +261,52 @@ export function createPool(config) {
261
261
  }
262
262
 
263
263
  /**
264
- * Turn an IMAP/network error into something worth showing a user.
264
+ * Turn an IMAP/SMTP/network error into something worth showing a user.
265
265
  *
266
266
  * ImapFlow's own messages are decent but its auth failures surface as raw
267
- * server responses, which vary by server and read like a stack trace.
267
+ * server responses, which vary by server and read like a stack trace. TLS
268
+ * failures are worse: OpenSSL's text ("…:tls_validate_record_header:wrong
269
+ * version number:…") names nothing the user can change. nodemailer also wraps
270
+ * every socket error as `ESOCKET`, dropping the TLS code, so those are
271
+ * recognised by `reason`/message as well as by code.
268
272
  *
269
- * @param {Error & {code?: string, authenticationFailed?: boolean}} err
273
+ * @param {Error & {code?: string, reason?: string, authenticationFailed?: boolean}} err
274
+ * @param {{host?: string, port?: number, secure?: boolean}} [connection] - what was tried, so the message can name it
270
275
  * @returns {string}
271
276
  */
272
- export function describeError(err) {
277
+ export function describeError(err, connection = {}) {
273
278
  if (!err) return 'Unknown error.';
274
- if (err.authenticationFailed) return 'The mail server rejected that username and password.';
279
+ if (err.authenticationFailed || err.code === 'EAUTH') return 'The mail server rejected that username and password.';
275
280
 
276
- switch (err.code) {
281
+ const host = connection.host || 'The mail server';
282
+ const port = connection.port ? `Port ${connection.port}` : 'That port';
283
+ const text = `${err.reason || ''} ${err.message || ''}`;
284
+
285
+ // Implicit TLS against a port that starts in plain text (587, 143, 25):
286
+ // the "server hello" is a greeting line, which OpenSSL reads as a bad version.
287
+ if (err.code === 'ERR_SSL_WRONG_VERSION_NUMBER' || /wrong version number/i.test(text)) {
288
+ return connection.secure
289
+ ? `${port} does not start with TLS, so Implicit TLS must be off for it - ports 587 and 143 switch to TLS with STARTTLS. Untick Implicit TLS, or use the implicit-TLS port (465 to send, 993 to read).`
290
+ : `${port} answered in a different TLS mode from the one chosen. Check the port and the Implicit TLS setting.`;
291
+ }
292
+ if (['DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN'].includes(err.code) || /self[- ]signed certificate/i.test(text)) {
293
+ return `${host === 'The mail server' ? 'The mail server’s' : `${host}’s`} TLS certificate is self-signed, so the server cannot prove who it is. `
294
+ + `Install a trusted certificate (Let’s Encrypt, for example) that covers this hostname on the mail server. `
295
+ + `For a server you control, the plugin’s allowInsecureTLS setting accepts it instead.`;
296
+ }
297
+ if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID' || /altnames|does not match certificate/i.test(text)) {
298
+ return `The mail server’s TLS certificate does not cover ${connection.host || 'that hostname'}. Use the hostname the certificate was issued for, or reissue it to include this one.`;
299
+ }
300
+ if (err.code === 'CERT_HAS_EXPIRED' || /certificate has expired/i.test(text)) {
301
+ return 'The mail server’s TLS certificate has expired. It needs renewing on the server.';
302
+ }
303
+ if (['UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'].includes(err.code) || /unable to (verify the first certificate|get local issuer)/i.test(text)) {
304
+ return 'The mail server’s TLS certificate could not be verified - it is missing its intermediate certificate, or was issued by an authority this server does not trust.';
305
+ }
306
+
307
+ // nodemailer reports these as ESOCKET/EDNS; the system code survives only in the message.
308
+ const netCode = text.match(/\b(ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ETIMEDOUT|ECONNRESET)\b/)?.[1] ?? err.code;
309
+ switch (netCode) {
277
310
  case 'ENOTFOUND':
278
311
  case 'EAI_AGAIN':
279
312
  return 'That mail server hostname could not be resolved.';
@@ -282,11 +315,6 @@ export function describeError(err) {
282
315
  case 'ETIMEDOUT':
283
316
  case 'ECONNRESET':
284
317
  return 'The mail server did not respond in time.';
285
- case 'CERT_HAS_EXPIRED':
286
- case 'DEPTH_ZERO_SELF_SIGNED_CERT':
287
- case 'SELF_SIGNED_CERT_IN_CHAIN':
288
- case 'ERR_TLS_CERT_ALTNAME_INVALID':
289
- return 'The mail server’s TLS certificate could not be verified.';
290
318
  default:
291
319
  return err.message || 'The mail server connection failed.';
292
320
  }
@@ -13,6 +13,7 @@ import {createRequire} from 'node:module';
13
13
  import nodemailer from 'nodemailer';
14
14
 
15
15
  import {bareAddress, formatFrom} from '../admin/mail/identity.js';
16
+ import {describeError} from './imapPool.js';
16
17
  import {normalisePriority, priorityHeaders} from './priority.js';
17
18
 
18
19
  // MailComposer is not on nodemailer's public export, but it is the piece that
@@ -389,7 +390,7 @@ export async function verifySmtp(transportConfig, allowInsecureTLS = false) {
389
390
  await transport.verify();
390
391
  return {ok: true};
391
392
  } catch (err) {
392
- return {ok: false, error: err.message};
393
+ return {ok: false, error: describeError(err, transportConfig)};
393
394
  } finally {
394
395
  transport.close();
395
396
  }