nodemailer 10.1.7 → 10.2.1

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cjs/addressparser/index.js +2 -5
  3. package/dist/cjs/dkim/index.js +1 -3
  4. package/dist/cjs/errors.d.ts +19 -0
  5. package/dist/cjs/errors.js +14 -1
  6. package/dist/cjs/fetch/cookies.js +2 -4
  7. package/dist/cjs/fetch/index.js +2 -8
  8. package/dist/cjs/mail-composer/index.js +42 -9
  9. package/dist/cjs/mailer/mail-message.js +5 -6
  10. package/dist/cjs/mime-funcs/index.d.ts +18 -1
  11. package/dist/cjs/mime-funcs/index.js +43 -1
  12. package/dist/cjs/mime-funcs/mime-types.js +3 -12
  13. package/dist/cjs/mime-node/index.d.ts +34 -1
  14. package/dist/cjs/mime-node/index.js +40 -15
  15. package/dist/cjs/package-info.d.ts +1 -1
  16. package/dist/cjs/package-info.js +1 -1
  17. package/dist/cjs/qp/index.js +3 -12
  18. package/dist/cjs/sendmail-transport/index.js +1 -3
  19. package/dist/cjs/shared/index.js +2 -6
  20. package/dist/cjs/smtp-connection/http-proxy-client.js +1 -1
  21. package/dist/cjs/smtp-connection/index.d.ts +6 -0
  22. package/dist/cjs/smtp-connection/index.js +15 -12
  23. package/dist/cjs/smtp-connection/server-message-id.d.ts +14 -0
  24. package/dist/cjs/smtp-connection/server-message-id.js +53 -0
  25. package/dist/cjs/smtp-pool/index.d.ts +11 -0
  26. package/dist/cjs/smtp-pool/index.js +25 -1
  27. package/dist/cjs/smtp-pool/pool-resource.js +2 -5
  28. package/dist/esm/addressparser/index.js +2 -5
  29. package/dist/esm/dkim/index.js +1 -3
  30. package/dist/esm/errors.d.ts +19 -0
  31. package/dist/esm/errors.js +12 -0
  32. package/dist/esm/fetch/cookies.js +2 -4
  33. package/dist/esm/fetch/index.js +2 -8
  34. package/dist/esm/mail-composer/index.js +42 -9
  35. package/dist/esm/mailer/mail-message.js +5 -6
  36. package/dist/esm/mime-funcs/index.d.ts +18 -1
  37. package/dist/esm/mime-funcs/index.js +43 -1
  38. package/dist/esm/mime-funcs/mime-types.js +3 -12
  39. package/dist/esm/mime-node/index.d.ts +34 -1
  40. package/dist/esm/mime-node/index.js +40 -15
  41. package/dist/esm/package-info.d.ts +1 -1
  42. package/dist/esm/package-info.js +1 -1
  43. package/dist/esm/qp/index.js +3 -12
  44. package/dist/esm/sendmail-transport/index.js +1 -3
  45. package/dist/esm/shared/index.js +2 -6
  46. package/dist/esm/smtp-connection/http-proxy-client.js +1 -1
  47. package/dist/esm/smtp-connection/index.d.ts +6 -0
  48. package/dist/esm/smtp-connection/index.js +16 -13
  49. package/dist/esm/smtp-connection/server-message-id.d.ts +14 -0
  50. package/dist/esm/smtp-connection/server-message-id.js +50 -0
  51. package/dist/esm/smtp-pool/index.d.ts +11 -0
  52. package/dist/esm/smtp-pool/index.js +25 -1
  53. package/dist/esm/smtp-pool/pool-resource.js +2 -5
  54. package/package.json +11 -9
