nodemailer 10.1.7 → 10.2.0

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 (50) hide show
  1. package/CHANGELOG.md +16 -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/mime-types.js +3 -12
  11. package/dist/cjs/mime-node/index.d.ts +34 -1
  12. package/dist/cjs/mime-node/index.js +35 -13
  13. package/dist/cjs/package-info.d.ts +1 -1
  14. package/dist/cjs/package-info.js +1 -1
  15. package/dist/cjs/qp/index.js +3 -12
  16. package/dist/cjs/sendmail-transport/index.js +1 -3
  17. package/dist/cjs/shared/index.js +2 -6
  18. package/dist/cjs/smtp-connection/http-proxy-client.js +1 -1
  19. package/dist/cjs/smtp-connection/index.d.ts +6 -0
  20. package/dist/cjs/smtp-connection/index.js +15 -12
  21. package/dist/cjs/smtp-connection/server-message-id.d.ts +14 -0
  22. package/dist/cjs/smtp-connection/server-message-id.js +53 -0
  23. package/dist/cjs/smtp-pool/index.d.ts +11 -0
  24. package/dist/cjs/smtp-pool/index.js +25 -1
  25. package/dist/cjs/smtp-pool/pool-resource.js +2 -5
  26. package/dist/esm/addressparser/index.js +2 -5
  27. package/dist/esm/dkim/index.js +1 -3
  28. package/dist/esm/errors.d.ts +19 -0
  29. package/dist/esm/errors.js +12 -0
  30. package/dist/esm/fetch/cookies.js +2 -4
  31. package/dist/esm/fetch/index.js +2 -8
  32. package/dist/esm/mail-composer/index.js +42 -9
  33. package/dist/esm/mailer/mail-message.js +5 -6
  34. package/dist/esm/mime-funcs/mime-types.js +3 -12
  35. package/dist/esm/mime-node/index.d.ts +34 -1
  36. package/dist/esm/mime-node/index.js +35 -13
  37. package/dist/esm/package-info.d.ts +1 -1
  38. package/dist/esm/package-info.js +1 -1
  39. package/dist/esm/qp/index.js +3 -12
  40. package/dist/esm/sendmail-transport/index.js +1 -3
  41. package/dist/esm/shared/index.js +2 -6
  42. package/dist/esm/smtp-connection/http-proxy-client.js +1 -1
  43. package/dist/esm/smtp-connection/index.d.ts +6 -0
  44. package/dist/esm/smtp-connection/index.js +16 -13
  45. package/dist/esm/smtp-connection/server-message-id.d.ts +14 -0
  46. package/dist/esm/smtp-connection/server-message-id.js +50 -0
  47. package/dist/esm/smtp-pool/index.d.ts +11 -0
  48. package/dist/esm/smtp-pool/index.js +25 -1
  49. package/dist/esm/smtp-pool/pool-resource.js +2 -5
  50. package/package.json +4 -2
