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