nodemailer 10.0.13 → 10.0.14

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 (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/addressparser/index.d.ts +0 -1
  3. package/dist/cjs/addressparser/index.js +55 -17
  4. package/dist/cjs/dkim/message-parser.js +28 -4
  5. package/dist/cjs/dkim/sign.d.ts +1 -0
  6. package/dist/cjs/dkim/sign.js +1 -0
  7. package/dist/cjs/mailer/index.d.ts +7 -1
  8. package/dist/cjs/mime-funcs/index.d.ts +2 -1
  9. package/dist/cjs/mime-funcs/index.js +197 -83
  10. package/dist/cjs/mime-node/index.js +2 -1
  11. package/dist/cjs/nodemailer.d.ts +13 -2
  12. package/dist/cjs/package-info.d.ts +1 -1
  13. package/dist/cjs/package-info.js +1 -1
  14. package/dist/cjs/punycode/index.js +1 -0
  15. package/dist/cjs/ses-transport/index.d.ts +6 -1
  16. package/dist/cjs/shared/index.d.ts +25 -1
  17. package/dist/cjs/shared/index.js +1 -0
  18. package/dist/cjs/smtp-connection/http-proxy-client.d.ts +11 -3
  19. package/dist/cjs/smtp-connection/http-proxy-client.js +17 -7
  20. package/dist/cjs/smtp-connection/index.d.ts +1 -1
  21. package/dist/cjs/smtp-connection/index.js +15 -1
  22. package/dist/cjs/smtp-pool/index.d.ts +6 -1
  23. package/dist/cjs/smtp-transport/index.d.ts +6 -1
  24. package/dist/cjs/well-known/services.js +6 -0
  25. package/dist/esm/addressparser/index.d.ts +0 -1
  26. package/dist/esm/addressparser/index.js +55 -17
  27. package/dist/esm/dkim/message-parser.js +28 -4
  28. package/dist/esm/dkim/sign.d.ts +1 -0
  29. package/dist/esm/dkim/sign.js +1 -0
  30. package/dist/esm/mailer/index.d.ts +7 -1
  31. package/dist/esm/mime-funcs/index.d.ts +2 -1
  32. package/dist/esm/mime-funcs/index.js +197 -83
  33. package/dist/esm/mime-node/index.js +2 -1
  34. package/dist/esm/nodemailer.d.ts +13 -2
  35. package/dist/esm/package-info.d.ts +1 -1
  36. package/dist/esm/package-info.js +1 -1
  37. package/dist/esm/punycode/index.js +1 -0
  38. package/dist/esm/ses-transport/index.d.ts +6 -1
  39. package/dist/esm/shared/index.d.ts +25 -1
  40. package/dist/esm/shared/index.js +1 -0
  41. package/dist/esm/smtp-connection/http-proxy-client.d.ts +11 -3
  42. package/dist/esm/smtp-connection/http-proxy-client.js +17 -7
  43. package/dist/esm/smtp-connection/index.d.ts +1 -1
  44. package/dist/esm/smtp-connection/index.js +15 -1
  45. package/dist/esm/smtp-pool/index.d.ts +6 -1
  46. package/dist/esm/smtp-transport/index.d.ts +6 -1
  47. package/dist/esm/well-known/services.js +6 -0
  48. package/dist/well-known/services.json +7 -0
  49. package/package.json +5 -5
@@ -39,6 +39,7 @@ export function quoteString(value) {
39
39
  * no need to encode the values in any way. If the value is plaintext but has
40
40
  * longer lines then allowed, then use format=flowed
41
41
  *
42
+ * @param str Multi line string to check
42
43
  * @param lineLength Max line length to check for
43
44
  * @returns Returns true if there is at least one line longer than lineLength chars
44
45
  */
@@ -185,7 +186,10 @@ export function buildHeaderValue(structured) {
185
186
  }
186
187
  });
187
188
  }
188
- else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
189
+ else if (!value.length || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
190
+ // a parameter value is a token or a quoted-string and a token is never empty, so
191
+ // a valueless parameter such as the 'flag' of 'multipart/mixed; flag; boundary=b'
192
+ // goes out as 'flag=""' rather than as the 'flag=' that parses as neither
189
193
  paramsArray.push(param + '=' + JSON.stringify(value));
190
194
  }