@@ -50,15 +50,9 @@ function encodeBytes(buffer, next, binary, previous) {
50
50
  for (let i = 0; i < len; i++) {
51
51
  const ord = buffer[i];
52
52
  const following = i + 1 < len ? buffer[i + 1] : next;
53
- const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a)
54
- ? ord === 0x0d
55
- ? following === 0x0a
56
- : (i > 0 ? buffer[i - 1] : previous) === 0x0d
57
- : true;
53
+ const lineBreakByte = binary && (ord === 0x0d || ord === 0x0a) ? (ord === 0x0d ? following === 0x0a : (i > 0 ? buffer[i - 1] : previous) === 0x0d) : true;
58
54
  // if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
59
- if (QP_LITERAL[ord] &&
60
- lineBreakByte &&
61
- !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
55
+ if (QP_LITERAL[ord] && lineBreakByte && !(isWhitespace(ord) && (following === undefined || following === 0x0a || following === 0x0d))) {
62
56
  output[pos++] = ord;
63
57
  continue;
64
58
  }
@@ -119,10 +113,7 @@ function wrap(str, lineLength) {
119
113
  line = line.substr(0, line.length - match[0].length);
120
114
  }
121
115
  // ensure that utf-8 sequences are not split
122
- while (line.length > 3 &&
123
- line.length < len - pos &&
124
- !line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
125
- (match = line.match(/[=][\da-f]{2}$/gi))) {
116
+ while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) {
126
117
  code = parseInt(match[0].substr(1, 2), 16);
127
118
  if (code < 128) {
128
119
  break;
@@ -109,9 +109,7 @@ class SendmailTransport {
109
109
  return done(err);
110
110
  }
111
111
  // force -i to keep single dots
112
- const args = this.args
113
- ? ['-i'].concat(this.args).concat(envelope.to)
114
- : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
112
+ const args = this.args ? ['-i'].concat(this.args).concat(envelope.to) : ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
115
113
  const callback = (err) => {
116
114
  if (returned) {
117
115
  // ignore any additional responses, already done
@@ -263,9 +263,7 @@ const resolveHostname = (options, callback) => {
263
263
  return respond(err);
264
264
  }
265
265
  // Get all supported addresses from dns.lookup
266
- const supportedAddresses = addresses
267
- ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
268
- : [];
266
+ const supportedAddresses = addresses ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address) : [];
269
267
  if (addresses && addresses.length && !supportedAddresses.length) {
270
268
  // there are addresses but none can be used
271
269
  console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
@@ -601,9 +599,7 @@ const assign = function (...args) {
601
599
  if ((0, objects_js_1.isProtoKey)(key)) {
602
600
  return;
603
601
  }
604
- if (['tls', 'auth'].includes(key) &&
605
- source[key] &&
606
- typeof source[key] === 'object') {
602
+ if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
607
603
  // tls and auth are special keys that need to be enumerated separately
608
604
  // other objects are passed as is. Enumerating is a copy of user supplied
609
605
  // keys just like the loop above, so it gets the same treatment
@@ -47,7 +47,7 @@ const errors = __importStar(require("../errors.js"));
47
47
  // never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
48
48
  const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
49
49
  // URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
50
- const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
50
+ const unbracket = (host) => (typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host);
51
51
  function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
52
52
  if (typeof tlsOptions === 'function') {
53
53
  callback = tlsOptions;
@@ -245,6 +245,12 @@ export interface SMTPConnectionSendInfo extends SMTPConnectionEnvelopeInfo {
245
245
  messageSize: number;
246
246
  /** Final server response for the message */
247
247
  response: string;
248
+ /**
249
+ * Message-ID the server gave the message, with angle brackets, when its final reply names
250
+ * one in a known format (Microsoft 365 and Outlook.com, Amazon SES). Such a server replaces
251
+ * the Message-ID header, so the delivered copy carries this id instead of messageId
252
+ */
253
+ serverMessageId?: string | undefined;
248
254
  }
249
255
  /**
250
256
  * Callback for send(), receives the result once the server accepted the message. The error
@@ -45,6 +45,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
45
45
  const data_stream_js_1 = __importDefault(require("./data-stream.js"));
46
46
  const node_stream_1 = require("node:stream");
47
47
  const shared = __importStar(require("../shared/index.js"));
48
+ const server_message_id_js_1 = require("./server-message-id.js");
48
49
  const errors_js_1 = require("../errors.js");
49
50
  // default timeout values in ms
50
51
  const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
@@ -399,9 +400,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
399
400
  if (this._destroyed || this._closing) {
400
401
  return;
401
402
  }
402
- const connectFn = secure
403
- ? node_tls_1.default.connect
404
- : node_net_1.default.connect;
403
+ const connectFn = secure ? node_tls_1.default.connect : node_net_1.default.connect;
405
404
  try {
406
405
  this._socket = connectFn(opts, () => this._onConnect());
407
406
  this._setupConnectionHandlers();
@@ -739,6 +738,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
739
738
  result.messageTime = Date.now() - envelopeTime;
740
739
  result.messageSize = stream.outByteCount;
741
740
  result.response = str;
741
+ const serverMessageId = (0, server_message_id_js_1.parseServerMessageId)(result.response, this.options.host);
742
+ if (serverMessageId) {
743
+ result.serverMessageId = serverMessageId;
744
+ }
745
+ // an LMTP server answers the message once per recipient, so recipients can be
746
+ // refused after the envelope info was built without any rejected errors on it
747
+ const rejectedErrors = this._envelope?.rejectedErrors;
748
+ if (!result.rejectedErrors && rejectedErrors && rejectedErrors.length) {
749
+ result.rejectedErrors = rejectedErrors;
750
+ }
742
751
  return callback(null, result);
743
752
  });
744
753
  if (typeof message.pipe === 'function') {
@@ -966,6 +975,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
966
975
  const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
967
976
  if (responseCode) {
968
977
  err.responseCode = responseCode;
978
+ err.permanent = (0, errors_js_1.isPermanentResponseCode)(responseCode);
969
979
  }
970
980
  if (command) {
971
981
  err.command = command;
@@ -1008,12 +1018,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
1008
1018
  this.close();
1009
1019
  return;
1010
1020
  }
1011
- if (!failureResponse &&
1012
- this.stage === 'connected' &&
1013
- !this._responseActions.length &&
1014
- !this._pendingSend &&
1015
- !this._destroyed &&
1016
- !this._closing) {
1021
+ if (!failureResponse && this.stage === 'connected' && !this._responseActions.length && !this._pendingSend && !this._destroyed && !this._closing) {
1017
1022
  // nothing was waiting for the server, so this is the server ending an idle session
1018
1023
  // (usually after its idle timeout), not a failure
1019
1024
  this.logger.info({
@@ -1234,9 +1239,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
1234
1239
  const args = [];
1235
1240
  let useSmtpUtf8 = false;
1236
1241
  this._envelope = (envelope || {});
1237
- this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '')
1238
- .toString()
1239
- .trim();
1242
+ this._envelope.from = ((this._envelope.from && this._envelope.from.address) || this._envelope.from || '').toString().trim();
1240
1243
  this._envelope.to = []
1241
1244
  .concat(this._envelope.to || [])
1242
1245
  .map(to => ((to && to.address) || to || '').toString().trim());
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
3
+ * servers replace the Message-ID header of a submitted message with one of their own, and
4
+ * the copy the recipients and the sent folder get carries that one instead of the id the
5
+ * message was built with. Only replies in a format known to carry such an id are read.
6
+ */
7
+ /**
8
+ * Reads the server assigned Message-ID out of the final reply to a message
9
+ *
10
+ * @param response Final server reply for the message
11
+ * @param [host] Hostname the connection was made to, SES replies are only read for an SES host
12
+ * @returns The Message-ID with its angle brackets, or undefined when the reply names none
13
+ */
14
+ export declare function parseServerMessageId(response: string | undefined, host?: string): string | undefined;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Reads the Message-ID a receiving server gave the message out of its final 250 reply. Some
4
+ * servers replace the Message-ID header of a submitted message with one of their own, and
5
+ * the copy the recipients and the sent folder get carries that one instead of the id the
6
+ * message was built with. Only replies in a format known to carry such an id are read.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parseServerMessageId = parseServerMessageId;
10
+ // Microsoft 365 and Outlook.com: "250 2.0.0 OK <id@host.prod.outlook.com> [Hostname=...]"
11
+ const OUTLOOK_REPLY = /^250 2\.0\.0 OK (<[^<>\s]+\.prod\.outlook\.com>)/i;
12
+ // Amazon SES: "250 Ok 0100018b5c3c0a4f-...-000000", the local part of the Message-ID it sets
13
+ const SES_REPLY = /^250 Ok ([0-9a-f]+(?:-[0-9a-f]+)+)$/i;
14
+ // an AWS region name such as us-east-1 or ap-southeast-2
15
+ const AWS_REGION = /^[a-z]{2}(?:-[a-z]+)+-\d+$/;
16
+ /**
17
+ * Finds the AWS region an SES SMTP endpoint is in from its hostname, such as
18
+ * email-smtp.eu-west-1.amazonaws.com or a VPC endpoint name under it
19
+ *
20
+ * @param host SMTP hostname
21
+ * @returns Region name, or undefined for a host outside amazonaws.com and awsapps.com
22
+ */
23
+ function sesRegion(host) {
24
+ const labels = host.toLowerCase().replace(/\.$/, '').split('.');
25
+ const domain = labels.slice(-2).join('.');
26
+ if (domain !== 'amazonaws.com' && domain !== 'awsapps.com') {
27
+ return undefined;
28
+ }
29
+ return labels.slice(0, -2).find(label => AWS_REGION.test(label));
30
+ }
31
+ /**
32
+ * Reads the server assigned Message-ID out of the final reply to a message
33
+ *
34
+ * @param response Final server reply for the message
35
+ * @param [host] Hostname the connection was made to, SES replies are only read for an SES host
36
+ * @returns The Message-ID with its angle brackets, or undefined when the reply names none
37
+ */
38
+ function parseServerMessageId(response, host) {
39
+ const reply = (response || '').toString().trim();
40
+ const outlook = reply.match(OUTLOOK_REPLY);
41
+ if (outlook) {
42
+ return outlook[1];
43
+ }
44
+ const ses = reply.match(SES_REPLY);
45
+ if (ses && host) {
46
+ const region = sesRegion(host);
47
+ if (region) {
48
+ // the SES Message-ID domain is email.amazonses.com in us-east-1 and the region elsewhere
49
+ return '<' + ses[1].toLowerCase() + '@' + (region === 'us-east-1' ? 'email' : region) + '.amazonses.com>';
50
+ }
51
+ }
52
+ return undefined;
53
+ }
@@ -110,6 +110,17 @@ declare class SMTPPool extends EventEmitter {
110
110
  * Returns true if there are free slots in the queue
111
111
  */
112
112
  isIdle(): boolean;
113
+ /**
114
+ * Counts the messages the pool has not finished with yet: the ones waiting in the queue
115
+ * plus the ones a connection is transmitting right now. isIdle() only tells whether the
116
+ * pool can take another message, it is already true while a message is still being sent,
117
+ * so use this to check that nothing would be lost by closing the pool. A message that is
118
+ * re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
119
+ * it is no longer counted by the time its callback runs.
120
+ *
121
+ * @returns Number of queued and in-flight messages
122
+ */
123
+ pendingCount(): number;
113
124
  /**
114
125
  * Verifies SMTP configuration
115
126
  *
@@ -100,6 +100,7 @@ class SMTPPool extends node_events_1.EventEmitter {
100
100
  this._queue = [];
101
101
  this._connections = [];
102
102
  this._connectionCounter = 0;
103
+ this._pending = 0;
103
104
  this.idling = true;
104
105
  setImmediate(() => {
105
106
  if (this.idling) {
@@ -132,10 +133,20 @@ class SMTPPool extends node_events_1.EventEmitter {
132
133
  setImmediate(() => callback(err));
133
134
  return false;
134
135
  }
136
+ // counted until the callback runs, whichever path runs it: delivered, failed, requeued
137
+ // until it gives up, or drained by close()
138
+ let counted = true;
139
+ this._pending++;
135
140
  this._queue.push({
136
141
  mail,
137
142
  requeueAttempts: 0,
138
- callback
143
+ callback: (err, info) => {
144
+ if (counted) {
145
+ counted = false;
146
+ this._pending--;
147
+ }
148
+ callback(err, info);
149
+ }
139
150
  });
140
151
  if (this.idling && this._queue.length >= this.options.maxConnections) {
141
152
  this.idling = false;
@@ -479,6 +490,19 @@ class SMTPPool extends node_events_1.EventEmitter {
479
490
  isIdle() {
480
491
  return this.idling;
481
492
  }
493
+ /**
494
+ * Counts the messages the pool has not finished with yet: the ones waiting in the queue
495
+ * plus the ones a connection is transmitting right now. isIdle() only tells whether the
496
+ * pool can take another message, it is already true while a message is still being sent,
497
+ * so use this to check that nothing would be lost by closing the pool. A message that is
498
+ * re-queued after its connection dropped is counted until its callback runs, also while it waits to be put back on the queue, and
499
+ * it is no longer counted by the time its callback runs.
500
+ *
501
+ * @returns Number of queued and in-flight messages
502
+ */
503
+ pendingCount() {
504
+ return this._pending;
505
+ }
482
506
  verify(callback) {
483
507
  let promise;
484
508
  if (!callback) {
@@ -57,8 +57,7 @@ class PoolResource extends node_events_1.EventEmitter {
57
57
  switch ((this.options.auth.type || '').toString().toUpperCase()) {
58
58
  case 'OAUTH2': {
59
59
  const oauth2 = new index_js_3.default(this.options.auth, this.logger);
60
- oauth2.provisionCallback =
61
- (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
60
+ oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
62
61
  this.auth = {
63
62
  type: 'OAUTH2',
64
63
  user: this.options.auth.user,
@@ -246,9 +245,7 @@ class PoolResource extends node_events_1.EventEmitter {
246
245
  this.connection.close();
247
246
  return;
248
247
  }
249
- if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) &&
250
- err.responseCode !== 421 &&
251
- !this.connection._destroyed) {
248
+ if ((err.code === errors.EENVELOPE || err.code === errors.EMESSAGE) && err.responseCode !== 421 && !this.connection._destroyed) {
252
249
  // The server refused this message, the connection itself is fine. Reset the
253
250
  // session and keep using it instead of opening a new one
254
251
  this.connection.reset(resetErr => {
@@ -691,9 +691,7 @@ function _handleAddress(tokens, from, to) {
691
691
  // A run that is a quoted local part with its '@' and domain is an addr-spec of
692
692
  // its own, with the quoting put back on, and takes its turn like any other
693
693
  const local = data.quotedLocalAt[i];
694
- const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local))
695
- ? _quoteLocalPart(data.text[i])
696
- : '';
694
+ const quotedLocal = data.textWasQuoted[i] && local >= 0 && QUOTED_LOCAL_DOMAIN.test(data.text[i].slice(local)) ? _quoteLocalPart(data.text[i]) : '';
697
695
  if (quotedLocal || (!data.textWasQuoted[i] && _isAddrSpec(candidates[i]))) {
698
696
  data.address = [quotedLocal || candidates[i]];
699
697
  data.text.splice(i, 1);
@@ -1018,8 +1016,7 @@ class Tokenizer {
1018
1016
  return;
1019
1017
  }
1020
1018
  if (this.node < 0) {
1021
- this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
1022
- (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
1019
+ this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) | (this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
1023
1020
  }
1024
1021
  if (chr === '\n') {
1025
1022
  // Convert newlines to spaces. Carriage return is ignored as \r and \n usually
@@ -20,9 +20,7 @@ class DKIMSigner {
20
20
  this.chunks = [];
21
21
  this.chunklen = 0;
22
22
  this.readPos = 0;
23
- this.cachePath = this.cacheDir
24
- ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex'))
25
- : false;
23
+ this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false;
26
24
  this.cache = false;
27
25
  this.headers = false;
28
26
  this.bodyHash = false;
@@ -68,6 +68,18 @@ export declare const isTransientError: (err: {
68
68
  code?: string | undefined;
69
69
  responseCode?: number | undefined;
70
70
  }) => boolean;
71
+ /**
72
+ * Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
73
+ * message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
74
+ * and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
75
+ * says the session was in the wrong state for the command rather than that the message was
76
+ * refused, so the same message can go through on a new connection. 2yz and 3yz replies are
77
+ * only an error when the client expected something else, and are not permanent either.
78
+ *
79
+ * @param responseCode SMTP reply code
80
+ * @returns true for a permanent rejection
81
+ */
82
+ export declare const isPermanentResponseCode: (responseCode: number) => boolean;
71
83
  /**
72
84
  * Code Node.js sets on an error when its permission model (`--permission`) denies an
73
85
  * operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
@@ -89,6 +101,13 @@ export interface NodemailerError extends NodeJS.ErrnoException {
89
101
  response?: string | undefined;
90
102
  /** Numeric SMTP response code */
91
103
  responseCode?: number | undefined;
104
+ /**
105
+ * Set together with responseCode, from the reply class: true when the server rejected the
106
+ * message for good (5xx except 503), false when the same message may be accepted on a later
107
+ * attempt (4xx, 503, or an unexpected 2xx or 3xx). Not set on an error that carries no SMTP
108
+ * reply, such as a connection failure or missing credentials, see isPermanentResponseCode
109
+ */
110
+ permanent?: boolean | undefined;
92
111
  /** URL of the resource that could not be fetched */
93
112
  sourceUrl?: string | undefined;
94
113
  /** Recipient address the error applies to */
@@ -69,6 +69,18 @@ export const EFETCH = 'EFETCH';
69
69
  * Tells whether the error ended the connection rather than rejected what was sent over it
70
70
  */
71
71
  export const isTransientError = (err) => err.responseCode === 421 || err.code === ECONNECTION || err.code === ESOCKET || err.code === ETIMEDOUT;
72
+ /**
73
+ * Tells whether an SMTP reply code is a permanent rejection, one that retrying the same
74
+ * message would get again. RFC 5321 makes every 5yz reply a permanent negative completion
75
+ * and every 4yz reply a transient one. 503 (bad sequence of commands) is the exception: it
76
+ * says the session was in the wrong state for the command rather than that the message was
77
+ * refused, so the same message can go through on a new connection. 2yz and 3yz replies are
78
+ * only an error when the client expected something else, and are not permanent either.
79
+ *
80
+ * @param responseCode SMTP reply code
81
+ * @returns true for a permanent rejection
82
+ */
83
+ export const isPermanentResponseCode = (responseCode) => responseCode >= 500 && responseCode < 600 && responseCode !== 503;
72
84
  /**
73
85
  * Code Node.js sets on an error when its permission model (`--permission`) denies an
74
86
  * operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
@@ -160,16 +160,14 @@ class Cookies {
160
160
  // check if hostname matches
161
161
  // .foo.com also matches subdomains, foo.com does not
162
162
  if (urlparts.hostname !== cookie.domain &&
163
- (cookie.domain.charAt(0) !== '.' ||
164
- ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
163
+ (cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
165
164
  return false;
166
165
  }
167
166
  // check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
168
167
  // identical paths match, otherwise the cookie path must be a directory prefix
169
168
  const pathname = urlparts.pathname || '/';
170
169
  const cookiePath = cookie.path;
171
- const pathMatches = pathname === cookiePath ||
172
- (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
170
+ const pathMatches = pathname === cookiePath || (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
173
171
  if (!pathMatches) {
174
172
  return false;
175
173
  }
@@ -224,11 +224,7 @@ function nmfetch(url, options) {
224
224
  }
225
225
  });
226
226
  }
227
- if (parsed.protocol === 'https:' &&
228
- parsed.hostname &&
229
- parsed.hostname !== reqOptions.host &&
230
- !net.isIP(parsed.hostname) &&
231
- !reqOptions.servername) {
227
+ if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
232
228
  reqOptions.servername = parsed.hostname;
233
229
  }
234
230
  try {
@@ -331,9 +327,7 @@ function nmfetch(url, options) {
331
327
  // downgrade would also put them on the wire in cleartext). Strip sensitive
332
328
  // request headers so an attacker who controls the redirect target cannot
333
329
  // harvest them.
334
- const crossOrigin = redirectParsed.protocol !== parsed.protocol ||
335
- redirectParsed.hostname !== parsed.hostname ||
336
- fetchPort(redirectParsed) !== fetchPort(parsed);
330
+ const crossOrigin = redirectParsed.protocol !== parsed.protocol || redirectParsed.hostname !== parsed.hostname || fetchPort(redirectParsed) !== fetchPort(parsed);
337
331
  if (options.headers && crossOrigin) {
338
332
  const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
339
333
  Object.keys(options.headers).forEach(key => {
@@ -16,6 +16,40 @@ function isContentObject(value) {
16
16
  const content = value;
17
17
  return typeof value === 'object' && !!(content.content || content.path || content.href || content.raw);
18
18
  }
19
+ /**
20
+ * Tells whether attachment content is a string or a Buffer that 7bit can describe: ASCII
21
+ * only, no NUL and no line over the 998 octets of RFC 5322. Anything else, a stream or a
22
+ * file path included, can not be checked here
23
+ */
24
+ function fitsSevenBit(content) {
25
+ let at;
26
+ let length;
27
+ if (typeof content === 'string') {
28
+ at = i => content.charCodeAt(i);
29
+ length = content.length;
30
+ }
31
+ else if (Buffer.isBuffer(content)) {
32
+ at = i => content[i];
33
+ length = content.length;
34
+ }
35
+ else {
36
+ return false;
37
+ }
38
+ let lineLength = 0;
39
+ for (let i = 0; i < length; i++) {
40
+ const c = at(i);
41
+ if (c === 0 || c > 0x7f) {
42
+ return false;
43
+ }
44
+ if (c === 0x0a || c === 0x0d) {
45
+ lineLength = 0;
46
+ }
47
+ else if (++lineLength > 998) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
19
53
  class MailComposer {
20
54
  constructor(mail) {
21
55
  this.mail = mail || {};
@@ -98,8 +132,11 @@ class MailComposer {
98
132
  contentTransferEncoding = attachment.contentTransferEncoding;
99
133
  }
100
134
  else if (isMessageNode) {
101
- // the content might include non-ASCII bytes but at this point we do not know it yet
102
- contentTransferEncoding = '8bit';
135
+ // a message/* part can not be encoded (RFC 2046 5.2.1), so the label has to describe
136
+ // the content as it is. A stream or a file might hold non-ASCII bytes that can not be
137
+ // checked here, so only content at hand that fits 7bit is labelled as such
138
+ // content with an `encoding` is decoded later, so what it decodes to is not known here either
139
+ contentTransferEncoding = !attachment.encoding && fitsSevenBit(attachment.content) ? '7bit' : '8bit';
103
140
  }
104
141
  else {
105
142
  contentTransferEncoding = 'base64'; // the default
@@ -114,9 +151,7 @@ class MailComposer {
114
151
  }
115
152
  else if (!isMessageNode && attachment.filename !== false) {
116
153
  // a backslash separates as well, so a Windows path does not put the sender's directories in the headers
117
- data.filename =
118
- (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
119
- 'attachment-' + (i + 1);
154
+ data.filename = (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
120
155
  if (data.filename.indexOf('.') < 0) {
121
156
  data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
122
157
  }
@@ -277,8 +312,7 @@ class MailComposer {
277
312
  if (this.mail.icalEvent) {
278
313
  eventObject = Object.assign({}, this._getIcalEvent());
279
314
  eventObject.filename = false;
280
- eventObject.contentType =
281
- 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
315
+ eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
282
316
  if (!eventObject.headers) {
283
317
  eventObject.headers = {};
284
318
  }
@@ -306,8 +340,7 @@ class MailComposer {
306
340
  alternative = this._processDataUrl(alternative);
307
341
  }
308
342
  const data = {
309
- contentType: alternative.contentType ||
310
- mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
343
+ contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
311
344
  contentTransferEncoding: alternative.contentTransferEncoding
312
345
  };
313
346
  if (alternative.filename) {
@@ -71,8 +71,7 @@ export default class MailMessage {
71
71
  if (!attachment.filename) {
72
72
  // a backslash separates as well, so a Windows path does not put the sender's directories in the headers
73
73
  attachment.filename =
74
- (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
75
- 'attachment-' + (i + 1);
74
+ (attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() || 'attachment-' + (i + 1);
76
75
  if (attachment.filename.indexOf('.') < 0) {
77
76
  attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
78
77
  }
@@ -207,7 +206,7 @@ export default class MailMessage {
207
206
  value = (value && value.value) || value;
208
207
  if (value) {
209
208
  if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
210
- value = this.message._encodeHeaderValue(key, value);
209
+ value = this.message.encodeHeaderValue(key, value);
211
210
  }
212
211
  data.normalizedHeaders[key] = value;
213
212
  }
@@ -219,10 +218,10 @@ export default class MailMessage {
219
218
  });
220
219
  }
221
220
  if (data.references) {
222
- data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
221
+ data.normalizedHeaders.references = this.message.encodeHeaderValue('references', data.references);
223
222
  }
224
223
  if (data.inReplyTo) {
225
- data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
224
+ data.normalizedHeaders['in-reply-to'] = this.message.encodeHeaderValue('in-reply-to', data.inReplyTo);
226
225
  }
227
226
  return callback(null, data);
228
227
  });
@@ -291,7 +290,7 @@ export default class MailMessage {
291
290
  comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
292
291
  // List-ID expects a bare domain-like identifier, so strip the
293
292
  // scheme prefix that _formatListUrl adds or passes through
294
- return ((value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<'));
293
+ return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<');
295
294
  }
296
295
  // List-*: <http://domain> (comment)
297
296
  // the ctext specials go out as quoted-pairs, otherwise a ")" closes the
@@ -107,6 +107,22 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str
107
107
  * @return A list of encoded keys and headers
108
108
  */
109
109
  export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[];
110
+ /**
111
+ * Options of parseHeaderValue()
112
+ */
113
+ export interface ParseHeaderValueOptions {
114
+ /**
115
+ * Drop the RFC 822 comments outside quoted strings, which the MIME headers allow and
116
+ * which are not part of the value: 'boundary=b (comment)' names the boundary 'b'. A
117
+ * parenthesis right behind the '=' of a parameter or behind a token character of its
118
+ * value continues the token, as it does for Python and postal-mime, so 'Invoice(1).pdf'
119
+ * keeps its name and 'boundary=(c)b' names the boundary '(c)b'. A comment that never
120
+ * closes runs to the end of the value, unless a parameter follows it, in which case it is
121
+ * content and the parameter survives. Comments are kept by default for the sake of the
122
+ * callers of this exported function, not because keeping them is ever the better reading
123
+ */
124
+ stripComments?: boolean | undefined;
125
+ }
110
126
  /**
111
127
  * Parses a header value with key=value arguments into a structured
112
128
  * object.
@@ -120,9 +136,10 @@ export declare function buildHeaderParam(key: string, data: string | Buffer, max
120
136
  * }
121
137
  *
122
138
  * @param str Header value
139
+ * @param options Options, see ParseHeaderValueOptions
123
140
  * @return Header value as a parsed structure
124
141
  */
125
- export declare function parseHeaderValue(str: string): ParsedHeaderValue;
142
+ export declare function parseHeaderValue(str: string, options?: ParseHeaderValueOptions): ParsedHeaderValue;
126
143
  /**
127
144
  * Returns file extension for a content type string. If no suitable extensions
128
145
  * are found, 'bin' is used as the default extension