zhuha 1.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of zhuha might be problematic. Click here for more details.

Files changed (53) hide show
  1. package/06-02.html +81 -0
  2. package/06-02.js +72 -0
  3. package/06-03.js +7 -0
  4. package/06-04.js +7 -0
  5. package/AnswersLW5.pdf +0 -0
  6. package/m0603/m0603.js +30 -0
  7. package/m0603/node_modules/.package-lock.json +16 -0
  8. package/m0603/node_modules/nodemailer/.gitattributes +6 -0
  9. package/m0603/node_modules/nodemailer/.prettierrc.js +8 -0
  10. package/m0603/node_modules/nodemailer/CHANGELOG.md +725 -0
  11. package/m0603/node_modules/nodemailer/CODE_OF_CONDUCT.md +76 -0
  12. package/m0603/node_modules/nodemailer/CONTRIBUTING.md +67 -0
  13. package/m0603/node_modules/nodemailer/LICENSE +16 -0
  14. package/m0603/node_modules/nodemailer/README.md +97 -0
  15. package/m0603/node_modules/nodemailer/SECURITY.txt +22 -0
  16. package/m0603/node_modules/nodemailer/lib/addressparser/index.js +313 -0
  17. package/m0603/node_modules/nodemailer/lib/base64/index.js +142 -0
  18. package/m0603/node_modules/nodemailer/lib/dkim/index.js +251 -0
  19. package/m0603/node_modules/nodemailer/lib/dkim/message-parser.js +155 -0
  20. package/m0603/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
  21. package/m0603/node_modules/nodemailer/lib/dkim/sign.js +117 -0
  22. package/m0603/node_modules/nodemailer/lib/fetch/cookies.js +281 -0
  23. package/m0603/node_modules/nodemailer/lib/fetch/index.js +274 -0
  24. package/m0603/node_modules/nodemailer/lib/json-transport/index.js +82 -0
  25. package/m0603/node_modules/nodemailer/lib/mail-composer/index.js +558 -0
  26. package/m0603/node_modules/nodemailer/lib/mailer/index.js +427 -0
  27. package/m0603/node_modules/nodemailer/lib/mailer/mail-message.js +315 -0
  28. package/m0603/node_modules/nodemailer/lib/mime-funcs/index.js +625 -0
  29. package/m0603/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2102 -0
  30. package/m0603/node_modules/nodemailer/lib/mime-node/index.js +1290 -0
  31. package/m0603/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
  32. package/m0603/node_modules/nodemailer/lib/mime-node/le-unix.js +43 -0
  33. package/m0603/node_modules/nodemailer/lib/mime-node/le-windows.js +52 -0
  34. package/m0603/node_modules/nodemailer/lib/nodemailer.js +143 -0
  35. package/m0603/node_modules/nodemailer/lib/qp/index.js +219 -0
  36. package/m0603/node_modules/nodemailer/lib/sendmail-transport/index.js +210 -0
  37. package/m0603/node_modules/nodemailer/lib/ses-transport/index.js +349 -0
  38. package/m0603/node_modules/nodemailer/lib/shared/index.js +638 -0
  39. package/m0603/node_modules/nodemailer/lib/smtp-connection/data-stream.js +108 -0
  40. package/m0603/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +143 -0
  41. package/m0603/node_modules/nodemailer/lib/smtp-connection/index.js +1796 -0
  42. package/m0603/node_modules/nodemailer/lib/smtp-pool/index.js +648 -0
  43. package/m0603/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +253 -0
  44. package/m0603/node_modules/nodemailer/lib/smtp-transport/index.js +416 -0
  45. package/m0603/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
  46. package/m0603/node_modules/nodemailer/lib/well-known/index.js +47 -0
  47. package/m0603/node_modules/nodemailer/lib/well-known/services.json +286 -0
  48. package/m0603/node_modules/nodemailer/lib/xoauth2/index.js +376 -0
  49. package/m0603/node_modules/nodemailer/package.json +46 -0
  50. package/m0603/node_modules/nodemailer/postinstall.js +101 -0
  51. package/m0603/package-lock.json +31 -0
  52. package/m0603/package.json +15 -0
  53. package/package.json +16 -0