191
195
  else {
@@ -207,9 +211,9 @@ export function buildHeaderValue(structured) {
207
211
  * title*0*=utf-8''unicode
208
212
  * title*1*=%20string
209
213
  *
214
+ * @param key Parameter name the generated keys are built from, for example title
210
215
  * @param data String to be encoded
211
216
  * @param [maxLength=50] Max length for generated chunks
212
- * @param [fromCharset='UTF-8'] Source sharacter set
213
217
  * @return A list of encoded keys and headers
214
218
  */
215
219
  export function buildHeaderParam(key, data, maxLength) {
@@ -336,6 +340,26 @@ export function buildHeaderParam(key, data, maxLength) {
336
340
  value: item.line
337
341
  }));
338
342
  }
343
+ /**
344
+ * An RFC 2045 token: printable ASCII without SPACE, the control characters, DEL and the
345
+ * tspecials. A charset name is one, and the name a continuation carries is written into
346
+ * the encoded word the parameter value becomes, so it is checked against this before it
347
+ * goes in. Whitespace used to come off it only because the value was trimmed first.
348
+ */
349
+ const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/;
350
+ /**
351
+ * Whether a string can be a header parameter name.
352
+ *
353
+ * A parameter name is a token, so it is never empty. A "__proto__" name would target the
354
+ * prototype chain of the params object instead of an own property of it and read back as
355
+ * Object.prototype, so it is no name either.
356
+ *
357
+ * @param name Candidate parameter name, already lowercased
358
+ * @return true when the name can be used
359
+ */
360
+ function _isParamName(name) {
361
+ return !!name && !isProtoKey(name);
362
+ }
339
363
  /**
340
364
  * Parses a header value with key=value arguments into a structured
341
365
  * object.
@@ -356,134 +380,224 @@ export function parseHeaderValue(str) {
356
380
  value: '',
357
381
  params: {}
358
382
  };
359
- // Parameter names come from a caller supplied contentType/contentDisposition. A
360
- // "__proto__" name would target the prototype chain of the params object instead of
361
- // an own property of it, and read back as Object.prototype, so it is dropped.
383
+ // A duplicated parameter resolves to its first occurrence, the way a duplicated header
384
+ // does. Letting the last one win disagrees with the receivers that take the first, and
385
+ // the two readings of 'boundary="b"; boundary="c"' name different delimiters. The
386
+ // continuation join below is the only writer of the name it builds, so it tests the
387
+ // name with _isParamName directly rather than taking that rule along from here.
362
388
  const setParam = (name, value) => {
363
- if (!isProtoKey(name)) {
389
+ name = name.toLowerCase();
390
+ if (_isParamName(name) && !Object.prototype.hasOwnProperty.call(response.params, name)) {
364
391
  response.params[name] = value;
365
392
  }
366
393
  };
367
394
  let key = false;
368
395
  let value = '';
369
- let type = 'value';
396
+ let stage = 'value';
370
397
  let quote = false;
371
398
  let escaped = false;
372
399
  let chr;
400
+ // Whitespace seen outside a quoted string is held back until a significant character
401
+ // follows it, so the whitespace around a value is dropped without trimming spaces the
402
+ // sender quoted on purpose. Trimming the stored value instead loses the trailing space
403
+ // of 'filename*0="Annual Report "', which the next continuation section is appended to.
404
+ let pendingSpace = '';
405
+ let quoteClosed = false;
406
+ // Whitespace ahead of the first character of a value is padding and is dropped, the
407
+ // whitespace between two characters of it is content
408
+ const flushSpace = () => {
409
+ if (value.length) {
410
+ value += pendingSpace;
411
+ }
412
+ pendingSpace = '';
413
+ };
414
+ const addChr = (c) => {
415
+ flushSpace();
416
+ value += c;
417
+ };
418
+ const takeValue = () => {
419
+ const taken = value;
420
+ value = '';
421
+ pendingSpace = '';
422
+ quoteClosed = false;
423
+ return taken;
424
+ };
425
+ const storeValue = () => {
426
+ const taken = takeValue();
427
+ if (key === false) {
428
+ response.value = taken;
429
+ }
430
+ else {
431
+ setParam(key, taken);
432
+ }
433
+ };
434
+ // A parameter name with no '=' is a valueless parameter, not the start of the next one.
435
+ // Without this the name keeps growing across the ';' and swallows whatever follows, which
436
+ // is how 'multipart/mixed; flag; boundary="AAA"' lost its boundary to a parameter named
437
+ // 'flag; boundary' and left the node declaring the generated boundary beside the asked
438
+ // for one, so a receiver reading the first of the two found no delimiter it matched.
439
+ const storeEmptyKey = () => {
440
+ setParam(takeValue().trim(), '');
441
+ };
373
442
  for (let i = 0, len = str.length; i < len; i++) {
374
443
  chr = str.charAt(i);
375
- if (type === 'key') {
444
+ if (stage === 'key') {
376
445
  if (chr === '=') {
377
- key = value.trim().toLowerCase();
378
- type = 'value';
379
- value = '';
446
+ key = takeValue().trim();
447
+ stage = 'value';
448
+ continue;
449
+ }
450
+ if (chr === ';') {
451
+ storeEmptyKey();
380
452
  continue;
381
453
  }
382
454
  value += chr;
383
455
  }
384
456
  else {
457
+ if (quoteClosed && chr !== ';') {
458
+ // Nothing behind a closed quoted string reaches the value. RFC 2045 says a
459
+ // parameter value is a token or a quoted string and not both, so what follows
460
+ // one is junk and only the ';' that ends the parameter still counts. Tested
461
+ // ahead of the branches rather than beside the append at the foot of them,
462
+ // where each branch above was a way around it: a second '"' reopened quoting
463
+ // and swallowed the rest of the header, so 'boundary="AAA" "; boundary=BBB"'
464
+ // read as a single boundary of 'AAA ', and the junk of
465
+ // 'boundary="AAA" (unterminated comment' still joined the declared boundary
466
+ // through the escape branch. quoteClosed is only ever set while no quote is
467
+ // open, and this is what keeps one from being opened afterwards.
468
+ escaped = false;
469
+ continue;
470
+ }
385
471
  if (escaped) {
386
- value += chr;
472
+ addChr(chr);
387
473
  }
388
- else if (chr === '\\') {
474
+ else if (quote && chr === '\\') {
475
+ // a backslash only escapes inside a quoted string, everywhere else it is an
476
+ // ordinary character. Treating it as an escape turns the parameter value
477
+ // 'C:\Users\me\report.txt' into 'C:Usersmereport.txt'
389
478
  escaped = true;
390
479
  continue;
391
480
  }
392
481
  else if (quote && chr === quote) {
393
482
  quote = false;
483
+ quoteClosed = true;
394
484
  }
395
485
  else if (!quote && chr === '"') {
396
486
  quote = chr;
487
+ flushSpace();
397
488
  }
398
489
  else if (!quote && chr === ';') {
399
- if (key === false) {
400
- response.value = value.trim();
401
- }
402
- else {
403
- setParam(key, value.trim());
404
- }
405
- type = 'key';
406
- value = '';
490
+ storeValue();
491
+ stage = 'key';
492
+ }
493
+ else if (!quote && (chr === ' ' || chr === '\t')) {
494
+ pendingSpace += chr;
407
495
  }
408
496
  else {
409
- value += chr;
497
+ addChr(chr);
410
498
  }
411
499
  escaped = false;
412
500
  }
413
501
  }
414
- if (type === 'value') {
415
- if (key === false) {
416
- response.value = value.trim();
417
- }
418
- else {
419
- setParam(key, value.trim());
420
- }
502
+ if (stage === 'value') {
503
+ storeValue();
421
504
  }
422
- else if (value.trim()) {
423
- setParam(value.trim().toLowerCase(), '');
505
+ else {
506
+ // a key with no value, as in 'Header-Key: somevalue; key=value; emptykey'
507
+ storeEmptyKey();
424
508
  }
425
509
  // handle parameter value continuations
426
510
  // https://tools.ietf.org/html/rfc2231#section-3
427
- // preprocess values
511
+ // Sections are collected in a list and ordered below rather than written into an array
512
+ // at their own section number. An index write makes the array as long as the number the
513
+ // header asked for, and the join that follows walks all of it, so the 55 byte value
514
+ // "attachment; filename*0*=utf-8''a; filename*4000000000=b" held a core for over two
515
+ // minutes.
516
+ const continuations = new Map();
428
517
  Object.keys(response.params).forEach(key => {
429
- let actualKey, nr, match, value;
430
- if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) {
431
- actualKey = key.substr(0, match.index);
432
- nr = Number(match[2] || match[3]) || 0;
433
- if (isProtoKey(actualKey)) {
434
- // see setParam. Reading it back would yield Object.prototype, which is
435
- // an object, so the initializer below would be skipped and the write
436
- // that follows would throw out of a header build the caller can not catch
437
- delete response.params[key];
438
- return;
439
- }
440
- if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
441
- response.params[actualKey] = {
442
- charset: false,
443
- values: []
444
- };
445
- }
446
- value = response.params[key];
447
- if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
448
- response.params[actualKey].charset = match[1] || 'iso-8859-1';
449
- value = match[2];
450
- }
451
- response.params[actualKey].values[nr] = value;
452
- // remove the old reference
453
- delete response.params[key];
518
+ const match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/);
519
+ if (!match) {
520
+ // not a continuation parameter, there is nothing to join
521
+ return;
522
+ }
523
+ const actualKey = key.substr(0, match.index);
524
+ const nr = Number(match[2] || match[3]) || 0;
525
+ // RFC 2231 section 4.1: only a section whose name ends in '*' is percent encoded
526
+ const encoded = match[0].substr(-1) === '*';
527
+ // remove the old reference
528
+ let value = response.params[key];
529
+ delete response.params[key];
530
+ if (!_isParamName(actualKey)) {
531
+ // the joined value can not be written back under this name. It is empty when the
532
+ // continuation suffix was all there was of it, as in the bare '*' of 'text/plain; a;*'
533
+ return;
534
+ }
535
+ let continuation = continuations.get(actualKey);
536
+ if (!continuation) {
537
+ continuation = { charset: false, sections: [] };
538
+ continuations.set(actualKey, continuation);
539
+ }
540
+ const charsetMatch = nr === 0 && encoded ? value.match(/^([^']*)'[^']*'(.*)$/) : null;
541
+ if (charsetMatch) {
542
+ // the charset is a token, and anything else named as one is no charset a consumer
543
+ // could resolve, so it reads as the unnamed case rather than being carried into
544
+ // the encoded word below. A "\r\n" of a prefix would otherwise reach a consumer
545
+ // of the parsed value as the charset of a word that no decoder can act on
546
+ continuation.charset = TOKEN.test(charsetMatch[1]) ? charsetMatch[1] : 'iso-8859-1';
547
+ value = charsetMatch[2];
454
548
  }
549
+ continuation.sections.push({ nr, value, encoded });
455
550
  });
456
551
  // concatenate split rfc2231 strings and convert encoded strings to mime encoded words
457
- Object.keys(response.params).forEach(key => {
458
- let value;
459
- if (response.params[key] && Array.isArray(response.params[key].values)) {
460
- value = response.params[key].values.map((val) => val || '').join('');
461
- if (response.params[key].charset) {
462
- // convert "%AB" to "=?charset?Q?=AB?="
463
- response.params[key] =
464
- '=?' +
465
- response.params[key].charset +
466
- '?Q?' +
467
- value
468
- // fix invalidly encoded chars
469
- .replace(/[=?_\s]/g, s => {
470
- const c = s.charCodeAt(0).toString(16);
471
- if (s === ' ') {
472
- return '_';
473
- }
474
- return '%' + (c.length < 2 ? '0' : '') + c;
475
- })
476
- // change from urlencoding to percent encoding
477
- .replace(/%/g, '=') +
478
- '?=';
479
- }
480
- else {
481
- response.params[key] = value;
482
- }
552
+ continuations.forEach((continuation, key) => {
553
+ if (Object.prototype.hasOwnProperty.call(response.params, key)) {
554
+ // The same name was also given as a plain parameter, which the starred keys were
555
+ // just deleted from around, so this write would be the only one in the function
556
+ // to override a name already taken. 'filename=plain.txt; filename*0=evil.txt'
557
+ // resolves to the plain parameter either way round, so the reading does not come
558
+ // down to which of the two spellings the sender put first
559
+ return;
560
+ }
561
+ continuation.sections.sort((a, b) => a.nr - b.nr);
562
+ if (!continuation.charset) {
563
+ // nothing said which charset the percent escapes of an encoded section are in,
564
+ // so every section is passed on as the text it already is
565
+ response.params[key] = continuation.sections.map(section => section.value).join('');
566
+ return;
483
567
  }
568
+ // convert "%AB" to "=?charset?Q?=AB?="
569
+ response.params[key] = '=?' + continuation.charset + '?Q?' + continuation.sections.map(_encodeContinuationSection).join('') + '?=';
484
570
  });
485
571
  return response;
486
572
  }
573
+ /**
574
+ * Renders one parameter value continuation section as the payload of a Q encoded word.
575
+ *
576
+ * A section whose name ends in '*' is percent encoded and its escapes carry the bytes of
577
+ * the value, so they only have to be rewritten into the "=AB" spelling a Q encoded word
578
+ * uses. A section without the '*' is literal text (RFC 2231 section 4.1), so its '%' is a
579
+ * '%' and is escaped along with the characters a Q encoded word can not carry bare.
580
+ * Decoding a literal section invents bytes that never appeared on the wire: it is how the
581
+ * value 'filename*0*=utf-8''safe; filename*1=%2F..%2F..%2Fetc%2Fpasswd' was emitted as a
582
+ * filename every receiving client reads back as 'safe/../../etc/passwd'.
583
+ *
584
+ * @param section One collected continuation section
585
+ * @return The section as Q encoded word payload
586
+ */
587
+ function _encodeContinuationSection(section) {
588
+ const specials = section.encoded ? /[=?_\s]/g : /[=?_\s%]/g;
589
+ return (section.value
590
+ // fix invalidly encoded chars
591
+ .replace(specials, s => {
592
+ const c = s.charCodeAt(0).toString(16);
593
+ if (s === ' ') {
594
+ return '_';
595
+ }
596
+ return '%' + (c.length < 2 ? '0' : '') + c;
597
+ })
598
+ // change from urlencoding to percent encoding
599
+ .replace(/%/g, '='));
600
+ }
487
601
  /**
488
602
  * Returns file extension for a content type string. If no suitable extensions
489
603
  * are found, 'bin' is used as the default extension
@@ -1256,6 +1256,7 @@ class MimeNode {
1256
1256
  *
1257
1257
  * @param addresses An array of address objects
1258
1258
  * @param [uniqueList] An array to be populated with addresses
1259
+ * @param [seenAddresses] Addresses already added to uniqueList, shared with recursive calls to keep deduplication linear
1259
1260
  * @return address string
1260
1261
  * @internal
1261
1262
  */
