@nocobase/plugin-workflow-mailer 1.2.13-alpha → 1.3.0-alpha.20240710084543

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