@depup/nodemailer 7.0.12-depup.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 (47) hide show
  1. package/.gitattributes +6 -0
  2. package/.ncurc.js +9 -0
  3. package/.prettierignore +8 -0
  4. package/.prettierrc +12 -0
  5. package/.prettierrc.js +10 -0
  6. package/.release-please-config.json +9 -0
  7. package/CHANGELOG.md +929 -0
  8. package/CODE_OF_CONDUCT.md +76 -0
  9. package/LICENSE +16 -0
  10. package/README.md +86 -0
  11. package/SECURITY.txt +22 -0
  12. package/eslint.config.js +88 -0
  13. package/lib/addressparser/index.js +383 -0
  14. package/lib/base64/index.js +139 -0
  15. package/lib/dkim/index.js +253 -0
  16. package/lib/dkim/message-parser.js +155 -0
  17. package/lib/dkim/relaxed-body.js +154 -0
  18. package/lib/dkim/sign.js +117 -0
  19. package/lib/fetch/cookies.js +281 -0
  20. package/lib/fetch/index.js +280 -0
  21. package/lib/json-transport/index.js +82 -0
  22. package/lib/mail-composer/index.js +629 -0
  23. package/lib/mailer/index.js +441 -0
  24. package/lib/mailer/mail-message.js +316 -0
  25. package/lib/mime-funcs/index.js +625 -0
  26. package/lib/mime-funcs/mime-types.js +2113 -0
  27. package/lib/mime-node/index.js +1316 -0
  28. package/lib/mime-node/last-newline.js +33 -0
  29. package/lib/mime-node/le-unix.js +43 -0
  30. package/lib/mime-node/le-windows.js +52 -0
  31. package/lib/nodemailer.js +157 -0
  32. package/lib/punycode/index.js +460 -0
  33. package/lib/qp/index.js +227 -0
  34. package/lib/sendmail-transport/index.js +210 -0
  35. package/lib/ses-transport/index.js +234 -0
  36. package/lib/shared/index.js +754 -0
  37. package/lib/smtp-connection/data-stream.js +108 -0
  38. package/lib/smtp-connection/http-proxy-client.js +143 -0
  39. package/lib/smtp-connection/index.js +1865 -0
  40. package/lib/smtp-pool/index.js +652 -0
  41. package/lib/smtp-pool/pool-resource.js +259 -0
  42. package/lib/smtp-transport/index.js +421 -0
  43. package/lib/stream-transport/index.js +135 -0
  44. package/lib/well-known/index.js +47 -0
  45. package/lib/well-known/services.json +611 -0
  46. package/lib/xoauth2/index.js +427 -0
  47. package/package.json +47 -0