@@ -1393,7 +1394,7 @@ class MimeNode {
1393
1394
  /**
1394
1395
  * If needed, mime encodes the name part
1395
1396
  *
1396
- * @param name Name part of an address
1397
+ * @param value Name part of an address
1397
1398
  * @returns Mime word encoded string if needed
1398
1399
  * @internal
1399
1400
  */
@@ -75,11 +75,22 @@ export declare function createTransport(transporter?: TransportConfig | Transpor
75
75
  /**
76
76
  * Creates a test account from the Ethereal service (https://ethereal.email)
77
77
  *
78
- * @param apiUrl Optional API endpoint, defaults to https://api.nodemailer.com
79
- * @param callback Callback function to run with the account object. If not set, a Promise is returned
78
+ * @param callback Callback function to run with the account object
80
79
  */
81
80
  export declare function createTestAccount(callback: TestAccountCallback): void;
81
+ /**
82
+ * Creates a test account from the Ethereal service (https://ethereal.email)
83
+ *
84
+ * @param apiUrl API endpoint, defaults to https://api.nodemailer.com
85
+ * @param callback Callback function to run with the account object
86
+ */
82
87
  export declare function createTestAccount(apiUrl: string | false | null | undefined, callback: TestAccountCallback): void;
88
+ /**
89
+ * Creates a test account from the Ethereal service (https://ethereal.email)
90
+ *
91
+ * @param [apiUrl] API endpoint, defaults to https://api.nodemailer.com
92
+ * @returns Promise that resolves with the account object
93
+ */
83
94
  export declare function createTestAccount(apiUrl?: string | false | null): Promise<TestAccount>;
84
95
  /**
85
96
  * Resolves the Ethereal web URL for a message sent through an Ethereal test account
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.13";
2
+ export declare const version = "10.0.14";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = 'nodemailer';
3
- export const version = '10.0.13';
3
+ export const version = '10.0.14';
4
4
  export const homepage = 'https://nodemailer.com/';
@@ -175,6 +175,7 @@ const basicToDigit = function (codePoint) {
175
175
  * @see `basicToDigit()`
176
176
  * @private
177
177
  * @param digit The numeric value of a basic code point.
178
+ * @param flag Non-zero to use the uppercase form of the code point.
178
179
  * @returns The basic code point whose value (when used for
179
180
  * representing integers) is `digit`, which needs to be in the range
180
181
  * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
@@ -85,9 +85,14 @@ declare class SESTransport extends EventEmitter {
85
85
  /**
86
86
  * Verifies SES configuration
87
87
  *
88
- * @param callback Callback function
88
+ * @returns Promise that resolves to true if the configuration is usable
89
89
  */
90
90
  verify(): Promise<true>;
91
+ /**
92
+ * Verifies SES configuration
93
+ *
94
+ * @param callback Callback function
95
+ */
91
96
  verify(callback: VerifyCallback): void;
92
97
  }
93
98
  /**
@@ -187,6 +187,7 @@ export declare const parseConnectionUrl: (str?: string | null) => ConnectionUrlO
187
187
  * creates a default console logger
188
188
  *
189
189
  * @param [options] Options object that might include 'logger' value
190
+ * @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself
190
191
  * @return bunyan compatible logger
191
192
  */
192
193
  export declare const getLogger: (options?: GetLoggerOptions, defaults?: LogEntry) => Logger;
@@ -210,18 +211,41 @@ export declare const parseDataURI: (uri: unknown) => ParsedDataURI | null;
210
211
  *
211
212
  * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
212
213
  * @param key Property name or an Array index
213
- * @param [options] Optional access policy: { disableFileAccess, disableUrlAccess }
214
214
  * @param callback Callback function with (err, value)
215
215
  */
216
216
  export declare function resolveContent(data: {
217
217
  [key: string]: any;
218
218
  }, key: string | number, callback: ResolveContentCallback): void;
219
+ /**
220
+ * Resolves a String or a Buffer value for content value
221
+ *
222
+ * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
223
+ * @param key Property name or an Array index
224
+ * @param options Access policy: { disableFileAccess, disableUrlAccess }
225
+ * @param callback Callback function with (err, value)
226
+ */
219
227
  export declare function resolveContent(data: {
220
228
  [key: string]: any;
221
229
  }, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback): void;
230
+ /**
231
+ * Resolves a String or a Buffer value for content value
232
+ *
233
+ * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
234
+ * @param key Property name or an Array index
235
+ * @param [options] Optional access policy: { disableFileAccess, disableUrlAccess }
236
+ * @returns Promise that resolves with the value
237
+ */
222
238
  export declare function resolveContent(data: {
223
239
  [key: string]: any;
224
240
  }, key: string | number, options?: ResolveContentOptions | false): Promise<any>;
241
+ /**
242
+ * Resolves a String or a Buffer value for content value
243
+ *
244
+ * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
245
+ * @param key Property name or an Array index
246
+ * @param options Access policy: { disableFileAccess, disableUrlAccess }
247
+ * @param callback Callback function with (err, value), a Promise is returned if not set
248
+ */
225
249
  export declare function resolveContent(data: {
226
250
  [key: string]: any;
227
251
  }, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback | undefined): Promise<any> | void;
@@ -318,6 +318,7 @@ export const _logFunc = (logger, level, defaults, data, message, ...args) => {
318
318
  * creates a default console logger
319
319
  *
320
320
  * @param [options] Options object that might include 'logger' value
321
+ * @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself
321
322
  * @return bunyan compatible logger
322
323
  */
323
324
  export const getLogger = (options, defaults) => {
@@ -21,13 +21,21 @@ export type HttpProxyClientCallback = Callback<net.Socket>;
21
21
  * socket.write("GET / HTTP/1.0\r\n\r\n");
22
22
  * });
23
23
  *
24
- * @param proxyUrl proxy configuration, etg "http://proxy.host:3128/"
24
+ * @param proxyUrl proxy configuration, e.g. "http://proxy.host:3128/"
25
25
  * @param destinationPort Port to open in destination host
26
26
  * @param destinationHost Destination hostname
27
- * @param [tlsOptions] Optional TLS options for an HTTPS proxy (e.g. { rejectUnauthorized: false })
28
- * @param callback Callback to run with the rocket object once connection is established
27
+ * @param callback Callback to run with the socket object once connection is established
29
28
  */
30
29
  declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, callback: HttpProxyClientCallback): void;
