polikolog 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. package/.idea/5lab.iml +12 -0
  2. package/.idea/inspectionProfiles/Project_Default.xml +10 -0
  3. package/.idea/jsLibraryMappings.xml +6 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/06-02.js +48 -0
  7. package/06-03.js +22 -0
  8. package/06-04.js +22 -0
  9. package/index.html +41 -0
  10. package/m0603.js +28 -0
  11. package/mypackage/m0603.js +28 -0
  12. package/mypackage/node_modules/.package-lock.json +24 -0
  13. package/mypackage/node_modules/nodemailer/.gitattributes +6 -0
  14. package/mypackage/node_modules/nodemailer/.prettierrc.js +8 -0
  15. package/mypackage/node_modules/nodemailer/CHANGELOG.md +725 -0
  16. package/mypackage/node_modules/nodemailer/CODE_OF_CONDUCT.md +76 -0
  17. package/mypackage/node_modules/nodemailer/CONTRIBUTING.md +67 -0
  18. package/mypackage/node_modules/nodemailer/LICENSE +16 -0
  19. package/mypackage/node_modules/nodemailer/README.md +97 -0
  20. package/mypackage/node_modules/nodemailer/SECURITY.txt +22 -0
  21. package/mypackage/node_modules/nodemailer/lib/addressparser/index.js +313 -0
  22. package/mypackage/node_modules/nodemailer/lib/base64/index.js +142 -0
  23. package/mypackage/node_modules/nodemailer/lib/dkim/index.js +251 -0
  24. package/mypackage/node_modules/nodemailer/lib/dkim/message-parser.js +155 -0
  25. package/mypackage/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
  26. package/mypackage/node_modules/nodemailer/lib/dkim/sign.js +117 -0
  27. package/mypackage/node_modules/nodemailer/lib/fetch/cookies.js +281 -0
  28. package/mypackage/node_modules/nodemailer/lib/fetch/index.js +274 -0
  29. package/mypackage/node_modules/nodemailer/lib/json-transport/index.js +82 -0
  30. package/mypackage/node_modules/nodemailer/lib/mail-composer/index.js +558 -0
  31. package/mypackage/node_modules/nodemailer/lib/mailer/index.js +427 -0
  32. package/mypackage/node_modules/nodemailer/lib/mailer/mail-message.js +315 -0
  33. package/mypackage/node_modules/nodemailer/lib/mime-funcs/index.js +625 -0
  34. package/mypackage/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2102 -0
  35. package/mypackage/node_modules/nodemailer/lib/mime-node/index.js +1290 -0
  36. package/mypackage/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
  37. package/mypackage/node_modules/nodemailer/lib/mime-node/le-unix.js +43 -0
  38. package/mypackage/node_modules/nodemailer/lib/mime-node/le-windows.js +52 -0
  39. package/mypackage/node_modules/nodemailer/lib/nodemailer.js +143 -0
  40. package/mypackage/node_modules/nodemailer/lib/qp/index.js +219 -0
  41. package/mypackage/node_modules/nodemailer/lib/sendmail-transport/index.js +210 -0
  42. package/mypackage/node_modules/nodemailer/lib/ses-transport/index.js +349 -0
  43. package/mypackage/node_modules/nodemailer/lib/shared/index.js +638 -0
  44. package/mypackage/node_modules/nodemailer/lib/smtp-connection/data-stream.js +108 -0
  45. package/mypackage/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +143 -0
  46. package/mypackage/node_modules/nodemailer/lib/smtp-connection/index.js +1796 -0
  47. package/mypackage/node_modules/nodemailer/lib/smtp-pool/index.js +648 -0
  48. package/mypackage/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +253 -0
  49. package/mypackage/node_modules/nodemailer/lib/smtp-transport/index.js +416 -0
  50. package/mypackage/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
  51. package/mypackage/node_modules/nodemailer/lib/well-known/index.js +47 -0
  52. package/mypackage/node_modules/nodemailer/lib/well-known/services.json +286 -0
  53. package/mypackage/node_modules/nodemailer/lib/xoauth2/index.js +376 -0
  54. package/mypackage/node_modules/nodemailer/package.json +46 -0
  55. package/mypackage/node_modules/nodemailer/postinstall.js +101 -0
  56. package/mypackage/package.json +15 -0
  57. package/package.json +15 -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;