nodemailer 10.0.11 → 10.0.13

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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/addressparser/index.js +97 -0
  3. package/dist/cjs/dkim/index.js +25 -8
  4. package/dist/cjs/dkim/sign.d.ts +2 -0
  5. package/dist/cjs/dkim/sign.js +17 -1
  6. package/dist/cjs/fetch/index.d.ts +4 -1
  7. package/dist/cjs/fetch/index.js +34 -59
  8. package/dist/cjs/package-info.d.ts +1 -1
  9. package/dist/cjs/package-info.js +1 -1
  10. package/dist/cjs/smtp-connection/data-stream.d.ts +2 -1
  11. package/dist/cjs/smtp-connection/data-stream.js +32 -24
  12. package/dist/cjs/smtp-connection/index.d.ts +5 -5
  13. package/dist/cjs/smtp-connection/index.js +78 -24
  14. package/dist/cjs/smtp-pool/index.d.ts +2 -1
  15. package/dist/cjs/smtp-pool/index.js +31 -5
  16. package/dist/cjs/smtp-pool/pool-resource.js +27 -29
  17. package/dist/cjs/smtp-transport/index.js +17 -27
  18. package/dist/cjs/xoauth2/index.d.ts +2 -0
  19. package/dist/cjs/xoauth2/index.js +11 -3
  20. package/dist/esm/addressparser/index.js +97 -0
  21. package/dist/esm/dkim/index.js +25 -8
  22. package/dist/esm/dkim/sign.d.ts +2 -0
  23. package/dist/esm/dkim/sign.js +17 -1
  24. package/dist/esm/fetch/index.d.ts +4 -1
  25. package/dist/esm/fetch/index.js +34 -59
  26. package/dist/esm/package-info.d.ts +1 -1
  27. package/dist/esm/package-info.js +1 -1
  28. package/dist/esm/smtp-connection/data-stream.d.ts +2 -1
  29. package/dist/esm/smtp-connection/data-stream.js +32 -24
  30. package/dist/esm/smtp-connection/index.d.ts +5 -5
  31. package/dist/esm/smtp-connection/index.js +78 -24
  32. package/dist/esm/smtp-pool/index.d.ts +2 -1
  33. package/dist/esm/smtp-pool/index.js +31 -5
  34. package/dist/esm/smtp-pool/pool-resource.js +27 -29
  35. package/dist/esm/smtp-transport/index.js +17 -27
  36. package/dist/esm/xoauth2/index.d.ts +2 -0
  37. package/dist/esm/xoauth2/index.js +11 -3
  38. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.0.13](https://github.com/nodemailer/nodemailer/compare/v10.0.12...v10.0.13) (2026-09-30)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * read the advertised SASL methods without backtracking regexes ([b5a896f](https://github.com/nodemailer/nodemailer/commit/b5a896fcb02df1628329d7cd665ca08490b14b56))
9
+ * strip comments inside an angle-addr before it becomes the address ([a502247](https://github.com/nodemailer/nodemailer/commit/a502247555fd81773eac337486781f3b6008e16a))
10
+
11
+ ## [10.0.12](https://github.com/nodemailer/nodemailer/compare/v10.0.11...v10.0.12) (2026-09-28)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * settle every send on a connection error, back off pool requeues, turn a bare CR into CRLF, bound fetch, honour requireTLS ([63ccd66](https://github.com/nodemailer/nodemailer/commit/63ccd66c894bbaac18c82084ee385bf6488226cf))
17
+
3
18
  ## [10.0.11](https://github.com/nodemailer/nodemailer/compare/v10.0.10...v10.0.11) (2026-09-27)
4
19
 
5
20
 
@@ -242,6 +242,81 @@ function _recoverAddrSpec(data) {
242
242
  .filter(part => part)
243
243
  .join(' ');
244
244
  }
245
+ /**
246
+ * Takes the RFC 5322 comments out of the contents of an angle-addr.
247
+ *
248
+ * The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
249
+ * a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
250
+ * put 'user@example.com(x)evil.com' into the envelope, a value that is no mailbox and that a
251
+ * receiver stripping the comment reads as 'user@example.comevil.com' (GHSA-g73g-hqqh-jr95).
252
+ * A comment is folding whitespace, so it is read as a space that splits two atoms and as
253
+ * nothing next to an '@', the same rule the token walk applies outside the brackets. Quoted
254
+ * strings and domain literals are copied through untouched, a '(' in there is not a comment.
255
+ *
256
+ * @param address Contents of the angle brackets
257
+ * @return The address with the comments removed, and the text of the comments
258
+ */
259
+ function _stripAddressComments(address) {
260
+ const comments = [];
261
+ let result = '';
262
+ let comment = '';
263
+ let depth = 0;
264
+ let closer = '';
265
+ // carried along rather than read back off the growing result, which would flatten it on
266
+ // every comment (see lastChars in _handleAddress)
267
+ let lastChar = '';
268
+ for (let i = 0, len = address.length; i < len; i++) {
269
+ const chr = address.charAt(i);
270
+ if (depth) {
271
+ if (chr === '\\' && i < len - 1) {
272
+ comment += address.charAt(++i);
273
+ }
274
+ else if (chr === '(') {
275
+ depth++;
276
+ comment += chr;
277
+ }
278
+ else if (chr === ')' && !--depth) {
279
+ comments.push(comment.trim());
280
+ comment = '';
281
+ if (lastChar !== '@' && address.charAt(i + 1) !== '@') {
282
+ result += ' ';
283
+ lastChar = ' ';
284
+ }
285
+ }
286
+ else {
287
+ comment += chr;
288
+ }
289
+ continue;
290
+ }
291
+ if (closer) {
292
+ if (chr === '\\' && closer === '"' && i < len - 1) {
293
+ result += chr + address.charAt(++i);
294
+ lastChar = address.charAt(i);
295
+ continue;
296
+ }
297
+ if (chr === closer) {
298
+ closer = '';
299
+ }
300
+ }
301
+ else if (chr === '"') {
302
+ closer = '"';
303
+ }
304
+ else if (chr === '[') {
305
+ closer = ']';
306
+ }
307
+ else if (chr === '(') {
308
+ depth = 1;
309
+ continue;
310
+ }
311
+ result += chr;
312
+ lastChar = chr;
313
+ }
314
+ if (depth) {
315
+ // an unterminated comment runs to the end of the address
316
+ comments.push(comment.trim());
317
+ }
318
+ return { address: result.trim(), comments: comments.filter(text => text) };
319
+ }
245
320
  /**
246
321
  * Converts tokens for a single address into an address object
247
322
  *
@@ -369,6 +444,24 @@ function _handleAddress(tokens, depth) {
369
444
  });
370
445
  }
371
446
  else {
447
+ // Comments come out of the angle-addr before anything asks whether one was found, so
448
+ // that brackets holding nothing but a comment read the same as empty ones
449
+ const addressComments = [];
450
+ const addressParts = [];
451
+ for (const part of data.address) {
452
+ if (part.indexOf('(') < 0) {
453
+ addressParts.push(part);
454
+ continue;
455
+ }
456
+ const stripped = _stripAddressComments(part);
457
+ for (const comment of stripped.comments) {
458
+ addressComments.push(comment);
459
+ }
460
+ if (stripped.address) {
461
+ addressParts.push(stripped.address);
462
+ }
463
+ }
464
+ data.address = addressParts;
372
465
  // If no address was found, try to detect one from regular text
373
466
  if (!data.address.length && data.text.length) {
374
467
  for (let i = data.text.length - 1; i >= 0; i--) {
@@ -435,6 +528,10 @@ function _handleAddress(tokens, depth) {
435
528
  data.text = '';
436
529
  }
437
530
  _recoverAddrSpec(data);
531
+ // a comment names the mailbox only when nothing else does, as one outside the brackets
532
+ if (!data.text && addressComments.length) {
533
+ data.text = addressComments.join(' ');
534
+ }
438
535
  const address = {
439
536
  address: data.address || data.text || '',
440
537
  name: data.text || data.address || ''
@@ -90,13 +90,22 @@ class DKIMSigner {
90
90
  return;
91
91
  }
92
92
  const key = this.keys[keyPos++];
93
- const dkimField = (0, sign_js_1.default)(this.headers, this.hashAlgo, this.bodyHash, {
94
- domainName: key.domainName,
95
- keySelector: key.keySelector,
96
- privateKey: key.privateKey,
97
- headerFieldNames: this.options.headerFieldNames,
98
- skipFields: this.options.skipFields
99
- });
93
+ let dkimField;
94
+ try {
95
+ dkimField = (0, sign_js_1.default)(this.headers, this.hashAlgo, this.bodyHash, {
96
+ domainName: key.domainName,
97
+ keySelector: key.keySelector,
98
+ privateKey: key.privateKey,
99
+ headerFieldNames: this.options.headerFieldNames,
100
+ skipFields: this.options.skipFields
101
+ });
102
+ }
103
+ catch (err) {
104
+ this.hasErrored = true;
105
+ this.cleanup();
106
+ this.output.emit('error', err);
107
+ return;
108
+ }
100
109
  if (dkimField) {
101
110
  this.output.write(Buffer.from(dkimField + '\r\n'));
102
111
  }
@@ -196,7 +205,15 @@ class DKIM {
196
205
  }
197
206
  const signer = new DKIMSigner(options, this.keys, inputStream, output);
198
207
  setImmediate(() => {
199
- signer.signStream();
208
+ try {
209
+ signer.signStream();
210
+ }
211
+ catch (_E) {
212
+ // the body hash is created here, an unknown hashAlgo throws inside this timer
213
+ // where nothing else could catch it
214
+ output.emit('error', sign_js_1.default.unsupportedHashAlgoError(signer.hashAlgo));
215
+ return;
216
+ }
200
217
  if (writeValue) {
201
218
  setImmediate(() => {
202
219
  inputStream.end(writeValue);
@@ -1,4 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
+ import type { NodemailerError } from '../errors.js';
2
3
  import type { MessageParserHeaderLine } from './message-parser.js';
3
4
  /**
4
5
  * Private key accepted by crypto.Sign#sign: a PEM string, a Buffer, a KeyObject or an
@@ -45,5 +46,6 @@ export interface DKIMRelaxedHeaders {
45
46
  declare function sign(headers: MessageParserHeaderLine[], hashAlgo: string, bodyHash: string, options?: DKIMSignOptions): string | false;
46
47
  declare namespace sign {
47
48
  var relaxedHeaders: (headers: MessageParserHeaderLine[], fieldNames?: string, skipFields?: string) => DKIMRelaxedHeaders;
49
+ var unsupportedHashAlgoError: (hashAlgo: string) => NodemailerError;
48
50
  }
49
51
  export default sign;
@@ -39,6 +39,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const punycode = __importStar(require("../punycode/index.js"));
40
40
  const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
41
41
  const node_crypto_1 = __importDefault(require("node:crypto"));
42
+ const errors = __importStar(require("../errors.js"));
43
+ /**
44
+ * Error for a hashAlgo value the crypto module does not know
45
+ */
46
+ function unsupportedHashAlgoError(hashAlgo) {
47
+ const err = new Error('Unsupported DKIM hash algorithm "' + hashAlgo + '"');
48
+ err.code = errors.ECONFIG;
49
+ return err;
50
+ }
42
51
  /**
43
52
  * Returns DKIM signature header line
44
53
  *
@@ -63,7 +72,13 @@ function sign(headers, hashAlgo, bodyHash, options) {
63
72
  const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
64
73
  const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
65
74
  canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
66
- const signer = node_crypto_1.default.createSign(('rsa-' + hashAlgo).toUpperCase());
75
+ let signer;
76
+ try {
77
+ signer = node_crypto_1.default.createSign(('rsa-' + hashAlgo).toUpperCase());
78
+ }
79
+ catch (_E) {
80
+ throw unsupportedHashAlgoError(hashAlgo);
81
+ }
67
82
  // the header lines are 'binary' strings, so this reproduces the original header bytes
68
83
  signer.update(canonicalizedHeaderData.headers, 'latin1');
69
84
  let signature;
@@ -76,6 +91,7 @@ function sign(headers, hashAlgo, bodyHash, options) {
76
91
  return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
77
92
  }
78
93
  sign.relaxedHeaders = relaxedHeaders;
94
+ sign.unsupportedHashAlgoError = unsupportedHashAlgoError;
79
95
  exports.default = sign;
80
96
  function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
81
97
  // the caller supplied tag values are interpolated straight into the tag list, and none of
@@ -25,8 +25,10 @@ export interface FetchOptions {
25
25
  tls?: {
26
26
  [key: string]: any;
27
27
  } | undefined;
28
- /** Request timeout in milliseconds */
28
+ /** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
29
29
  timeout?: number | undefined;
30
+ /** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
31
+ maxBytes?: number | undefined;
30
32
  /** Maximum number of redirects to follow (default 5) */
31
33
  maxRedirects?: number | undefined;
32
34
  /** Resolve responses with a status code of 300 or above instead of emitting an error */
@@ -47,6 +49,7 @@ export interface FetchResponse extends PassThrough {
47
49
  declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
48
50
  declare namespace nmfetch {
49
51
  var Cookies: typeof import("./cookies.js").default;
52
+ var DEFAULT_TIMEOUT: number;
50
53
  }
51
54
  type CookiesJar = Cookies;
52
55
  /**
@@ -47,6 +47,10 @@ const node_net_1 = __importDefault(require("node:net"));
47
47
  const errors = __importStar(require("../errors.js"));
48
48
  const objects_js_1 = require("../shared/objects.js");
49
49
  const MAX_REDIRECTS = 5;
50
+ // a stalled server would otherwise hold the request, and whatever waits on it, open forever
51
+ const DEFAULT_TIMEOUT = 60 * 1000;
52
+ // the body is usually buffered in memory by the caller, so an unbounded download is refused
53
+ const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
50
54
  // Only genuine TLS settings are taken from options.tls. That object reaches us straight
51
55
  // from a user supplied attachment (content.tls), so keys like host, port, path, socketPath
52
56
  // or lookup would otherwise repoint the request at a destination that never went through
@@ -252,28 +256,22 @@ function nmfetch(url, options) {
252
256
  });
253
257
  return fetchRes;
254
258
  }
255
- if (options.timeout) {
256
- req.setTimeout(options.timeout, () => {
257
- if (finished) {
258
- return;
259
- }
260
- finished = true;
261
- req.abort();
262
- const err = new Error('Request Timeout');
263
- err.code = errors.EFETCH;
264
- err.sourceUrl = url;
265
- fetchRes.emit('error', err);
266
- });
267
- }
268
- req.on('error', (err) => {
259
+ // reports the first failure of this request on fetchRes and releases the request
260
+ const fail = (err, sourceUrl = url) => {
269
261
  if (finished) {
270
262
  return;
271
263
  }
272
264
  finished = true;
273
265
  err.code = errors.EFETCH;
274
- err.sourceUrl = url;
266
+ err.sourceUrl = sourceUrl;
275
267
  fetchRes.emit('error', err);
276
- });
268
+ req.abort();
269
+ };
270
+ const timeout = typeof options.timeout === 'number' && options.timeout >= 0 ? options.timeout : DEFAULT_TIMEOUT;
271
+ if (timeout) {
272
+ req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
273
+ }
274
+ req.on('error', (err) => fail(err));
277
275
  req.on('response', res => {
278
276
  let inflate;
279
277
  if (finished) {
@@ -294,13 +292,7 @@ function nmfetch(url, options) {
294
292
  // redirect
295
293
  options.redirects++;
296
294
  if (options.redirects > options.maxRedirects) {
297
- finished = true;
298
- const err = new Error('Maximum redirect count exceeded');
299
- err.code = errors.EFETCH;
300
- err.sourceUrl = url;
301
- fetchRes.emit('error', err);
302
- req.abort();
303
- return;
295
+ return fail(new Error('Maximum redirect count exceeded'));
304
296
  }
305
297
  // redirect does not include POST body
306
298
  options.method = 'GET';
@@ -320,13 +312,7 @@ function nmfetch(url, options) {
320
312
  // call: that call gets its own `finished` flag and no handle on this
321
313
  // request, so this one would stay open and could emit a second error on
322
314
  // the shared fetchRes once it times out. Callers listen with req.once().
323
- finished = true;
324
- const err = new Error('Unsupported protocol for URL ' + redirectUrl);
325
- err.code = errors.EFETCH;
326
- err.sourceUrl = redirectUrl;
327
- fetchRes.emit('error', err);
328
- req.abort();
329
- return;
315
+ return fail(new Error('Unsupported protocol for URL ' + redirectUrl), redirectUrl);
330
316
  }
331
317
  // Do not forward credentials when the redirect leaves the original
332
318
  // security context: a different host, or a downgrade from https to
@@ -343,41 +329,33 @@ function nmfetch(url, options) {
343
329
  }
344
330
  });
345
331
  }
332
+ // this request is done with, release it so its socket and timeout do not
333
+ // outlive it and report a late error on the shared fetchRes
334
+ finished = true;
335
+ res.resume();
336
+ req.abort();
346
337
  return nmfetch(redirectUrl, options);
347
338
  }
348
339
  fetchRes.statusCode = res.statusCode;
349
340
  fetchRes.headers = res.headers;
350
341
  if (res.statusCode >= 300 && !options.allowErrorResponse) {
351
- finished = true;
352
- const err = new Error('Invalid status code ' + res.statusCode);
353
- err.code = errors.EFETCH;
354
- err.sourceUrl = url;
355
- fetchRes.emit('error', err);
356
- req.abort();
357
- return;
342
+ return fail(new Error('Invalid status code ' + res.statusCode));
358
343
  }
359
- res.on('error', (err) => {
360
- if (finished) {
344
+ res.on('error', (err) => fail(err));
345
+ const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
346
+ const source = inflate || res;
347
+ let received = 0;
348
+ source.on('data', (chunk) => {
349
+ received += chunk.length;
350
+ if (received <= maxBytes || finished) {
361
351
  return;
362
352
  }
363
- finished = true;
364
- err.code = errors.EFETCH;
365
- err.sourceUrl = url;
366
- fetchRes.emit('error', err);
367
- req.abort();
353
+ source.unpipe(fetchRes);
354
+ fail(new Error('Response size exceeds the allowed ' + maxBytes + ' bytes'));
368
355
  });
369
356
  if (inflate) {
370
357
  res.pipe(inflate).pipe(fetchRes);
371
- inflate.on('error', (err) => {
372
- if (finished) {
373
- return;
374
- }
375
- finished = true;
376
- err.code = errors.EFETCH;
377
- err.sourceUrl = url;
378
- fetchRes.emit('error', err);
379
- req.abort();
380
- });
358
+ inflate.on('error', (err) => fail(err));
381
359
  }
382
360
  else {
383
361
  res.pipe(fetchRes);
@@ -392,11 +370,7 @@ function nmfetch(url, options) {
392
370
  req.write(body);
393
371
  }
394
372
  catch (err) {
395
- finished = true;
396
- err.code = errors.EFETCH;
397
- err.sourceUrl = url;
398
- fetchRes.emit('error', err);
399
- return;
373
+ return fail(err);
400
374
  }
401
375
  }
402
376
  req.end();
@@ -404,6 +378,7 @@ function nmfetch(url, options) {
404
378
  return fetchRes;
405
379
  }
406
380
  nmfetch.Cookies = cookies_js_1.default;
381
+ nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
407
382
  exports.default = nmfetch;
408
383
  module.exports = exports.default;
409
384
  Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.11";
2
+ export declare const version = "10.0.13";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = 'nodemailer';
6
- exports.version = '10.0.11';
6
+ exports.version = '10.0.13';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -1,7 +1,8 @@
1
1
  import { Transform, type TransformOptions } from 'node:stream';
2
2
  /**
3
3
  * Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
4
- * Also makes sure that only <CR><LF> sequences are used for linebreaks
4
+ * Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
5
+ * are both turned into <CR><LF>
5
6
  *
6
7
  * @param options Stream options
7
8
  */
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const node_stream_1 = require("node:stream");
4
+ // bytes inserted into the output, shared as they are only ever copied by Buffer.concat
5
+ const INSERT_LF = Buffer.from('\n');
6
+ const INSERT_LF_DOT = Buffer.from('\n.');
7
+ const INSERT_CR = Buffer.from('\r');
8
+ const INSERT_DOT = Buffer.from('.');
4
9
  /**
5
10
  * Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
6
- * Also makes sure that only <CR><LF> sequences are used for linebreaks
11
+ * Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
12
+ * are both turned into <CR><LF>
7
13
  *
8
14
  * @param options Stream options
9
15
  */
@@ -32,33 +38,35 @@ class DataStream extends node_stream_1.Transform {
32
38
  }
33
39
  this.inByteCount += chunk.length;
34
40
  for (i = 0, len = chunk.length; i < len; i++) {
35
- if (chunk[i] === 0x2e) {
36
- // .
37
- if ((i && chunk[i - 1] === 0x0a) || (!i && (!this.lastByte || this.lastByte === 0x0a))) {
38
- buf = chunk.slice(lastPos, i + 1);
39
- chunks.push(buf);
40
- chunks.push(Buffer.from('.'));
41
- chunklen += buf.length + 1;
42
- lastPos = i + 1;
43
- }
41
+ const byte = chunk[i];
42
+ const prev = i ? chunk[i - 1] : this.lastByte;
43
+ let insert = false;
44
+ if (prev === 0x0d && byte !== 0x0a) {
45
+ // a bare CR becomes CRLF. A receiver that treats a lone CR as a line end would
46
+ // otherwise see "\r.\r" as the end of the data (SMTP smuggling), so a dot
47
+ // following it is stuffed like at the start of any other line
48
+ insert = byte === 0x2e ? INSERT_LF_DOT : INSERT_LF;
49
+ }
50
+ else if (byte === 0x0a && prev !== 0x0d) {
51
+ // a bare LF becomes CRLF
52
+ insert = INSERT_CR;
44
53
  }
45
- else if (chunk[i] === 0x0a) {
46
- // \n
47
- if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
48
- if (i > lastPos) {
49
- buf = chunk.slice(lastPos, i);
50
- chunks.push(buf);
51
- chunklen += buf.length + 2;
52
- }
53
- else {
54
- chunklen += 2;
55
- }
56
- chunks.push(Buffer.from('\r\n'));
57
- lastPos = i + 1;
54
+ else if (byte === 0x2e && (prev === 0x0a || prev === false)) {
55
+ // a dot at the start of a line
56
+ insert = INSERT_DOT;
57
+ }
58
+ if (insert) {
59
+ if (i > lastPos) {
60
+ buf = chunk.slice(lastPos, i);
61
+ chunks.push(buf);
62
+ chunklen += buf.length;
58
63
  }
64
+ chunks.push(insert);
65
+ chunklen += insert.length;
66
+ lastPos = i;
59
67
  }
60
68
  }
61
- if (chunklen) {
69
+ if (chunks.length) {
62
70
  // add last piece
63
71
  if (lastPos < chunk.length) {
64
72
  buf = chunk.slice(lastPos);
@@ -26,11 +26,11 @@ export interface SMTPConnectionOptions {
26
26
  secured?: boolean | undefined;
27
27
  /** Server name for SNI, defaults to host when that is not an IP address */
28
28
  servername?: string | undefined;
29
- /** Ignore STARTTLS even when the server advertises it */
29
+ /** Ignore STARTTLS even when the server advertises it, has no effect when requireTLS is set */
30
30
  ignoreTLS?: boolean | undefined;
31
- /** Force STARTTLS, fail when the server does not support it */
31
+ /** Force STARTTLS, fail when the server does not support it. Takes precedence over ignoreTLS and opportunisticTLS */
32
32
  requireTLS?: boolean | undefined;
33
- /** Continue unencrypted when the STARTTLS upgrade fails */
33
+ /** Continue unencrypted when the STARTTLS upgrade fails, has no effect when requireTLS is set */
34
34
  opportunisticTLS?: boolean | undefined;
35
35
  /** Name of the client server, sent with EHLO/HELO, CRLF is stripped */
36
36
  name?: string | undefined;
@@ -288,8 +288,8 @@ export type SMTPConnectionResponseAction = (str: string) => void;
288
288
  * * **port** - is the port to connect to (defaults to 587 or 465)
289
289
  * * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
290
290
  * * **secure** - use SSL
291
- * * **ignoreTLS** - ignore server support for STARTTLS
292
- * * **requireTLS** - forces the client to use STARTTLS
291
+ * * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
292
+ * * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
293
293
  * * **name** - the name of the client server
294
294
  * * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
295
295
  * * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)