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