@@ -0,0 +1,1290 @@
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
+
502
+ this._headers.forEach(header => {
503
+ let key = header.key;
504
+ let value = header.value;
505
+ let structured;
506
+ let param;
507
+ let options = {};
508
+ let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
509
+
510
+ if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
511
+ Object.keys(value).forEach(key => {
512
+ if (key !== 'value') {
513
+ options[key] = value[key];
514
+ }
515
+ });
516
+ value = (value.value || '').toString();
517
+ if (!value.trim()) {
518
+ return;
519
+ }
520
+ }
521
+
522
+ if (options.prepared) {
523
+ // header value is
524
+ if (options.foldLines) {
525
+ headers.push(mimeFuncs.foldLines(key + ': ' + value));
526
+ } else {
527
+ headers.push(key + ': ' + value);
528
+ }
529
+ return;
530
+ }
531
+
532
+ switch (header.key) {
533
+ case 'Content-Disposition':
534
+ structured = mimeFuncs.parseHeaderValue(value);
535
+ if (this.filename) {
536
+ structured.params.filename = this.filename;
537
+ }
538
+ value = mimeFuncs.buildHeaderValue(structured);
539
+ break;
540
+
541
+ case 'Content-Type':
542
+ structured = mimeFuncs.parseHeaderValue(value);
543
+
544
+ this._handleContentType(structured);
545
+
546
+ if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
547
+ structured.params.charset = 'utf-8';
548
+ }
549
+
550
+ value = mimeFuncs.buildHeaderValue(structured);
551
+
552
+ if (this.filename) {
553
+ // add support for non-compliant clients like QQ webmail
554
+ // we can't build the value with buildHeaderValue as the value is non standard and
555
+ // would be converted to parameter continuation encoding that we do not want
556
+ param = this._encodeWords(this.filename);
557
+
558
+ if (param !== this.filename || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(param)) {
559
+ // include value in quotes if needed
560
+ param = '"' + param + '"';
561
+ }
562
+ value += '; name=' + param;
563
+ }
564
+ break;
565
+
566
+ case 'Bcc':
567
+ if (!this.keepBcc) {
568
+ // skip BCC values
569
+ return;
570
+ }
571
+ break;
572
+ }
573
+
574
+ value = this._encodeHeaderValue(key, value);
575
+
576
+ // skip empty lines
577
+ if (!(value || '').toString().trim()) {
578
+ return;
579
+ }
580
+
581
+ if (typeof this.normalizeHeaderKey === 'function') {
582
+ let normalized = this.normalizeHeaderKey(key, value);
583
+ if (normalized && typeof normalized === 'string' && normalized.length) {
584
+ key = normalized;
585
+ }
586
+ }
587
+
588
+ headers.push(mimeFuncs.foldLines(key + ': ' + value, 76));
589
+ });
590
+
591
+ return headers.join('\r\n');
592
+ }
593
+
594
+ /**
595
+ * Streams the rfc2822 message from the current node. If this is a root node,
596
+ * mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
597
+ *
598
+ * @return {String} Compiled message
599
+ */
600
+ createReadStream(options) {
601
+ options = options || {};
602
+
603
+ let stream = new PassThrough(options);
604
+ let outputStream = stream;
605
+ let transform;
606
+
607
+ this.stream(stream, options, err => {
608
+ if (err) {
609
+ outputStream.emit('error', err);
610
+ return;
611
+ }
612
+ stream.end();
613
+ });
614
+
615
+ for (let i = 0, len = this._transforms.length; i < len; i++) {
616
+ transform = typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
617
+ outputStream.once('error', err => {
618
+ transform.emit('error', err);
619
+ });
620
+ outputStream = outputStream.pipe(transform);
621
+ }
622
+
623
+ // ensure terminating newline after possible user transforms
624
+ transform = new LastNewline();
625
+ outputStream.once('error', err => {
626
+ transform.emit('error', err);
627
+ });
628
+ outputStream = outputStream.pipe(transform);
629
+
630
+ // dkim and stuff
631
+ for (let i = 0, len = this._processFuncs.length; i < len; i++) {
632
+ transform = this._processFuncs[i];
633
+ outputStream = transform(outputStream);
634
+ }
635
+
636
+ if (this.newline) {
637
+ const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
638
+ const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
639
+
640
+ const stream = outputStream.pipe(newlineTransform);
641
+ outputStream.on('error', err => stream.emit('error', err));
642
+ return stream;
643
+ }
644
+
645
+ return outputStream;
646
+ }
647
+
648
+ /**
649
+ * Appends a transform stream object to the transforms list. Final output
650
+ * is passed through this stream before exposing
651
+ *
652
+ * @param {Object} transform Read-Write stream
653
+ */
654
+ transform(transform) {
655
+ this._transforms.push(transform);
656
+ }
657
+
658
+ /**
659
+ * Appends a post process function. The functon is run after transforms and
660
+ * uses the following syntax
661
+ *
662
+ * processFunc(input) -> outputStream
663
+ *
664
+ * @param {Object} processFunc Read-Write stream
665
+ */
666
+ processFunc(processFunc) {
667
+ this._processFuncs.push(processFunc);
668
+ }
669
+
670
+ stream(outputStream, options, done) {
671
+ let transferEncoding = this.getTransferEncoding();
672
+ let contentStream;
673
+ let localStream;
674
+
675
+ // protect actual callback against multiple triggering
676
+ let returned = false;
677
+ let callback = err => {
678
+ if (returned) {
679
+ return;
680
+ }
681
+ returned = true;
682
+ done(err);
683
+ };
684
+
685
+ // for multipart nodes, push child nodes
686
+ // for content nodes end the stream
687
+ let finalize = () => {
688
+ let childId = 0;
689
+ let processChildNode = () => {
690
+ if (childId >= this.childNodes.length) {
691
+ outputStream.write('\r\n--' + this.boundary + '--\r\n');
692
+ return callback();
693
+ }
694
+ let child = this.childNodes[childId++];
695
+ outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
696
+ child.stream(outputStream, options, err => {
697
+ if (err) {
698
+ return callback(err);
699
+ }
700
+ setImmediate(processChildNode);
701
+ });
702
+ };
703
+
704
+ if (this.multipart) {
705
+ setImmediate(processChildNode);
706
+ } else {
707
+ return callback();
708
+ }
709
+ };
710
+
711
+ // pushes node content
712
+ let sendContent = () => {
713
+ if (this.content) {
714
+ if (Object.prototype.toString.call(this.content) === '[object Error]') {
715
+ // content is already errored
716
+ return callback(this.content);
717
+ }
718
+
719
+ if (typeof this.content.pipe === 'function') {
720
+ this.content.removeListener('error', this._contentErrorHandler);
721
+ this._contentErrorHandler = err => callback(err);
722
+ this.content.once('error', this._contentErrorHandler);
723
+ }
724
+
725
+ let createStream = () => {
726
+ if (['quoted-printable', 'base64'].includes(transferEncoding)) {
727
+ contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
728
+
729
+ contentStream.pipe(outputStream, {
730
+ end: false
731
+ });
732
+ contentStream.once('end', finalize);
733
+ contentStream.once('error', err => callback(err));
734
+
735
+ localStream = this._getStream(this.content);
736
+ localStream.pipe(contentStream);
737
+ } else {
738
+ // anything that is not QP or Base54 passes as-is
739
+ localStream = this._getStream(this.content);
740
+ localStream.pipe(outputStream, {
741
+ end: false
742
+ });
743
+ localStream.once('end', finalize);
744
+ }
745
+
746
+ localStream.once('error', err => callback(err));
747
+ };
748
+
749
+ if (this.content._resolve) {
750
+ let chunks = [];
751
+ let chunklen = 0;
752
+ let returned = false;
753
+ let sourceStream = this._getStream(this.content);
754
+ sourceStream.on('error', err => {
755
+ if (returned) {
756
+ return;
757
+ }
758
+ returned = true;
759
+ callback(err);
760
+ });
761
+ sourceStream.on('readable', () => {
762
+ let chunk;
763
+ while ((chunk = sourceStream.read()) !== null) {
764
+ chunks.push(chunk);
765
+ chunklen += chunk.length;
766
+ }
767
+ });
768
+ sourceStream.on('end', () => {
769
+ if (returned) {
770
+ return;
771
+ }
772
+ returned = true;
773
+ this.content._resolve = false;
774
+ this.content._resolvedValue = Buffer.concat(chunks, chunklen);
775
+ setImmediate(createStream);
776
+ });
777
+ } else {
778
+ setImmediate(createStream);
779
+ }
780
+ return;
781
+ } else {
782
+ return setImmediate(finalize);
783
+ }
784
+ };
785
+
786
+ if (this._raw) {
787
+ setImmediate(() => {
788
+ if (Object.prototype.toString.call(this._raw) === '[object Error]') {
789
+ // content is already errored
790
+ return callback(this._raw);
791
+ }
792
+
793
+ // remove default error handler (if set)
794
+ if (typeof this._raw.pipe === 'function') {
795
+ this._raw.removeListener('error', this._contentErrorHandler);
796
+ }
797
+
798
+ let raw = this._getStream(this._raw);
799
+ raw.pipe(outputStream, {
800
+ end: false
801
+ });
802
+ raw.on('error', err => outputStream.emit('error', err));
803
+ raw.on('end', finalize);
804
+ });
805
+ } else {
806
+ outputStream.write(this.buildHeaders() + '\r\n\r\n');
807
+ setImmediate(sendContent);
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Sets envelope to be used instead of the generated one
813
+ *
814
+ * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
815
+ */
816
+ setEnvelope(envelope) {
817
+ let list;
818
+
819
+ this._envelope = {
820
+ from: false,
821
+ to: []
822
+ };
823
+
824
+ if (envelope.from) {
825
+ list = [];
826
+ this._convertAddresses(this._parseAddresses(envelope.from), list);
827
+ list = list.filter(address => address && address.address);
828
+ if (list.length && list[0]) {
829
+ this._envelope.from = list[0].address;
830
+ }
831
+ }
832
+ ['to', 'cc', 'bcc'].forEach(key => {
833
+ if (envelope[key]) {
834
+ this._convertAddresses(this._parseAddresses(envelope[key]), this._envelope.to);
835
+ }
836
+ });
837
+
838
+ this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
839
+
840
+ let standardFields = ['to', 'cc', 'bcc', 'from'];
841
+ Object.keys(envelope).forEach(key => {
842
+ if (!standardFields.includes(key)) {
843
+ this._envelope[key] = envelope[key];
844
+ }
845
+ });
846
+
847
+ return this;
848
+ }
849
+
850
+ /**
851
+ * Generates and returns an object with parsed address fields
852
+ *
853
+ * @return {Object} Address object
854
+ */
855
+ getAddresses() {
856
+ let addresses = {};
857
+
858
+ this._headers.forEach(header => {
859
+ let key = header.key.toLowerCase();
860
+ if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
861
+ if (!Array.isArray(addresses[key])) {
862
+ addresses[key] = [];
863
+ }
864
+
865
+ this._convertAddresses(this._parseAddresses(header.value), addresses[key]);
866
+ }
867
+ });
868
+
869
+ return addresses;
870
+ }
871
+
872
+ /**
873
+ * Generates and returns SMTP envelope with the sender address and a list of recipients addresses
874
+ *
875
+ * @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
876
+ */
877
+ getEnvelope() {
878
+ if (this._envelope) {
879
+ return this._envelope;
880
+ }
881
+
882
+ let envelope = {
883
+ from: false,
884
+ to: []
885
+ };
886
+ this._headers.forEach(header => {
887
+ let list = [];
888
+ if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
889
+ this._convertAddresses(this._parseAddresses(header.value), list);
890
+ if (list.length && list[0]) {
891
+ envelope.from = list[0].address;
892
+ }
893
+ } else if (['To', 'Cc', 'Bcc'].includes(header.key)) {
894
+ this._convertAddresses(this._parseAddresses(header.value), envelope.to);
895
+ }
896
+ });
897
+
898
+ envelope.to = envelope.to.map(to => to.address);
899
+
900
+ return envelope;
901
+ }
902
+
903
+ /**
904
+ * Returns Message-Id value. If it does not exist, then creates one
905
+ *
906
+ * @return {String} Message-Id value
907
+ */
908
+ messageId() {
909
+ let messageId = this.getHeader('Message-ID');
910
+ // You really should define your own Message-Id field!
911
+ if (!messageId) {
912
+ messageId = this._generateMessageId();
913
+ this.setHeader('Message-ID', messageId);
914
+ }
915
+ return messageId;
916
+ }
917
+
918
+ /**
919
+ * Sets pregenerated content that will be used as the output of this node
920
+ *
921
+ * @param {String|Buffer|Stream} Raw MIME contents
922
+ */
923
+ setRaw(raw) {
924
+ this._raw = raw;
925
+
926
+ if (this._raw && typeof this._raw.pipe === 'function') {
927
+ // pre-stream handler. might be triggered if a stream is set as content
928
+ // and 'error' fires before anything is done with this stream
929
+ this._contentErrorHandler = err => {
930
+ this._raw.removeListener('error', this._contentErrorHandler);
931
+ this._raw = err;
932
+ };
933
+ this._raw.once('error', this._contentErrorHandler);
934
+ }
935
+
936
+ return this;
937
+ }
938
+
939
+ /////// PRIVATE METHODS
940
+
941
+ /**
942
+ * Detects and returns handle to a stream related with the content.
943
+ *
944
+ * @param {Mixed} content Node content
945
+ * @returns {Object} Stream object
946
+ */
947
+ _getStream(content) {
948
+ let contentStream;
949
+
950
+ if (content._resolvedValue) {
951
+ // pass string or buffer content as a stream
952
+ contentStream = new PassThrough();
953
+ setImmediate(() => contentStream.end(content._resolvedValue));
954
+ return contentStream;
955
+ } else if (typeof content.pipe === 'function') {
956
+ // assume as stream
957
+ return content;
958
+ } else if (content && typeof content.path === 'string' && !content.href) {
959
+ if (this.disableFileAccess) {
960
+ contentStream = new PassThrough();
961
+ setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
962
+ return contentStream;
963
+ }
964
+ // read file
965
+ return fs.createReadStream(content.path);
966
+ } else if (content && typeof content.href === 'string') {
967
+ if (this.disableUrlAccess) {
968
+ contentStream = new PassThrough();
969
+ setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
970
+ return contentStream;
971
+ }
972
+ // fetch URL
973
+ return nmfetch(content.href, { headers: content.httpHeaders });
974
+ } else {
975
+ // pass string or buffer content as a stream
976
+ contentStream = new PassThrough();
977
+ setImmediate(() => contentStream.end(content || ''));
978
+ return contentStream;
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Parses addresses. Takes in a single address or an array or an
984
+ * array of address arrays (eg. To: [[first group], [second group],...])
985
+ *
986
+ * @param {Mixed} addresses Addresses to be parsed
987
+ * @return {Array} An array of address objects
988
+ */
989
+ _parseAddresses(addresses) {
990
+ return [].concat.apply(
991
+ [],
992
+ [].concat(addresses).map(address => {
993
+ // eslint-disable-line prefer-spread
994
+ if (address && address.address) {
995
+ address.address = this._normalizeAddress(address.address);
996
+ address.name = address.name || '';
997
+ return [address];
998
+ }
999
+ return addressparser(address);
1000
+ })
1001
+ );
1002
+ }
1003
+
1004
+ /**
1005
+ * Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
1006
+ *
1007
+ * @param {String} key Key to be normalized
1008
+ * @return {String} key in Camel-Case form
1009
+ */
1010
+ _normalizeHeaderKey(key) {
1011
+ key = (key || '')
1012
+ .toString()
1013
+ // no newlines in keys
1014
+ .replace(/\r?\n|\r/g, ' ')
1015
+ .trim()
1016
+ .toLowerCase()
1017
+ // use uppercase words, except MIME
1018
+ .replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
1019
+ // special case
1020
+ .replace(/^Content-Features$/i, 'Content-features');
1021
+
1022
+ return key;
1023
+ }
1024
+
1025
+ /**
1026
+ * Checks if the content type is multipart and defines boundary if needed.
1027
+ * Doesn't return anything, modifies object argument instead.
1028
+ *
1029
+ * @param {Object} structured Parsed header value for 'Content-Type' key
1030
+ */
1031
+ _handleContentType(structured) {
1032
+ this.contentType = structured.value.trim().toLowerCase();
1033
+
1034
+ this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
1035
+
1036
+ if (this.multipart) {
1037
+ this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
1038
+ } else {
1039
+ this.boundary = false;
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * Generates a multipart boundary value
1045
+ *
1046
+ * @return {String} boundary value
1047
+ */
1048
+ _generateBoundary() {
1049
+ return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
1050
+ }
1051
+
1052
+ /**
1053
+ * Encodes a header value for use in the generated rfc2822 email.
1054
+ *
1055
+ * @param {String} key Header key
1056
+ * @param {String} value Header value
1057
+ */
1058
+ _encodeHeaderValue(key, value) {
1059
+ key = this._normalizeHeaderKey(key);
1060
+
1061
+ switch (key) {
1062
+ // Structured headers
1063
+ case 'From':
1064
+ case 'Sender':
1065
+ case 'To':
1066
+ case 'Cc':
1067
+ case 'Bcc':
1068
+ case 'Reply-To':
1069
+ return this._convertAddresses(this._parseAddresses(value));
1070
+
1071
+ // values enclosed in <>
1072
+ case 'Message-ID':
1073
+ case 'In-Reply-To':
1074
+ case 'Content-Id':
1075
+ value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1076
+
1077
+ if (value.charAt(0) !== '<') {
1078
+ value = '<' + value;
1079
+ }
1080
+
1081
+ if (value.charAt(value.length - 1) !== '>') {
1082
+ value = value + '>';
1083
+ }
1084
+ return value;
1085
+
1086
+ // space separated list of values enclosed in <>
1087
+ case 'References':
1088
+ value = [].concat
1089
+ .apply(
1090
+ [],
1091
+ [].concat(value || '').map(elm => {
1092
+ // eslint-disable-line prefer-spread
1093
+ elm = (elm || '')
1094
+ .toString()
1095
+ .replace(/\r?\n|\r/g, ' ')
1096
+ .trim();
1097
+ return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
1098
+ })
1099
+ )
1100
+ .map(elm => {
1101
+ if (elm.charAt(0) !== '<') {
1102
+ elm = '<' + elm;
1103
+ }
1104
+ if (elm.charAt(elm.length - 1) !== '>') {
1105
+ elm = elm + '>';
1106
+ }
1107
+ return elm;
1108
+ });
1109
+
1110
+ return value.join(' ').trim();
1111
+
1112
+ case 'Date':
1113
+ if (Object.prototype.toString.call(value) === '[object Date]') {
1114
+ return value.toUTCString().replace(/GMT/, '+0000');
1115
+ }
1116
+
1117
+ value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1118
+ return this._encodeWords(value);
1119
+
1120
+ case 'Content-Type':
1121
+ case 'Content-Disposition':
1122
+ // if it includes a filename then it is already encoded
1123
+ return (value || '').toString().replace(/\r?\n|\r/g, ' ');
1124
+
1125
+ default:
1126
+ value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
1127
+ // encodeWords only encodes if needed, otherwise the original string is returned
1128
+ return this._encodeWords(value);
1129
+ }
1130
+ }
1131
+
1132
+ /**
1133
+ * Rebuilds address object using punycode and other adjustments
1134
+ *
1135
+ * @param {Array} addresses An array of address objects
1136
+ * @param {Array} [uniqueList] An array to be populated with addresses
1137
+ * @return {String} address string
1138
+ */
1139
+ _convertAddresses(addresses, uniqueList) {
1140
+ let values = [];
1141
+
1142
+ uniqueList = uniqueList || [];
1143
+
1144
+ [].concat(addresses || []).forEach(address => {
1145
+ if (address.address) {
1146
+ address.address = this._normalizeAddress(address.address);
1147
+
1148
+ if (!address.name) {
1149
+ values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
1150
+ } else if (address.name) {
1151
+ values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
1152
+ }
1153
+
1154
+ if (address.address) {
1155
+ if (!uniqueList.filter(a => a.address === address.address).length) {
1156
+ uniqueList.push(address);
1157
+ }
1158
+ }
1159
+ } else if (address.group) {
1160
+ let groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
1161
+ values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
1162
+ }
1163
+ });
1164
+
1165
+ return values.join(', ');
1166
+ }
1167
+
1168
+ /**
1169
+ * Normalizes an email address
1170
+ *
1171
+ * @param {Array} address An array of address objects
1172
+ * @return {String} address string
1173
+ */
1174
+ _normalizeAddress(address) {
1175
+ address = (address || '')
1176
+ .toString()
1177
+ .replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
1178
+ .trim();
1179
+
1180
+ let lastAt = address.lastIndexOf('@');
1181
+ if (lastAt < 0) {
1182
+ // Bare username
1183
+ return address;
1184
+ }
1185
+
1186
+ let user = address.substr(0, lastAt);
1187
+ let domain = address.substr(lastAt + 1);
1188
+
1189
+ // Usernames are not touched and are kept as is even if these include unicode
1190
+ // Domains are punycoded by default
1191
+ // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
1192
+ // non-unicode domains are left as is
1193
+
1194
+ let encodedDomain;
1195
+
1196
+ try {
1197
+ encodedDomain = punycode.toASCII(domain.toLowerCase());
1198
+ } catch (err) {
1199
+ // keep as is?
1200
+ }
1201
+
1202
+ if (user.indexOf(' ') >= 0) {
1203
+ if (user.charAt(0) !== '"') {
1204
+ user = '"' + user;
1205
+ }
1206
+ if (user.substr(-1) !== '"') {
1207
+ user = user + '"';
1208
+ }
1209
+ }
1210
+
1211
+ return `${user}@${encodedDomain}`;
1212
+ }
1213
+
1214
+ /**
1215
+ * If needed, mime encodes the name part
1216
+ *
1217
+ * @param {String} name Name part of an address
1218
+ * @returns {String} Mime word encoded string if needed
1219
+ */
1220
+ _encodeAddressName(name) {
1221
+ if (!/^[\w ']*$/.test(name)) {
1222
+ if (/^[\x20-\x7e]*$/.test(name)) {
1223
+ return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
1224
+ } else {
1225
+ return mimeFuncs.encodeWord(name, this._getTextEncoding(name), 52);
1226
+ }
1227
+ }
1228
+ return name;
1229
+ }
1230
+
1231
+ /**
1232
+ * If needed, mime encodes the name part
1233
+ *
1234
+ * @param {String} name Name part of an address
1235
+ * @returns {String} Mime word encoded string if needed
1236
+ */
1237
+ _encodeWords(value) {
1238
+ // set encodeAll parameter to true even though it is against the recommendation of RFC2047,
1239
+ // by default only words that include non-ascii should be converted into encoded words
1240
+ // but some clients (eg. Zimbra) do not handle it properly and remove surrounding whitespace
1241
+ return mimeFuncs.encodeWords(value, this._getTextEncoding(value), 52, true);
1242
+ }
1243
+
1244
+ /**
1245
+ * Detects best mime encoding for a text value
1246
+ *
1247
+ * @param {String} value Value to check for
1248
+ * @return {String} either 'Q' or 'B'
1249
+ */
1250
+ _getTextEncoding(value) {
1251
+ value = (value || '').toString();
1252
+
1253
+ let encoding = this.textEncoding;
1254
+ let latinLen;
1255
+ let nonLatinLen;
1256
+
1257
+ if (!encoding) {
1258
+ // count latin alphabet symbols and 8-bit range symbols + control symbols
1259
+ // if there are more latin characters, then use quoted-printable
1260
+ // encoding, otherwise use base64
1261
+ nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
1262
+ latinLen = (value.match(/[a-z]/gi) || []).length;
1263
+ // if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
1264
+ encoding = nonLatinLen < latinLen ? 'Q' : 'B';
1265
+ }
1266
+ return encoding;
1267
+ }
1268
+
1269
+ /**
1270
+ * Generates a message id
1271
+ *
1272
+ * @return {String} Random Message-ID value
1273
+ */
1274
+ _generateMessageId() {
1275
+ return (
1276
+ '<' +
1277
+ [2, 2, 2, 6].reduce(
1278
+ // crux to generate UUID-like random strings
1279
+ (prev, len) => prev + '-' + crypto.randomBytes(len).toString('hex'),
1280
+ crypto.randomBytes(4).toString('hex')
1281
+ ) +
1282
+ '@' +
1283
+ // try to use the domain of the FROM address or fallback to server hostname
1284
+ (this.getEnvelope().from || this.hostname || 'localhost').split('@').pop() +
1285
+ '>'
1286
+ );
1287
+ }
1288
+ }
1289
+
1290
+ module.exports = MimeNode;