@@ -0,0 +1,1316 @@
1
+ /* eslint no-undefined: 0, prefer-spread: 0, no-control-regex: 0 */
2
+
3
+ 'use strict';
4
+
5
+ const crypto = require('crypto');
6
+ const fs = require('fs');
7
+ const punycode = require('../punycode');
8
+ const PassThrough = require('stream').PassThrough;
9
+ const shared = require('../shared');
10
+
11
+ const mimeFuncs = require('../mime-funcs');
12
+ const qp = require('../qp');
13
+ const base64 = require('../base64');
14
+ const addressparser = require('../addressparser');
15
+ const nmfetch = require('../fetch');
16
+ const LastNewline = require('./last-newline');
17
+
18
+ const LeWindows = require('./le-windows');
19
+ const LeUnix = require('./le-unix');
20
+
21
+ /**
22
+ * Creates a new mime tree node. Assumes 'multipart/*' as the content type
23
+ * if it is a branch, anything else counts as leaf. If rootNode is missing from
24
+ * the options, assumes this is the root.
25
+ *
26
+ * @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
27
+ * @param {Object} [options] optional options
28
+ * @param {Object} [options.rootNode] root node for this tree
29
+ * @param {Object} [options.parentNode] immediate parent for this node
30
+ * @param {Object} [options.filename] filename for an attachment node
31
+ * @param {String} [options.baseBoundary] shared part of the unique multipart boundary
32
+ * @param {Boolean} [options.keepBcc] If true, do not exclude Bcc from the generated headers
33
+ * @param {Function} [options.normalizeHeaderKey] method to normalize header keys for custom caseing
34
+ * @param {String} [options.textEncoding] either 'Q' (the default) or 'B'
35
+ */
36
+ class MimeNode {
37
+ constructor(contentType, options) {
38
+ this.nodeCounter = 0;
39
+
40
+ options = options || {};
41
+
42
+ /**
43
+ * shared part of the unique multipart boundary
44
+ */
45
+ this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
46
+ this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
47
+
48
+ this.disableFileAccess = !!options.disableFileAccess;
49
+ this.disableUrlAccess = !!options.disableUrlAccess;
50
+
51
+ this.normalizeHeaderKey = options.normalizeHeaderKey;
52
+
53
+ /**
54
+ * If date headers is missing and current node is the root, this value is used instead
55
+ */
56
+ this.date = new Date();
57
+
58
+ /**
59
+ * Root node for current mime tree
60
+ */
61
+ this.rootNode = options.rootNode || this;
62
+
63
+ /**
64
+ * If true include Bcc in generated headers (if available)
65
+ */
66
+ this.keepBcc = !!options.keepBcc;
67
+
68
+ /**
69
+ * If filename is specified but contentType is not (probably an attachment)
70
+ * detect the content type from filename extension
71
+ */
72
+ if (options.filename) {
73
+ /**
74
+ * Filename for this node. Useful with attachments
75
+ */
76
+ this.filename = options.filename;
77
+ if (!contentType) {
78
+ contentType = mimeFuncs.detectMimeType(this.filename.split('.').pop());
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Indicates which encoding should be used for header strings: "Q" or "B"
84
+ */
85
+ this.textEncoding = (options.textEncoding || '').toString().trim().charAt(0).toUpperCase();
86
+
87
+ /**
88
+ * Immediate parent for this node (or undefined if not set)
89
+ */
90
+ this.parentNode = options.parentNode;
91
+
92
+ /**
93
+ * Hostname for default message-id values
94
+ */
95
+ this.hostname = options.hostname;
96
+
97
+ /**
98
+ * If set to 'win' then uses \r\n, if 'linux' then \n. If not set (or `raw` is used) then newlines are kept as is.
99
+ */
100
+ this.newline = options.newline;
101
+
102
+ /**
103
+ * An array for possible child nodes
104
+ */
105
+ this.childNodes = [];
106
+
107
+ /**
108
+ * Used for generating unique boundaries (prepended to the shared base)
109
+ */
110
+ this._nodeId = ++this.rootNode.nodeCounter;
111
+
112
+ /**
113
+ * A list of header values for this node in the form of [{key:'', value:''}]
114
+ */
115
+ this._headers = [];
116
+
117
+ /**
118
+ * True if the content only uses ASCII printable characters
119
+ * @type {Boolean}
120
+ */
121
+ this._isPlainText = false;
122
+
123
+ /**
124
+ * True if the content is plain text but has longer lines than allowed
125
+ * @type {Boolean}
126
+ */
127
+ this._hasLongLines = false;
128
+
129
+ /**
130
+ * If set, use instead this value for envelopes instead of generating one
131
+ * @type {Boolean}
132
+ */
133
+ this._envelope = false;
134
+
135
+ /**
136
+ * If set then use this value as the stream content instead of building it
137
+ * @type {String|Buffer|Stream}
138
+ */
139
+ this._raw = false;
140
+
141
+ /**
142
+ * Additional transform streams that the message will be piped before
143
+ * exposing by createReadStream
144
+ * @type {Array}
145
+ */
146
+ this._transforms = [];
147
+
148
+ /**
149
+ * Additional process functions that the message will be piped through before
150
+ * exposing by createReadStream. These functions are run after transforms
151
+ * @type {Array}
152
+ */
153
+ this._processFuncs = [];
154
+
155
+ /**
156
+ * If content type is set (or derived from the filename) add it to headers
157
+ */
158
+ if (contentType) {
159
+ this.setHeader('Content-Type', contentType);
160
+ }
161
+ }
162
+
163
+ /////// PUBLIC METHODS
164
+
165
+ /**
166
+ * Creates and appends a child node.Arguments provided are passed to MimeNode constructor
167
+ *
168
+ * @param {String} [contentType] Optional content type
169
+ * @param {Object} [options] Optional options object
170
+ * @return {Object} Created node object
171
+ */
172
+ createChild(contentType, options) {
173
+ if (!options && typeof contentType === 'object') {
174
+ options = contentType;
175
+ contentType = undefined;
176
+ }
177
+ let node = new MimeNode(contentType, options);
178
+ this.appendChild(node);
179
+ return node;
180
+ }
181
+
182
+ /**
183
+ * Appends an existing node to the mime tree. Removes the node from an existing
184
+ * tree if needed
185
+ *
186
+ * @param {Object} childNode node to be appended
187
+ * @return {Object} Appended node object
188
+ */
189
+ appendChild(childNode) {
190
+ if (childNode.rootNode !== this.rootNode) {
191
+ childNode.rootNode = this.rootNode;
192
+ childNode._nodeId = ++this.rootNode.nodeCounter;
193
+ }
194
+
195
+ childNode.parentNode = this;
196
+
197
+ this.childNodes.push(childNode);
198
+ return childNode;
199
+ }
200
+
201
+ /**
202
+ * Replaces current node with another node
203
+ *
204
+ * @param {Object} node Replacement node
205
+ * @return {Object} Replacement node
206
+ */
207
+ replace(node) {
208
+ if (node === this) {
209
+ return this;
210
+ }
211
+
212
+ this.parentNode.childNodes.forEach((childNode, i) => {
213
+ if (childNode === this) {
214
+ node.rootNode = this.rootNode;
215
+ node.parentNode = this.parentNode;
216
+ node._nodeId = this._nodeId;
217
+
218
+ this.rootNode = this;
219
+ this.parentNode = undefined;
220
+
221
+ node.parentNode.childNodes[i] = node;
222
+ }
223
+ });
224
+
225
+ return node;
226
+ }
227
+
228
+ /**
229
+ * Removes current node from the mime tree
230
+ *
231
+ * @return {Object} removed node
232
+ */
233
+ remove() {
234
+ if (!this.parentNode) {
235
+ return this;
236
+ }
237
+
238
+ for (let i = this.parentNode.childNodes.length - 1; i >= 0; i--) {
239
+ if (this.parentNode.childNodes[i] === this) {
240
+ this.parentNode.childNodes.splice(i, 1);
241
+ this.parentNode = undefined;
242
+ this.rootNode = this;
243
+ return this;
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Sets a header value. If the value for selected key exists, it is overwritten.
250
+ * You can set multiple values as well by using [{key:'', value:''}] or
251
+ * {key: 'value'} as the first argument.
252
+ *
253
+ * @param {String|Array|Object} key Header key or a list of key value pairs
254
+ * @param {String} value Header value
255
+ * @return {Object} current node
256
+ */
257
+ setHeader(key, value) {
258
+ let added = false,
259
+ headerValue;
260
+
261
+ // Allow setting multiple headers at once
262
+ if (!value && key && typeof key === 'object') {
263
+ // allow {key:'content-type', value: 'text/plain'}
264
+ if (key.key && 'value' in key) {
265
+ this.setHeader(key.key, key.value);
266
+ } else if (Array.isArray(key)) {
267
+ // allow [{key:'content-type', value: 'text/plain'}]
268
+ key.forEach(i => {
269
+ this.setHeader(i.key, i.value);
270
+ });
271
+ } else {
272
+ // allow {'content-type': 'text/plain'}
273
+ Object.keys(key).forEach(i => {
274
+ this.setHeader(i, key[i]);
275
+ });
276
+ }
277
+ return this;
278
+ }
279
+
280
+ key = this._normalizeHeaderKey(key);
281
+
282
+ headerValue = {
283
+ key,
284
+ value
285
+ };
286
+
287
+ // Check if the value exists and overwrite
288
+ for (let i = 0, len = this._headers.length; i < len; i++) {
289
+ if (this._headers[i].key === key) {
290
+ if (!added) {
291
+ // replace the first match
292
+ this._headers[i] = headerValue;
293
+ added = true;
294
+ } else {
295
+ // remove following matches
296
+ this._headers.splice(i, 1);
297
+ i--;
298
+ len--;
299
+ }
300
+ }
301
+ }
302
+
303
+ // match not found, append the value
304
+ if (!added) {
305
+ this._headers.push(headerValue);
306
+ }
307
+
308
+ return this;
309
+ }
310
+
311
+ /**
312
+ * Adds a header value. If the value for selected key exists, the value is appended
313
+ * as a new field and old one is not touched.
314
+ * You can set multiple values as well by using [{key:'', value:''}] or
315
+ * {key: 'value'} as the first argument.
316
+ *
317
+ * @param {String|Array|Object} key Header key or a list of key value pairs
318
+ * @param {String} value Header value
319
+ * @return {Object} current node
320
+ */
321
+ addHeader(key, value) {
322
+ // Allow setting multiple headers at once
323
+ if (!value && key && typeof key === 'object') {
324
+ // allow {key:'content-type', value: 'text/plain'}
325
+ if (key.key && key.value) {
326
+ this.addHeader(key.key, key.value);
327
+ } else if (Array.isArray(key)) {
328
+ // allow [{key:'content-type', value: 'text/plain'}]
329
+ key.forEach(i => {
330
+ this.addHeader(i.key, i.value);
331
+ });
332
+ } else {
333
+ // allow {'content-type': 'text/plain'}
334
+ Object.keys(key).forEach(i => {
335
+ this.addHeader(i, key[i]);
336
+ });
337
+ }
338
+ return this;
339
+ } else if (Array.isArray(value)) {
340
+ value.forEach(val => {
341
+ this.addHeader(key, val);
342
+ });
343
+ return this;
344
+ }
345
+
346
+ this._headers.push({
347
+ key: this._normalizeHeaderKey(key),
348
+ value
349
+ });
350
+
351
+ return this;
352
+ }
353
+
354
+ /**
355
+ * Retrieves the first mathcing value of a selected key
356
+ *
357
+ * @param {String} key Key to search for
358
+ * @retun {String} Value for the key
359
+ */
360
+ getHeader(key) {
361
+ key = this._normalizeHeaderKey(key);
362
+ for (let i = 0, len = this._headers.length; i < len; i++) {
363
+ if (this._headers[i].key === key) {
364
+ return this._headers[i].value;
365
+ }
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Sets body content for current node. If the value is a string, charset is added automatically
371
+ * to Content-Type (if it is text/*). If the value is a Buffer, you need to specify
372
+ * the charset yourself
373
+ *
374
+ * @param (String|Buffer) content Body content
375
+ * @return {Object} current node
376
+ */
377
+ setContent(content) {
378
+ this.content = content;
379
+ if (typeof this.content.pipe === 'function') {
380
+ // pre-stream handler. might be triggered if a stream is set as content
381
+ // and 'error' fires before anything is done with this stream
382
+ this._contentErrorHandler = err => {
383
+ this.content.removeListener('error', this._contentErrorHandler);
384
+ this.content = err;
385
+ };
386
+ this.content.once('error', this._contentErrorHandler);
387
+ } else if (typeof this.content === 'string') {
388
+ this._isPlainText = mimeFuncs.isPlainText(this.content);
389
+ if (this._isPlainText && mimeFuncs.hasLongerLines(this.content, 76)) {
390
+ // If there are lines longer than 76 symbols/bytes do not use 7bit
391
+ this._hasLongLines = true;
392
+ }
393
+ }
394
+ return this;
395
+ }
396
+
397
+ build(callback) {
398
+ let promise;
399
+
400
+ if (!callback) {
401
+ promise = new Promise((resolve, reject) => {
402
+ callback = shared.callbackPromise(resolve, reject);
403
+ });
404
+ }
405
+
406
+ let stream = this.createReadStream();
407
+ let buf = [];
408
+ let buflen = 0;
409
+ let returned = false;
410
+
411
+ stream.on('readable', () => {
412
+ let chunk;
413
+
414
+ while ((chunk = stream.read()) !== null) {
415
+ buf.push(chunk);
416
+ buflen += chunk.length;
417
+ }
418
+ });
419
+
420
+ stream.once('error', err => {
421
+ if (returned) {
422
+ return;
423
+ }
424
+ returned = true;
425
+
426
+ return callback(err);
427
+ });
428
+
429
+ stream.once('end', chunk => {
430
+ if (returned) {
431
+ return;
432
+ }
433
+ returned = true;
434
+
435
+ if (chunk && chunk.length) {
436
+ buf.push(chunk);
437
+ buflen += chunk.length;
438
+ }
439
+ return callback(null, Buffer.concat(buf, buflen));
440
+ });
441
+
442
+ return promise;
443
+ }
444
+
445
+ getTransferEncoding() {
446
+ let transferEncoding = false;
447
+ let contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
448
+
449
+ if (this.content) {
450
+ transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
451
+ if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
452
+ if (/^text\//i.test(contentType)) {
453
+ // If there are no special symbols, no need to modify the text
454
+ if (this._isPlainText && !this._hasLongLines) {
455
+ transferEncoding = '7bit';
456
+ } else if (typeof this.content === 'string' || this.content instanceof Buffer) {
457
+ // detect preferred encoding for string value
458
+ transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
459
+ } else {
460
+ // we can not check content for a stream, so either use preferred encoding or fallback to QP
461
+ transferEncoding = this.textEncoding === 'B' ? 'base64' : 'quoted-printable';
462
+ }
463
+ } else if (!/^(multipart|message)\//i.test(contentType)) {
464
+ transferEncoding = transferEncoding || 'base64';
465
+ }
466
+ }
467
+ }
468
+ return transferEncoding;
469
+ }
470
+
471
+ /**
472
+ * Builds the header block for the mime node. Append \r\n\r\n before writing the content
473
+ *
474
+ * @returns {String} Headers
475
+ */
476
+ buildHeaders() {
477
+ let transferEncoding = this.getTransferEncoding();
478
+ let headers = [];
479
+
480
+ if (transferEncoding) {
481
+ this.setHeader('Content-Transfer-Encoding', transferEncoding);
482
+ }
483
+
484
+ if (this.filename && !this.getHeader('Content-Disposition')) {
485
+ this.setHeader('Content-Disposition', 'attachment');
486
+ }
487
+
488
+ // Ensure mandatory header fields
489
+ if (this.rootNode === this) {
490
+ if (!this.getHeader('Date')) {
491
+ this.setHeader('Date', this.date.toUTCString().replace(/GMT/, '+0000'));
492
+ }
493
+
494
+ // ensure that Message-Id is present
495
+ this.messageId();
496
+
497
+ if (!this.getHeader('MIME-Version')) {
498
+ this.setHeader('MIME-Version', '1.0');
499
+ }
500
+
501
+ // Ensure that Content-Type is the last header for the root node
502
+ for (let i = this._headers.length - 2; i >= 0; i--) {
503
+ let header = this._headers[i];
504
+ if (header.key === 'Content-Type') {
505
+ this._headers.splice(i, 1);
506
+ this._headers.push(header);
507
+ }
508
+ }
509
+ }
510
+
511
+ this._headers.forEach(header => {
512
+ let key = header.key;
513
+ let value = header.value;
514
+ let structured;
515
+ let param;
516
+ let options = {};
517
+ let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
518
+
519
+ if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
520
+ Object.keys(value).forEach(key => {
521
+ if (key !== 'value') {
522
+ options[key] = value[key];
523
+ }
524
+ });
525
+ value = (value.value || '').toString();
526
+ if (!value.trim()) {
527
+ return;
528
+ }
529
+ }
530
+
531
+ if (options.prepared) {
532
+ // header value is
533
+ if (options.foldLines) {
534
+ headers.push(mimeFuncs.foldLines(key + ': ' + value));
535
+ } else {
536
+ headers.push(key + ': ' + value);
537
+ }
538
+ return;
539
+ }
540
+
541
+ switch (header.key) {
542
+ case 'Content-Disposition':
543
+ structured = mimeFuncs.parseHeaderValue(value);
544
+ if (this.filename) {
545
+ structured.params.filename = this.filename;
546
+ }
547
+ value = mimeFuncs.buildHeaderValue(structured);
548
+ break;
549
+
550
+ case 'Content-Type':
551
+ structured = mimeFuncs.parseHeaderValue(value);
552
+
553
+ this._handleContentType(structured);
554
+
555
+ if (
556
+ structured.value.match(/^text\/plain\b/) &&
557
+ typeof this.content === 'string' &&
558
+ /[\u0080-\uFFFF]/.test(this.content)
559
+ ) {
560
+ structured.params.charset = 'utf-8';
561
+ }
562
+
563
+ value = mimeFuncs.buildHeaderValue(structured);
564
+
565
+ if (this.filename) {
566
+ // add support for non-compliant clients like QQ webmail
567
+ // we can't build the value with buildHeaderValue as the value is non standard and
568
+ // would be converted to parameter continuation encoding that we do not want
569
+ param = this._encodeWords(this.filename);
570
+
571
+ if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
572
+ // include value in quotes if needed
573
+ param = '"' + param + '"';
574
+ }
575
+ value += '; name=' + param;
576
+ }
577
+ break;
578
+
579
+ case 'Bcc':
580
+ if (!this.keepBcc) {
581
+ // skip BCC values
582
+ return;
583
+ }
584
+ break;
585
+ }
586
+
587
+ value = this._encodeHeaderValue(key, value);
588
+
589
+ // skip empty lines
590
+ if (!(value || '').toString().trim()) {
591
+ return;
592
+ }
593
+
594
+ if (typeof this.normalizeHeaderKey === 'function') {
595
+ let normalized = this.normalizeHeaderKey(key, value);
596
+ if (normalized && typeof normalized === 'string' && normalized.length) {
597
+ key = normalized;
598
+ }
599
+ }
600
+
601
+ headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
602
+ });
603
+
604
+ return headers.join('\r\n');
605
+ }
606
+
607
+ /**
608
+ * Streams the rfc2822 message from the current node. If this is a root node,
609
+ * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
610
+ *
611
+ * @return {String} Compiled message
612
+ */
613
+ createReadStream(options) {
614
+ options = options || {};
615
+
616
+ let stream = new PassThrough(options);
617
+ let outputStream = stream;
618
+ let transform;
619
+
620
+ this.stream(stream, options, err => {
621
+ if (err) {
622
+ outputStream.emit('error', err);
623
+ return;
624
+ }
625
+ stream.end();
626
+ });
627
+
628
+ for (let i = 0, len = this._transforms.length; i < len; i++) {
629
+ transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
630
+ outputStream.once('error', err => {
631
+ transform.emit('error', err);
632
+ });
633
+ outputStream = outputStream.pipe(transform);
634
+ }
635
+
636
+ // ensure terminating newline after possible user transforms
637
+ transform = new LastNewline();
638
+ outputStream.once('error', err => {
639
+ transform.emit('error', err);
640
+ });
641
+ outputStream = outputStream.pipe(transform);
642
+
643
+ // dkim and stuff
644
+ for (let i = 0, len = this._processFuncs.length; i < len; i++) {
645
+ transform = this._processFuncs[i];
646
+ outputStream = transform(outputStream);
647
+ }
648
+
649
+ if (this.newline) {
650
+ const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
651
+ const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
652
+
653
+ const stream = outputStream.pipe(newlineTransform);
654
+ outputStream.on('error', err => stream.emit('error', err));
655
+ return stream;
656
+ }
657
+
658
+ return outputStream;
659
+ }
660
+
661
+ /**
662
+ * Appends a transform stream object to the transforms list. Final output
663
+ * is passed through this stream before exposing
664
+ *
665
+ * @param {Object} transform Read-Write stream
666
+ */
667
+ transform(transform) {
668
+ this._transforms.push(transform);
669
+ }
670
+
671
+ /**
672
+ * Appends a post process function. The functon is run after transforms and
673
+ * uses the following syntax
674
+ *
675
+ * processFunc(input) -> outputStream
676
+ *
677
+ * @param {Object} processFunc Read-Write stream
678
+ */
679
+ processFunc(processFunc) {
680
+ this._processFuncs.push(processFunc);
681
+ }
682
+
683
+ stream(outputStream, options, done) {
684
+ let transferEncoding = this.getTransferEncoding();
685
+ let contentStream;
686
+ let localStream;
687
+
688
+ // protect actual callback against multiple triggering
689
+ let returned = false;
690
+ let callback = err => {
691
+ if (returned) {
692
+ return;
693
+ }
694
+ returned = true;
695
+ done(err);
696
+ };
697
+
698
+ // for multipart nodes, push child nodes
699
+ // for content nodes end the stream
700
+ let finalize = () => {
701
+ let childId = 0;
702
+ let processChildNode = () => {
703
+ if (childId >= this.childNodes.length) {
704
+ outputStream.write('\r\n--' + this.boundary + '--\r\n');
705
+ return callback();
706
+ }
707
+ let child = this.childNodes[childId++];
708
+ outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
709
+ child.stream(outputStream, options, err => {
710
+ if (err) {
711
+ return callback(err);
712
+ }
713
+ setImmediate(processChildNode);
714
+ });
715
+ };
716
+
717
+ if (this.multipart) {
718
+ setImmediate(processChildNode);
719
+ } else {
720
+ return callback();
721
+ }
722
+ };
723
+
724
+ // pushes node content
725
+ let sendContent = () => {
726
+ if (this.content) {
727
+ if (Object.prototype.toString.call(this.content) === '[object Error]') {
728
+ // content is already errored
729
+ return callback(this.content);
730
+ }
731
+
732
+ if (typeof this.content.pipe === 'function') {
733
+ this.content.removeListener('error', this._contentErrorHandler);
734
+ this._contentErrorHandler = err => callback(err);
735
+ this.content.once('error', this._contentErrorHandler);
736
+ }
737
+
738
+ let createStream = () => {
739
+ if (['quoted-printable', 'base64'].includes(transferEncoding)) {
740
+ contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
741
+
742
+ contentStream.pipe(outputStream, {
743
+ end: false
744
+ });
745
+ contentStream.once('end', finalize);
746
+ contentStream.once('error', err => callback(err));
747
+
748
+ localStream = this._getStream(this.content);
749
+ localStream.pipe(contentStream);
750
+ } else {
751
+ // anything that is not QP or Base54 passes as-is
752
+ localStream = this._getStream(this.content);
753
+ localStream.pipe(outputStream, {
754
+ end: false
755
+ });
756
+ localStream.once('end', finalize);
757
+ }
758
+
759
+ localStream.once('error', err => callback(err));
760
+ };
761
+
762
+ if (this.content._resolve) {
763
+ let chunks = [];
764
+ let chunklen = 0;
765
+ let returned = false;
766
+ let sourceStream = this._getStream(this.content);
767
+ sourceStream.on('error', err => {
768
+ if (returned) {
769
+ return;
770
+ }
771
+ returned = true;
772
+ callback(err);
773
+ });
774
+ sourceStream.on('readable', () => {
775
+ let chunk;
776
+ while ((chunk = sourceStream.read()) !== null) {
777
+ chunks.push(chunk);
778
+ chunklen += chunk.length;
779
+ }
780
+ });
781
+ sourceStream.on('end', () => {
782
+ if (returned) {
783
+ return;
784
+ }
785
+ returned = true;
786
+ this.content._resolve = false;
787
+ this.content._resolvedValue = Buffer.concat(chunks, chunklen);
788
+ setImmediate(createStream);
789
+ });
790
+ } else {
791
+ setImmediate(createStream);
792
+ }
793
+ return;
794
+ } else {
795
+ return setImmediate(finalize);
796
+ }
797
+ };
798
+
799
+ if (this._raw) {
800
+ setImmediate(() => {
801
+ if (Object.prototype.toString.call(this._raw) === '[object Error]') {
802
+ // content is already errored
803
+ return callback(this._raw);
804
+ }
805
+
806
+ // remove default error handler (if set)
807
+ if (typeof this._raw.pipe === 'function') {
808
+ this._raw.removeListener('error', this._contentErrorHandler);
809
+ }
810
+
811
+ let raw = this._getStream(this._raw);
812
+ raw.pipe(outputStream, {
813
+ end: false
814
+ });
815
+ raw.on('error', err => outputStream.emit('error', err));
816
+ raw.on('end', finalize);
817
+ });
818
+ } else {
819
+ outputStream.write(this.buildHeaders() + '\r\n\r\n');
820
+ setImmediate(sendContent);
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Sets envelope to be used instead of the generated one
826
+ *
827
+ * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
828
+ */
829
+ setEnvelope(envelope) {
830
+ let list;
831
+
832
+ this._envelope = {
833
+ from: false,
834
+ to: []
835
+ };
836
+
837
+ if (envelope.from) {
838
+ list = [];
839
+ this._convertAddresses(this._parseAddresses(envelope.from), list);
840
+ list = list.filter(address => address && address.address);
841
+ if (list.length && list[0]) {
842
+ this._envelope.from = list[0].address;
843
+ }
844
+ }
845
+ ['to', 'cc', 'bcc'].forEach(key => {
846
+ if (envelope[key]) {
847
+ this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
848
+ }
849
+ });
850
+
851
+ this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
852
+
853
+ let standardFields = ['to', 'cc', 'bcc', 'from'];
854
+ Object.keys(envelope).forEach(key => {
855
+ if (!standardFields.includes(key)) {
856
+ this._envelope[key] = envelope[key];
857
+ }
858
+ });
859
+
860
+ return this;
861
+ }
862
+
863
+ /**
864
+ * Generates and returns an object with parsed address fields
865
+ *
866
+ * @return {Object} Address object
867
+ */
868
+ getAddresses() {
869
+ let addresses = {};
870
+
871
+ this._headers.forEach(header => {
872
+ let key = header.key.toLowerCase();
873
+ if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
874
+ if (!Array.isArray(addresses[key])) {
875
+ addresses[key] = [];
876
+ }
877
+
878
+ this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
879
+ }
880
+ });
881
+
882
+ return addresses;
883
+ }
884
+
885
+ /**
886
+ * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
887
+ *
888
+ * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
889
+ */
890
+ getEnvelope() {
891
+ if (this._envelope) {
892
+ return this._envelope;
893
+ }
894
+
895
+ let envelope = {
896
+ from: false,
897
+ to: []
898
+ };
899
+ this._headers.forEach(header => {
900
+ let list = [];
901
+ if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
902
+ this._convertAddresses(this._parseAddresses(header.value), list);
903
+ if (list.length && list[0]) {
904
+ envelope.from = list[0].address;
905
+ }
906
+ } else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
907
+ this._convertAddresses(this._parseAddresses(header.value), envelope.to);
908
+ }
909
+ });
910
+
911
+ envelope.to = envelope.to.map(to => to.address);
912
+
913
+ return envelope;
914
+ }
915
+
916
+ /**
917
+ * Returns Message-Id value. If it does not exist, then creates one
918
+ *
919
+ * @return {String} Message-Id value
920
+ */
921
+ messageId() {
922
+ let messageId = this.getHeader('Message-ID');
923
+ // You really should define your own Message-Id field!
924
+ if (!messageId) {
925
+ messageId = this._generateMessageId();
926
+ this.setHeader('Message-ID', messageId);
927
+ }
928
+ return messageId;
929
+ }
930
+
931
+ /**
932
+ * Sets pregenerated content that will be used as the output of this node
933
+ *
934
+ * @param {String|Buffer|Stream} Raw MIME contents
935
+ */
936
+ setRaw(raw) {
937
+ this._raw = raw;
938
+
939
+ if (this._raw && typeof this._raw.pipe === 'function') {
940
+ // pre-stream handler. might be triggered if a stream is set as content
941
+ // and 'error' fires before anything is done with this stream
942
+ this._contentErrorHandler = err => {
943
+ this._raw.removeListener('error', this._contentErrorHandler);
944
+ this._raw = err;
945
+ };
946
+ this._raw.once('error', this._contentErrorHandler);
947
+ }
948
+
949
+ return this;
950
+ }
951
+
952
+ /////// PRIVATE METHODS
953
+
954
+ /**
955
+ * Detects and returns handle to a stream related with the content.
956
+ *
957
+ * @param {Mixed} content Node content
958
+ * @returns {Object} Stream object
959
+ */
960
+ _getStream(content) {
961
+ let contentStream;
962
+
963
+ if (content._resolvedValue) {
964
+ // pass string or buffer content as a stream
965
+ contentStream = new PassThrough();
966
+
967
+ setImmediate(() => {
968
+ try {
969
+ contentStream.end(content._resolvedValue);
970
+ } catch (_err) {
971
+ contentStream.emit('error', _err);
972
+ }
973
+ });
974
+
975
+ return contentStream;
976
+ } else if (typeof content.pipe === 'function') {
977
+ // assume as stream
978
+ return content;
979
+ } else if (content && typeof content.path === 'string' && !content.href) {
980
+ if (this.disableFileAccess) {
981
+ contentStream = new PassThrough();
982
+ setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
983
+ return contentStream;
984
+ }
985
+ // read file
986
+ return fs.createReadStream(content.path);
987
+ } else if (content && typeof content.href === 'string') {
988
+ if (this.disableUrlAccess) {
989
+ contentStream = new PassThrough();
990
+ setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
991
+ return contentStream;
992
+ }
993
+ // fetch URL
994
+ return nmfetch(content.href, { headers: content.httpHeaders });
995
+ } else {
996
+ // pass string or buffer content as a stream
997
+ contentStream = new PassThrough();
998
+
999
+ setImmediate(() => {
1000
+ try {
1001
+ contentStream.end(content || '');
1002
+ } catch (_err) {
1003
+ contentStream.emit('error', _err);
1004
+ }
1005
+ });
1006
+ return contentStream;
1007
+ }
1008
+ }
1009
+
1010
+ /**
1011
+ * Parses addresses. Takes in a single address or an array or an
1012
+ * array of address arrays (eg. To: [[first group], [second group],...])
1013
+ *
1014
+ * @param {Mixed} addresses Addresses to be parsed
1015
+ * @return {Array} An array of address objects
1016
+ */
1017
+ _parseAddresses(addresses) {
1018
+ return [].concat.apply(
1019
+ [],
1020
+ [].concat(addresses).map(address => {
1021
+ if (address && address.address) {
1022
+ address.address = this._normalizeAddress(address.address);
1023
+ address.name = address.name || '';
1024
+ return [address];
1025
+ }
1026
+ return addressparser(address);
1027
+ })
1028
+ );
1029
+ }
1030
+
1031
+ /**
1032
+ * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
1033
+ *
1034
+ * @param {String} key Key to be normalized
1035
+ * @return {String} key in Camel-Case form
1036
+ */
1037
+ _normalizeHeaderKey(key) {
1038
+ key = (key || '')
1039
+ .toString()
1040
+ // no newlines in keys
1041
+ .replace(/\r?\n|\r/g, ' ')
1042
+ .trim()
1043
+ .toLowerCase()
1044
+ // use uppercase words, except MIME
1045
+ .replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
1046
+ // special case
1047
+ .replace(/^Content-Features$/i, 'Content-features');
1048
+
1049
+ return key;
1050
+ }
1051
+
1052
+ /**
1053
+ * Checks if the content type is multipart and defines boundary if needed.
1054
+ * Doesn't return anything, modifies object argument instead.
1055
+ *
1056
+ * @param {Object} structured Parsed header value for 'Content-Type' key
1057
+ */
1058
+ _handleContentType(structured) {
1059
+ this.contentType = structured.value.trim().toLowerCase();
1060
+
1061
+ this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
1062
+
1063
+ if (this.multipart) {
1064
+ this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
1065
+ } else {
1066
+ this.boundary = false;
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Generates a multipart boundary value
1072
+ *
1073
+ * @return {String} boundary value
1074
+ */
1075
+ _generateBoundary() {
1076
+ return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
1077
+ }
1078
+
1079
+ /**
1080
+ * Encodes a header value for use in the generated rfc2822 email.
1081
+ *
1082
+ * @param {String} key Header key
1083
+ * @param {String} value Header value
1084
+ */
1085
+ _encodeHeaderValue(key, value) {
1086
+ key = this._normalizeHeaderKey(key);
1087
+
1088
+ switch (key) {
1089
+ // Structured headers
1090
+ case 'From':
1091
+ case 'Sender':
1092
+ case 'To':
1093
+ case 'Cc':
1094
+ case 'Bcc':
1095
+ case 'Reply-To':
1096
+ return this._convertAddresses(this._parseAddresses(value));
1097
+
1098
+ // values enclosed in <>
1099
+ case 'Message-ID':
1100
+ case 'In-Reply-To':
1101
+ case 'Content-Id':
1102
+ value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1103
+
1104
+ if (value.charAt(0) !== '<') {
1105
+ value = '<' + value;
1106
+ }
1107
+
1108
+ if (value.charAt(value.length - 1) !== '>') {
1109
+ value = value + '>';
1110
+ }
1111
+ return value;
1112
+
1113
+ // space separated list of values enclosed in <>
1114
+ case 'References':
1115
+ value = [].concat
1116
+ .apply(
1117
+ [],
1118
+ [].concat(value || '').map(elm => {
1119
+ elm = (elm || '')
1120
+ .toString()
1121
+ .replace(/\r?\n|\r/g, ' ')
1122
+ .trim();
1123
+ return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
1124
+ })
1125
+ )
1126
+ .map(elm => {
1127
+ if (elm.charAt(0) !== '<') {
1128
+ elm = '<' + elm;
1129
+ }
1130
+ if (elm.charAt(elm.length - 1) !== '>') {
1131
+ elm = elm + '>';
1132
+ }
1133
+ return elm;
1134
+ });
1135
+
1136
+ return value.join(' ').trim();
1137
+
1138
+ case 'Date':
1139
+ if (Object.prototype.toString.call(value) === '[object Date]') {
1140
+ return value.toUTCString().replace(/GMT/, '+0000');
1141
+ }
1142
+
1143
+ value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1144
+ return this._encodeWords(value);
1145
+
1146
+ case 'Content-Type':
1147
+ case 'Content-Disposition':
1148
+ // if it includes a filename then it is already encoded
1149
+ return (value || '').toString().replace(/\r?\n|\r/g, ' ');
1150
+
1151
+ default:
1152
+ value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1153
+ // encodeWords only encodes if needed, otherwise the original string is returned
1154
+ return this._encodeWords(value);
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Rebuilds address object using punycode and other adjustments
1160
+ *
1161
+ * @param {Array} addresses An array of address objects
1162
+ * @param {Array} [uniqueList] An array to be populated with addresses
1163
+ * @return {String} address string
1164
+ */
1165
+ _convertAddresses(addresses, uniqueList) {
1166
+ let values = [];
1167
+
1168
+ uniqueList = uniqueList || [];
1169
+
1170
+ [].concat(addresses || []).forEach(address => {
1171
+ if (address.address) {
1172
+ address.address = this._normalizeAddress(address.address);
1173
+
1174
+ if (!address.name) {
1175
+ values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
1176
+ } else if (address.name) {
1177
+ values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
1178
+ }
1179
+
1180
+ if (address.address) {
1181
+ if (!uniqueList.filter(a => a.address === address.address).length) {
1182
+ uniqueList.push(address);
1183
+ }
1184
+ }
1185
+ } else if (address.group) {
1186
+ let groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
1187
+ values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
1188
+ }
1189
+ });
1190
+
1191
+ return values.join(', ');
1192
+ }
1193
+
1194
+ /**
1195
+ * Normalizes an email address
1196
+ *
1197
+ * @param {Array} address An array of address objects
1198
+ * @return {String} address string
1199
+ */
1200
+ _normalizeAddress(address) {
1201
+ address = (address || '')
1202
+ .toString()
1203
+ .replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
1204
+ .trim();
1205
+
1206
+ let lastAt = address.lastIndexOf('@');
1207
+ if (lastAt < 0) {
1208
+ // Bare username
1209
+ return address;
1210
+ }
1211
+
1212
+ let user = address.substr(0, lastAt);
1213
+ let domain = address.substr(lastAt + 1);
1214
+
1215
+ // Usernames are not touched and are kept as is even if these include unicode
1216
+ // Domains are punycoded by default
1217
+ // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
1218
+ // non-unicode domains are left as is
1219
+
1220
+ let encodedDomain;
1221
+
1222
+ try {
1223
+ encodedDomain = punycode.toASCII(domain.toLowerCase());
1224
+ } catch (_err) {
1225
+ // keep as is?
1226
+ }
1227
+
1228
+ if (user.indexOf(' ') >= 0) {
1229
+ if (user.charAt(0) !== '"') {
1230
+ user = '"' + user;
1231
+ }
1232
+ if (user.substr(-1) !== '"') {
1233
+ user = user + '"';
1234
+ }
1235
+ }
1236
+
1237
+ return `${user}@${encodedDomain}`;
1238
+ }
1239
+
1240
+ /**
1241
+ * If needed, mime encodes the name part
1242
+ *
1243
+ * @param {String} name Name part of an address
1244
+ * @returns {String} Mime word encoded string if needed
1245
+ */
1246
+ _encodeAddressName(name) {
1247
+ if (!/^[\w ]*$/.test(name)) {
1248
+ if (/^[\x20-\x7e]*$/.test(name)) {
1249
+ return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
1250
+ } else {
1251
+ return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
1252
+ }
1253
+ }
1254
+ return name;
1255
+ }
1256
+
1257
+ /**
1258
+ * If needed, mime encodes the name part
1259
+ *
1260
+ * @param {String} name Name part of an address
1261
+ * @returns {String} Mime word encoded string if needed
1262
+ */
1263
+ _encodeWords(value) {
1264
+ // set encodeAll parameter to true even though it is against the recommendation of RFC2047,
1265
+ // by default only words that include non-ascii should be converted into encoded words
1266
+ // but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
1267
+ return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
1268
+ }
1269
+
1270
+ /**
1271
+ * Detects best mime encoding for a text value
1272
+ *
1273
+ * @param {String} value Value to check for
1274
+ * @return {String} either 'Q' or 'B'
1275
+ */
1276
+ _getTextEncoding(value) {
1277
+ value = (value || '').toString();
1278
+
1279
+ let encoding = this.textEncoding;
1280
+ let latinLen;
1281
+ let nonLatinLen;
1282
+
1283
+ if (!encoding) {
1284
+ // count latin alphabet symbols and 8-bit range symbols + control symbols
1285
+ // if there are more latin characters, then use quoted-printable
1286
+ // encoding, otherwise use base64
1287
+ nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length;
1288
+ latinLen = (value.match(/[a-z]/gi) || []).length;
1289
+ // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
1290
+ encoding = nonLatinLen < latinLen ? 'Q' : 'B';
1291
+ }
1292
+ return encoding;
1293
+ }
1294
+
1295
+ /**
1296
+ * Generates a message id
1297
+ *
1298
+ * @return {String} Random Message-ID value
1299
+ */
1300
+ _generateMessageId() {
1301
+ return (
1302
+ '<' +
1303
+ [2, 2, 2, 6].reduce(
1304
+ // crux to generate UUID-like random strings
1305
+ (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'),
1306
+ crypto.randomBytes(4).toString('hex')
1307
+ ) +
1308
+ '@' +
1309
+ // try to use the domain of the FROM address or fallback to server hostname
1310
+ (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
1311
+ '>'
1312
+ );
1313
+ }
1314
+ }
1315
+
1316
+ module.exports = MimeNode;