30
+ /**
31
+ * Establishes proxied connection to destinationPort through an HTTPS proxy
32
+ *
33
+ * @param proxyUrl proxy configuration, e.g. "https://proxy.host:3128/"
34
+ * @param destinationPort Port to open in destination host
35
+ * @param destinationHost Destination hostname
36
+ * @param tlsOptions TLS options for the proxy connection (e.g. { rejectUnauthorized: false })
37
+ * @param callback Callback to run with the socket object once connection is established
38
+ */
31
39
  declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, tlsOptions: HttpProxyClientOptions | undefined, callback: HttpProxyClientCallback): void;
32
40
  /**
33
41
  * Socket timeout in milliseconds while the CONNECT handshake is in progress, defaults to 30 seconds.
@@ -88,18 +88,28 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
88
88
  .join('\r\n') +
89
89
  // End request
90
90
  '\r\n\r\n');
91
- let headers = '';
91
+ // The response is collected as chunks and only the bytes that just arrived, together
92
+ // with the three before them, are searched for the end of the headers. Appending to a
93
+ // string and searching all of it again re-read the whole response on every chunk.
94
+ const chunks = [];
95
+ let received = 0;
96
+ let tail = '';
92
97
  const onSocketData = (chunk) => {
93
98
  let match;
94
- let remainder;
95
99
  if (finished) {
96
100
  return;
97
101
  }
98
- headers += chunk.toString('binary');
99
- if ((match = headers.match(/\r\n\r\n/))) {
102
+ const window = tail + chunk.toString('binary');
103
+ const windowEnd = window.indexOf('\r\n\r\n');
104
+ chunks.push(chunk);
105
+ received += chunk.length;
106
+ tail = window.slice(-3);
107
+ if (windowEnd >= 0) {
100
108
  socket.removeListener('data', onSocketData);
101
- remainder = headers.substr(match.index + match[0].length);
102
- headers = headers.substr(0, match.index);
109
+ const headerEnd = received - window.length + windowEnd;
110
+ const response = Buffer.concat(chunks, received).toString('binary');
111
+ const headers = response.substr(0, headerEnd);
112
+ const remainder = response.substr(headerEnd + 4);
103
113
  if (remainder) {
104
114
  socket.unshift(Buffer.from(remainder, 'binary'));
105
115
  }
@@ -123,7 +133,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
123
133
  socket.setTimeout(0);
124
134
  return done(null, socket);
125
135
  }
126
- if (headers.length > MAX_RESPONSE_HEADER_BYTES) {
136
+ if (received > MAX_RESPONSE_HEADER_BYTES) {
127
137
  socket.removeListener('data', onSocketData);
128
138
  const err = new Error('Proxy response headers too large');
129
139
  err.code = errors.EPROXY;
@@ -379,7 +379,7 @@ declare class SMTPConnection extends EventEmitter {
379
379
  *
380
380
  * @param envelope Envelope object, {from: addr, to: [addr]}
381
381
  * @param message String, Buffer or a Stream
382
- * @param callback Callback to return once sending is completed
382
+ * @param done Callback to return once sending is completed
383
383
  */
384
384
  send(envelope: SMTPEnvelope, message: string | Buffer | Readable, done: SMTPConnectionSendCallback): void;
385
385
  /**