@@ -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
@@ -1097,10 +1097,7 @@ const extensions = new Map([
1097
1097
  ['bdm', 'application/vnd.syncml.dm+wbxml'],
1098
1098
  ['bed', 'application/vnd.realvnc.bed'],
1099
1099
  ['bh2', 'application/vnd.fujitsu.oasysprs'],
1100
- [
1101
- 'bin',
1102
- ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']
1103
- ],
1100
+ ['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']],
1104
1101
  ['bm', 'image/bmp'],
1105
1102
  ['bmi', 'application/vnd.bmi'],
1106
1103
  ['bmp', ['image/bmp', 'image/x-windows-bmp']],
@@ -1145,10 +1142,7 @@ const extensions = new Map([
1145
1142
  ['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
1146
1143
  ['cil', 'application/vnd.ms-artgalry'],
1147
1144
  ['cla', 'application/vnd.claymore'],
1148
- [
1149
- 'class',
1150
- ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']
1151
- ],
1145
+ ['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']],
1152
1146
  ['clkk', 'application/vnd.crick.clicker.keyboard'],
1153
1147
  ['clkp', 'application/vnd.crick.clicker.palette'],
1154
1148
  ['clkt', 'application/vnd.crick.clicker.template'],
@@ -1753,10 +1747,7 @@ const extensions = new Map([
1753
1747
  ['sbml', 'application/sbml+xml'],
1754
1748
  ['sc', 'application/vnd.ibm.secure-container'],
1755
1749
  ['scd', 'application/x-msschedule'],
1756
- [
1757
- 'scm',
1758
- ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']
1759
- ],
1750
+ ['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']],
1760
1751
  ['scq', 'application/scvp-cv-request'],
1761
1752
  ['scs', 'application/scvp-cv-response'],
1762
1753
  ['sct', 'text/scriptlet'],
@@ -58,6 +58,13 @@ export interface MimeNodePreparedHeaderValue {
58
58
  * objects (for the address headers), a prepared value object, or a list of these
59
59
  */
60
60
  export type MimeNodeHeaderValue = string | number | boolean | Date | MimeNodeAddress | MimeNodePreparedHeaderValue | MimeNodeHeaderValue[] | null | undefined;
61
+ /**
62
+ * Options for the static MimeNode.encodeHeaderValue
63
+ */
64
+ export interface MimeNodeHeaderEncodingOptions {
65
+ /** Either 'Q' (the default) or 'B' */
66
+ textEncoding?: string | undefined;
67
+ }
61
68
  /**
62
69
  * A single header, the shape the headers are stored in and one of the list forms
63
70
  * setHeader and addHeader accept
@@ -319,6 +326,31 @@ declare class MimeNode {
319
326
  * @param raw Raw MIME contents
320
327
  */
321
328
  setRaw(raw: MimeNodeContent): this;
329
+ /**
330
+ * Encodes a header value the way this node encodes it in the generated message:
331
+ * address headers are punycoded and mime encoded, message id headers get their
332
+ * angle brackets, a Date object is formatted and any other value is mime encoded
333
+ * when it holds non-ascii or control characters (using the node's `textEncoding`).
334
+ *
335
+ * The result is a single unfolded line without the key. Fold it before writing it
336
+ * into a message, `key + ': ' + value` folded at 76 characters is what the node does.
337
+ *
338
+ * @param key Header key, case insensitive
339
+ * @param value Header value
340
+ * @returns Encoded header value
341
+ */
342
+ encodeHeaderValue(key: string, value: MimeNodeHeaderValue): string;
343
+ /**
344
+ * Encodes a header value without a node at hand. Same as the instance method of a
345
+ * node created with the given options.
346
+ *
347
+ * @param key Header key, case insensitive
348
+ * @param value Header value
349
+ * @param [options] Encoding options
350
+ * @param [options.textEncoding] either 'Q' (the default) or 'B'
351
+ * @returns Encoded header value
352
+ */
353
+ static encodeHeaderValue(key: string, value: MimeNodeHeaderValue, options?: MimeNodeHeaderEncodingOptions): string;
322
354
  }
323
355
  /**
324
356
  * Type aliases in the layout of @types/nodemailer, so `MimeNode.Options` style references keep working
@@ -327,7 +359,8 @@ declare namespace MimeNode {
327
359
  type Options = MimeNodeOptions;
328
360
  type Addresses = MimeNodeAddresses;
329
361
  type Envelope = MimeNodeEnvelope;
362
+ type HeaderEncodingOptions = MimeNodeHeaderEncodingOptions;
330
363
  }
331
364
  /** The same aliases as module level exports, for `import * as MimeNode` and `import MimeNode = require()` */
332
- export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope };
365
+ export type { MimeNodeOptions as Options, MimeNodeAddresses as Addresses, MimeNodeEnvelope as Envelope, MimeNodeHeaderEncodingOptions as HeaderEncodingOptions };
333
366
  export default MimeNode;