nodemailer 4.1.3 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/lib/base64/index.js +4 -1
- package/lib/dkim/message-parser.js +4 -1
- package/lib/dkim/sign.js +19 -8
- package/lib/fetch/cookies.js +59 -51
- package/lib/fetch/index.js +10 -2
- package/lib/json-transport/index.js +4 -36
- package/lib/mail-composer/index.js +74 -49
- package/lib/mailer/mail-message.js +82 -1
- package/lib/mime-funcs/index.js +13 -3
- package/lib/mime-funcs/mime-types.js +9 -2
- package/lib/mime-node/index.js +17 -4
- package/lib/ses-transport/index.js +7 -1
- package/lib/shared/index.js +13 -2
- package/lib/smtp-connection/http-proxy-client.js +3 -1
- package/lib/smtp-connection/index.js +16 -2
- package/lib/smtp-pool/pool-resource.js +12 -2
- package/lib/smtp-transport/index.js +9 -2
- package/lib/xoauth2/index.js +6 -2
- package/package.json +5 -5
- package/.github/stale.yml +0 -17
- package/.npmignore +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 4.4.0 2017-11-10
|
|
4
|
+
|
|
5
|
+
- Changed default behavior for attachment option contentTransferEncoding. If it is unset then base64 encoding is used for the attachment. If it is set to false then previous default applies (base64 for most, 7bit for text)
|
|
6
|
+
|
|
7
|
+
## 4.3.1 2017-10-25
|
|
8
|
+
|
|
9
|
+
- Fixed a confict with Electron.js where timers do not have unref method
|
|
10
|
+
|
|
11
|
+
## 4.3.0 2017-10-23
|
|
12
|
+
|
|
13
|
+
- Added new mail object method `mail.normalize(cb)` that should make creating HTTP API based transports much easier
|
|
14
|
+
|
|
15
|
+
## 4.2.0 2017-10-13
|
|
16
|
+
|
|
17
|
+
- Expose streamed messages size and timers in info response
|
|
18
|
+
|
|
3
19
|
## v4.1.3 2017-10-06
|
|
4
20
|
|
|
5
21
|
- Allow generating preview links without calling createTestAccount first
|
package/lib/base64/index.js
CHANGED
|
@@ -35,7 +35,10 @@ function wrap(str, lineLength) {
|
|
|
35
35
|
let pos = 0;
|
|
36
36
|
let chunkLength = lineLength * 1024;
|
|
37
37
|
while (pos < str.length) {
|
|
38
|
-
let wrappedLines = str
|
|
38
|
+
let wrappedLines = str
|
|
39
|
+
.substr(pos, chunkLength)
|
|
40
|
+
.replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n')
|
|
41
|
+
.trim();
|
|
39
42
|
result.push(wrappedLines);
|
|
40
43
|
pos += chunkLength;
|
|
41
44
|
}
|
|
@@ -144,7 +144,10 @@ class MessageParser extends Transform {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
return lines.filter(line => line.trim()).map(line => ({
|
|
147
|
-
key: line
|
|
147
|
+
key: line
|
|
148
|
+
.substr(0, line.indexOf(':'))
|
|
149
|
+
.trim()
|
|
150
|
+
.toLowerCase(),
|
|
148
151
|
line
|
|
149
152
|
}));
|
|
150
153
|
}
|
package/lib/dkim/sign.js
CHANGED
|
@@ -70,13 +70,20 @@ function relaxedHeaders(headers, fieldNames, skipFields) {
|
|
|
70
70
|
let skip = new Set();
|
|
71
71
|
let headerFields = new Map();
|
|
72
72
|
|
|
73
|
-
(skipFields || '')
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
(skipFields || '')
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.split(':')
|
|
76
|
+
.forEach(field => {
|
|
77
|
+
skip.add(field.trim());
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
(fieldNames || '')
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.split(':')
|
|
83
|
+
.filter(field => !skip.has(field.trim()))
|
|
84
|
+
.forEach(field => {
|
|
85
|
+
includedFields.add(field.trim());
|
|
86
|
+
});
|
|
80
87
|
|
|
81
88
|
for (let i = headers.length - 1; i >= 0; i--) {
|
|
82
89
|
let line = headers[i];
|
|
@@ -102,5 +109,9 @@ function relaxedHeaders(headers, fieldNames, skipFields) {
|
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
function relaxedHeaderLine(line) {
|
|
105
|
-
return line
|
|
112
|
+
return line
|
|
113
|
+
.substr(line.indexOf(':') + 1)
|
|
114
|
+
.replace(/\r?\n/g, '')
|
|
115
|
+
.replace(/\s+/g, ' ')
|
|
116
|
+
.trim();
|
|
106
117
|
}
|
package/lib/fetch/cookies.js
CHANGED
|
@@ -64,7 +64,9 @@ class Cookies {
|
|
|
64
64
|
* @returns {String} Cookie header or empty string if no matches were found
|
|
65
65
|
*/
|
|
66
66
|
get(url) {
|
|
67
|
-
return this.list(url)
|
|
67
|
+
return this.list(url)
|
|
68
|
+
.map(cookie => cookie.name + '=' + cookie.value)
|
|
69
|
+
.join('; ');
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -103,57 +105,63 @@ class Cookies {
|
|
|
103
105
|
parse(cookieStr) {
|
|
104
106
|
let cookie = {};
|
|
105
107
|
|
|
106
|
-
(cookieStr || '')
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
(cookieStr || '')
|
|
109
|
+
.toString()
|
|
110
|
+
.split(';')
|
|
111
|
+
.forEach(cookiePart => {
|
|
112
|
+
let valueParts = cookiePart.split('=');
|
|
113
|
+
let key = valueParts
|
|
114
|
+
.shift()
|
|
115
|
+
.trim()
|
|
116
|
+
.toLowerCase();
|
|
117
|
+
let value = valueParts.join('=').trim();
|
|
118
|
+
let domain;
|
|
119
|
+
|
|
120
|
+
if (!key) {
|
|
121
|
+
// skip empty parts
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
116
124
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
125
|
+
switch (key) {
|
|
126
|
+
case 'expires':
|
|
127
|
+
value = new Date(value);
|
|
128
|
+
// ignore date if can not parse it
|
|
129
|
+
if (value.toString() !== 'Invalid Date') {
|
|
130
|
+
cookie.expires = value;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'path':
|
|
135
|
+
cookie.path = value;
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case 'domain':
|
|
139
|
+
domain = value.toLowerCase();
|
|
140
|
+
if (domain.length && domain.charAt(0) !== '.') {
|
|
141
|
+
domain = '.' + domain; // ensure preceeding dot for user set domains
|
|
142
|
+
}
|
|
143
|
+
cookie.domain = domain;
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'max-age':
|
|
147
|
+
cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case 'secure':
|
|
151
|
+
cookie.secure = true;
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case 'httponly':
|
|
155
|
+
cookie.httponly = true;
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
default:
|
|
159
|
+
if (!cookie.name) {
|
|
160
|
+
cookie.name = key;
|
|
161
|
+
cookie.value = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
157
165
|
|
|
158
166
|
return cookie;
|
|
159
167
|
}
|
package/lib/fetch/index.js
CHANGED
|
@@ -33,7 +33,11 @@ function fetch(url, options) {
|
|
|
33
33
|
|
|
34
34
|
let fetchRes = options.fetchRes;
|
|
35
35
|
let parsed = urllib.parse(url);
|
|
36
|
-
let method =
|
|
36
|
+
let method =
|
|
37
|
+
(options.method || '')
|
|
38
|
+
.toString()
|
|
39
|
+
.trim()
|
|
40
|
+
.toUpperCase() || 'GET';
|
|
37
41
|
let finished = false;
|
|
38
42
|
let cookies;
|
|
39
43
|
let body;
|
|
@@ -99,7 +103,11 @@ function fetch(url, options) {
|
|
|
99
103
|
headers['Content-Length'] = body.length;
|
|
100
104
|
}
|
|
101
105
|
// if method is not provided, use POST instead of GET
|
|
102
|
-
method =
|
|
106
|
+
method =
|
|
107
|
+
(options.method || '')
|
|
108
|
+
.toString()
|
|
109
|
+
.trim()
|
|
110
|
+
.toUpperCase() || 'POST';
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
let req;
|
|
@@ -57,7 +57,7 @@ class JSONTransport {
|
|
|
57
57
|
);
|
|
58
58
|
|
|
59
59
|
setImmediate(() => {
|
|
60
|
-
mail.
|
|
60
|
+
mail.normalize((err, data) => {
|
|
61
61
|
if (err) {
|
|
62
62
|
this.logger.error(
|
|
63
63
|
{
|
|
@@ -72,43 +72,11 @@ class JSONTransport {
|
|
|
72
72
|
return done(err);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
data.
|
|
76
|
-
|
|
77
|
-
['html', 'text', 'watchHtml'].forEach(key => {
|
|
78
|
-
if (data[key] && data[key].content) {
|
|
79
|
-
if (typeof data[key].content === 'string') {
|
|
80
|
-
data[key] = data[key].content;
|
|
81
|
-
} else if (Buffer.isBuffer(data[key].content)) {
|
|
82
|
-
data[key] = data[key].content.toString();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
|
|
88
|
-
data.icalEvent.content = data.icalEvent.content.toString('base64');
|
|
89
|
-
data.icalEvent.encoding = 'base64';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (data.alternatives && data.alternatives.length) {
|
|
93
|
-
data.alternatives.forEach(alternative => {
|
|
94
|
-
if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
|
|
95
|
-
alternative.content = alternative.content.toString('base64');
|
|
96
|
-
alternative.encoding = 'base64';
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (data.attachments && data.attachments.length) {
|
|
102
|
-
data.attachments.forEach(attachment => {
|
|
103
|
-
if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
|
|
104
|
-
attachment.content = attachment.content.toString('base64');
|
|
105
|
-
attachment.encoding = 'base64';
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
75
|
+
delete data.envelope;
|
|
76
|
+
delete data.normalizedHeaders;
|
|
109
77
|
|
|
110
78
|
return done(null, {
|
|
111
|
-
envelope
|
|
79
|
+
envelope,
|
|
112
80
|
messageId,
|
|
113
81
|
message: JSON.stringify(data)
|
|
114
82
|
});
|
|
@@ -41,7 +41,10 @@ class MailComposer {
|
|
|
41
41
|
} else {
|
|
42
42
|
this.message = this._createContentNode(
|
|
43
43
|
false,
|
|
44
|
-
[]
|
|
44
|
+
[]
|
|
45
|
+
.concat(this._alternatives || [])
|
|
46
|
+
.concat(this._attachments.attached || [])
|
|
47
|
+
.shift() || {
|
|
45
48
|
contentType: 'text/plain',
|
|
46
49
|
content: ''
|
|
47
50
|
}
|
|
@@ -91,13 +94,18 @@ class MailComposer {
|
|
|
91
94
|
data = {
|
|
92
95
|
contentType: attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'),
|
|
93
96
|
contentDisposition: attachment.contentDisposition || (isMessageNode ? 'inline' : 'attachment'),
|
|
94
|
-
contentTransferEncoding: attachment.contentTransferEncoding
|
|
97
|
+
contentTransferEncoding: 'contentTransferEncoding' in attachment ? attachment.contentTransferEncoding : 'base64'
|
|
95
98
|
};
|
|
96
99
|
|
|
97
100
|
if (attachment.filename) {
|
|
98
101
|
data.filename = attachment.filename;
|
|
99
102
|
} else if (!isMessageNode && attachment.filename !== false) {
|
|
100
|
-
data.filename =
|
|
103
|
+
data.filename =
|
|
104
|
+
(attachment.path || attachment.href || '')
|
|
105
|
+
.split('/')
|
|
106
|
+
.pop()
|
|
107
|
+
.split('?')
|
|
108
|
+
.shift() || 'attachment-' + (i + 1);
|
|
101
109
|
if (data.filename.indexOf('.') < 0) {
|
|
102
110
|
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
|
103
111
|
}
|
|
@@ -240,7 +248,12 @@ class MailComposer {
|
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
eventObject.filename = false;
|
|
243
|
-
eventObject.contentType =
|
|
251
|
+
eventObject.contentType =
|
|
252
|
+
'text/calendar; charset="utf-8"; method=' +
|
|
253
|
+
(eventObject.method || 'PUBLISH')
|
|
254
|
+
.toString()
|
|
255
|
+
.trim()
|
|
256
|
+
.toUpperCase();
|
|
244
257
|
if (!eventObject.headers) {
|
|
245
258
|
eventObject.headers = {};
|
|
246
259
|
}
|
|
@@ -257,51 +270,57 @@ class MailComposer {
|
|
|
257
270
|
html.contentType = 'text/html' + (!html.encoding && mimeFuncs.isPlainText(html.content) ? '' : '; charset=utf-8');
|
|
258
271
|
}
|
|
259
272
|
|
|
260
|
-
[]
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
[]
|
|
274
|
+
.concat(text || [])
|
|
275
|
+
.concat(watchHtml || [])
|
|
276
|
+
.concat(html || [])
|
|
277
|
+
.concat(eventObject || [])
|
|
278
|
+
.concat(this.mail.alternatives || [])
|
|
279
|
+
.forEach(alternative => {
|
|
280
|
+
let data;
|
|
281
|
+
|
|
282
|
+
if (/^data:/i.test(alternative.path || alternative.href)) {
|
|
283
|
+
alternative = this._processDataUrl(alternative);
|
|
284
|
+
}
|
|
266
285
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
data = {
|
|
287
|
+
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
|
288
|
+
contentTransferEncoding: alternative.contentTransferEncoding
|
|
289
|
+
};
|
|
271
290
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
291
|
+
if (alternative.filename) {
|
|
292
|
+
data.filename = alternative.filename;
|
|
293
|
+
}
|
|
275
294
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
295
|
+
if (/^https?:\/\//i.test(alternative.path)) {
|
|
296
|
+
alternative.href = alternative.path;
|
|
297
|
+
alternative.path = undefined;
|
|
298
|
+
}
|
|
280
299
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
if (alternative.raw) {
|
|
301
|
+
data.raw = alternative.raw;
|
|
302
|
+
} else if (alternative.path) {
|
|
303
|
+
data.content = {
|
|
304
|
+
path: alternative.path
|
|
305
|
+
};
|
|
306
|
+
} else if (alternative.href) {
|
|
307
|
+
data.content = {
|
|
308
|
+
href: alternative.href
|
|
309
|
+
};
|
|
310
|
+
} else {
|
|
311
|
+
data.content = alternative.content || '';
|
|
312
|
+
}
|
|
294
313
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
314
|
+
if (alternative.encoding) {
|
|
315
|
+
data.encoding = alternative.encoding;
|
|
316
|
+
}
|
|
298
317
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
318
|
+
if (alternative.headers) {
|
|
319
|
+
data.headers = alternative.headers;
|
|
320
|
+
}
|
|
302
321
|
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
alternatives.push(data);
|
|
323
|
+
});
|
|
305
324
|
|
|
306
325
|
return alternatives;
|
|
307
326
|
}
|
|
@@ -337,12 +356,15 @@ class MailComposer {
|
|
|
337
356
|
this._createRelated(node);
|
|
338
357
|
}
|
|
339
358
|
|
|
340
|
-
[]
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
359
|
+
[]
|
|
360
|
+
.concat((!this._useAlternative && this._alternatives) || [])
|
|
361
|
+
.concat(this._attachments.attached || [])
|
|
362
|
+
.forEach(element => {
|
|
363
|
+
// if the element is a html node from related subpart then ignore it
|
|
364
|
+
if (!this._useRelated || element !== this._htmlNode) {
|
|
365
|
+
this._createContentNode(node, element);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
346
368
|
|
|
347
369
|
return node;
|
|
348
370
|
}
|
|
@@ -426,7 +448,10 @@ class MailComposer {
|
|
|
426
448
|
element.content = element.content || '';
|
|
427
449
|
|
|
428
450
|
let node;
|
|
429
|
-
let encoding = (element.encoding || 'utf8')
|
|
451
|
+
let encoding = (element.encoding || 'utf8')
|
|
452
|
+
.toString()
|
|
453
|
+
.toLowerCase()
|
|
454
|
+
.replace(/[-_\s]/g, '');
|
|
430
455
|
|
|
431
456
|
if (!parentNode) {
|
|
432
457
|
node = new MimeNode(element.contentType, {
|
|
@@ -58,7 +58,12 @@ class MailMessage {
|
|
|
58
58
|
if (this.data.attachments && this.data.attachments.length) {
|
|
59
59
|
this.data.attachments.forEach((attachment, i) => {
|
|
60
60
|
if (!attachment.filename) {
|
|
61
|
-
attachment.filename =
|
|
61
|
+
attachment.filename =
|
|
62
|
+
(attachment.path || attachment.href || '')
|
|
63
|
+
.split('/')
|
|
64
|
+
.pop()
|
|
65
|
+
.split('?')
|
|
66
|
+
.shift() || 'attachment-' + (i + 1);
|
|
62
67
|
if (attachment.filename.indexOf('.') < 0) {
|
|
63
68
|
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
|
64
69
|
}
|
|
@@ -130,6 +135,82 @@ class MailMessage {
|
|
|
130
135
|
setImmediate(() => resolveNext());
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
normalize(callback) {
|
|
139
|
+
let envelope = this.data.envelope || this.message.getEnvelope();
|
|
140
|
+
let messageId = this.message.messageId();
|
|
141
|
+
|
|
142
|
+
this.resolveAll((err, data) => {
|
|
143
|
+
if (err) {
|
|
144
|
+
return callback(err);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
data.envelope = envelope;
|
|
148
|
+
data.messageId = messageId;
|
|
149
|
+
|
|
150
|
+
['html', 'text', 'watchHtml'].forEach(key => {
|
|
151
|
+
if (data[key] && data[key].content) {
|
|
152
|
+
if (typeof data[key].content === 'string') {
|
|
153
|
+
data[key] = data[key].content;
|
|
154
|
+
} else if (Buffer.isBuffer(data[key].content)) {
|
|
155
|
+
data[key] = data[key].content.toString();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
|
|
161
|
+
data.icalEvent.content = data.icalEvent.content.toString('base64');
|
|
162
|
+
data.icalEvent.encoding = 'base64';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (data.alternatives && data.alternatives.length) {
|
|
166
|
+
data.alternatives.forEach(alternative => {
|
|
167
|
+
if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
|
|
168
|
+
alternative.content = alternative.content.toString('base64');
|
|
169
|
+
alternative.encoding = 'base64';
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (data.attachments && data.attachments.length) {
|
|
175
|
+
data.attachments.forEach(attachment => {
|
|
176
|
+
if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
|
|
177
|
+
attachment.content = attachment.content.toString('base64');
|
|
178
|
+
attachment.encoding = 'base64';
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
data.normalizedHeaders = {};
|
|
184
|
+
Object.keys(data.headers || {}).forEach(key => {
|
|
185
|
+
let value = [].concat(data.headers[key] || []).shift();
|
|
186
|
+
value = (value && value.value) || value;
|
|
187
|
+
if (value) {
|
|
188
|
+
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
|
189
|
+
value = this.message._encodeHeaderValue(key, value);
|
|
190
|
+
}
|
|
191
|
+
data.normalizedHeaders[key] = value;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (data.list && typeof data.list === 'object') {
|
|
196
|
+
let listHeaders = this._getListHeaders(data.list);
|
|
197
|
+
listHeaders.forEach(entry => {
|
|
198
|
+
data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (data.references) {
|
|
203
|
+
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (data.inReplyTo) {
|
|
207
|
+
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return callback(null, data);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
133
214
|
setMailerHeader() {
|
|
134
215
|
if (!this.message || !this.data.xMailer) {
|
|
135
216
|
return;
|
package/lib/mime-funcs/index.js
CHANGED
|
@@ -49,7 +49,11 @@ module.exports = {
|
|
|
49
49
|
* @return {String} Single or several mime words joined together
|
|
50
50
|
*/
|
|
51
51
|
encodeWord(data, mimeWordEncoding, maxLength) {
|
|
52
|
-
mimeWordEncoding = (mimeWordEncoding || 'Q')
|
|
52
|
+
mimeWordEncoding = (mimeWordEncoding || 'Q')
|
|
53
|
+
.toString()
|
|
54
|
+
.toUpperCase()
|
|
55
|
+
.trim()
|
|
56
|
+
.charAt(0);
|
|
53
57
|
maxLength = maxLength || 0;
|
|
54
58
|
|
|
55
59
|
let encodedStr;
|
|
@@ -62,7 +66,10 @@ module.exports = {
|
|
|
62
66
|
if (mimeWordEncoding === 'Q') {
|
|
63
67
|
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
|
|
64
68
|
encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => {
|
|
65
|
-
let ord = chr
|
|
69
|
+
let ord = chr
|
|
70
|
+
.charCodeAt(0)
|
|
71
|
+
.toString(16)
|
|
72
|
+
.toUpperCase();
|
|
66
73
|
if (chr === ' ') {
|
|
67
74
|
return '_';
|
|
68
75
|
} else {
|
|
@@ -575,7 +582,10 @@ module.exports = {
|
|
|
575
582
|
|
|
576
583
|
encodeURICharComponent: chr => {
|
|
577
584
|
let res = '';
|
|
578
|
-
let ord = chr
|
|
585
|
+
let ord = chr
|
|
586
|
+
.charCodeAt(0)
|
|
587
|
+
.toString(16)
|
|
588
|
+
.toUpperCase();
|
|
579
589
|
|
|
580
590
|
if (ord.length % 2) {
|
|
581
591
|
ord = '0' + ord;
|
|
@@ -2063,7 +2063,11 @@ module.exports = {
|
|
|
2063
2063
|
}
|
|
2064
2064
|
|
|
2065
2065
|
let parsed = path.parse(filename);
|
|
2066
|
-
let extension = (parsed.ext.substr(1) || parsed.name || '')
|
|
2066
|
+
let extension = (parsed.ext.substr(1) || parsed.name || '')
|
|
2067
|
+
.split('?')
|
|
2068
|
+
.shift()
|
|
2069
|
+
.trim()
|
|
2070
|
+
.toLowerCase();
|
|
2067
2071
|
let value = defaultMimeType;
|
|
2068
2072
|
|
|
2069
2073
|
if (extensions.has(extension)) {
|
|
@@ -2080,7 +2084,10 @@ module.exports = {
|
|
|
2080
2084
|
if (!mimeType) {
|
|
2081
2085
|
return defaultExtension;
|
|
2082
2086
|
}
|
|
2083
|
-
let parts = (mimeType || '')
|
|
2087
|
+
let parts = (mimeType || '')
|
|
2088
|
+
.toLowerCase()
|
|
2089
|
+
.trim()
|
|
2090
|
+
.split('/');
|
|
2084
2091
|
let rootType = parts.shift().trim();
|
|
2085
2092
|
let subType = parts.join('/').trim();
|
|
2086
2093
|
|
package/lib/mime-node/index.js
CHANGED
|
@@ -76,7 +76,11 @@ class MimeNode {
|
|
|
76
76
|
/**
|
|
77
77
|
* Indicates which encoding should be used for header strings: "Q" or "B"
|
|
78
78
|
*/
|
|
79
|
-
this.textEncoding = (options.textEncoding || '')
|
|
79
|
+
this.textEncoding = (options.textEncoding || '')
|
|
80
|
+
.toString()
|
|
81
|
+
.trim()
|
|
82
|
+
.charAt(0)
|
|
83
|
+
.toUpperCase();
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
86
|
* Immediate parent for this node (or undefined if not set)
|
|
@@ -423,10 +427,16 @@ class MimeNode {
|
|
|
423
427
|
|
|
424
428
|
getTransferEncoding() {
|
|
425
429
|
let transferEncoding = false;
|
|
426
|
-
let contentType = (this.getHeader('Content-Type') || '')
|
|
430
|
+
let contentType = (this.getHeader('Content-Type') || '')
|
|
431
|
+
.toString()
|
|
432
|
+
.toLowerCase()
|
|
433
|
+
.trim();
|
|
427
434
|
|
|
428
435
|
if (this.content) {
|
|
429
|
-
transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '')
|
|
436
|
+
transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '')
|
|
437
|
+
.toString()
|
|
438
|
+
.toLowerCase()
|
|
439
|
+
.trim();
|
|
430
440
|
if (!transferEncoding || !['base64', 'quoted-printable'].includes(transferEncoding)) {
|
|
431
441
|
if (/^text\//i.test(contentType)) {
|
|
432
442
|
// If there are no special symbols, no need to modify the text
|
|
@@ -1047,7 +1057,10 @@ class MimeNode {
|
|
|
1047
1057
|
[],
|
|
1048
1058
|
[].concat(value || '').map(elm => {
|
|
1049
1059
|
// eslint-disable-line prefer-spread
|
|
1050
|
-
elm = (elm || '')
|
|
1060
|
+
elm = (elm || '')
|
|
1061
|
+
.toString()
|
|
1062
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
1063
|
+
.trim();
|
|
1051
1064
|
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
|
|
1052
1065
|
})
|
|
1053
1066
|
)
|
|
@@ -123,7 +123,13 @@ class SESTransport extends EventEmitter {
|
|
|
123
123
|
|
|
124
124
|
let delay = Math.max(oldest + 1001, now + 20);
|
|
125
125
|
this.sendingRateTTL = setTimeout(() => this._checkRatedQueue(), now - delay);
|
|
126
|
-
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
this.sendingRateTTL.unref();
|
|
129
|
+
} catch (E) {
|
|
130
|
+
// Ignore. Happens on envs with non-node timer implementation
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
return false;
|
|
128
134
|
}
|
|
129
135
|
|
package/lib/shared/index.js
CHANGED
|
@@ -187,7 +187,10 @@ module.exports.resolveContent = (data, key, callback) => {
|
|
|
187
187
|
|
|
188
188
|
let content = (data && data[key] && data[key].content) || data[key];
|
|
189
189
|
let contentStream;
|
|
190
|
-
let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
|
190
|
+
let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
|
191
|
+
.toString()
|
|
192
|
+
.toLowerCase()
|
|
193
|
+
.replace(/[-_\s]/g, '');
|
|
191
194
|
|
|
192
195
|
if (!content) {
|
|
193
196
|
return callback(null, content);
|
|
@@ -363,7 +366,15 @@ function createDefaultLogger(levels) {
|
|
|
363
366
|
|
|
364
367
|
message = util.format(message, ...args);
|
|
365
368
|
message.split(/\r?\n/).forEach(line => {
|
|
366
|
-
console.log(
|
|
369
|
+
console.log(
|
|
370
|
+
'[%s] %s %s',
|
|
371
|
+
new Date()
|
|
372
|
+
.toISOString()
|
|
373
|
+
.substr(0, 19)
|
|
374
|
+
.replace(/T/, ' '),
|
|
375
|
+
levelNames.get(level),
|
|
376
|
+
prefix + line
|
|
377
|
+
);
|
|
367
378
|
});
|
|
368
379
|
};
|
|
369
380
|
|
|
@@ -78,7 +78,9 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
|
|
78
78
|
destinationPort +
|
|
79
79
|
' HTTP/1.1\r\n' +
|
|
80
80
|
// HTTP request headers
|
|
81
|
-
Object.keys(reqHeaders)
|
|
81
|
+
Object.keys(reqHeaders)
|
|
82
|
+
.map(key => key + ': ' + reqHeaders[key])
|
|
83
|
+
.join('\r\n') +
|
|
82
84
|
// End request
|
|
83
85
|
'\r\n\r\n'
|
|
84
86
|
);
|
|
@@ -45,7 +45,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
45
45
|
constructor(options) {
|
|
46
46
|
super(options);
|
|
47
47
|
|
|
48
|
-
this.id = crypto
|
|
48
|
+
this.id = crypto
|
|
49
|
+
.randomBytes(8)
|
|
50
|
+
.toString('base64')
|
|
51
|
+
.replace(/\W/g, '');
|
|
49
52
|
this.stage = 'init';
|
|
50
53
|
|
|
51
54
|
this.options = options || {};
|
|
@@ -315,7 +318,11 @@ class SMTPConnection extends EventEmitter {
|
|
|
315
318
|
this._auth = authData || {};
|
|
316
319
|
|
|
317
320
|
// Select SASL authentication method
|
|
318
|
-
this._authMethod =
|
|
321
|
+
this._authMethod =
|
|
322
|
+
(this._auth.method || '')
|
|
323
|
+
.toString()
|
|
324
|
+
.trim()
|
|
325
|
+
.toUpperCase() || false;
|
|
319
326
|
if (!this._authMethod && this._auth.oauth2 && !this._auth.credentials) {
|
|
320
327
|
this._authMethod = 'XOAUTH2';
|
|
321
328
|
} else if (!this._authMethod || (this._authMethod === 'XOAUTH2' && !this._auth.oauth2)) {
|
|
@@ -405,15 +412,22 @@ class SMTPConnection extends EventEmitter {
|
|
|
405
412
|
message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
|
|
406
413
|
}
|
|
407
414
|
|
|
415
|
+
let startTime = Date.now();
|
|
408
416
|
this._setEnvelope(envelope, (err, info) => {
|
|
409
417
|
if (err) {
|
|
410
418
|
return callback(err);
|
|
411
419
|
}
|
|
420
|
+
let envelopeTime = Date.now();
|
|
412
421
|
let stream = this._createSendStream((err, str) => {
|
|
413
422
|
if (err) {
|
|
414
423
|
return callback(err);
|
|
415
424
|
}
|
|
425
|
+
|
|
426
|
+
info.envelopeTime = envelopeTime - startTime;
|
|
427
|
+
info.messageTime = Date.now() - envelopeTime;
|
|
428
|
+
info.messageSize = stream.outByteCount;
|
|
416
429
|
info.response = str;
|
|
430
|
+
|
|
417
431
|
return callback(null, info);
|
|
418
432
|
});
|
|
419
433
|
if (typeof message.pipe === 'function') {
|
|
@@ -35,6 +35,9 @@ class PoolResource extends EventEmitter {
|
|
|
35
35
|
break;
|
|
36
36
|
}
|
|
37
37
|
default:
|
|
38
|
+
if (!this.options.auth.user && !this.options.auth.pass) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
38
41
|
this.auth = {
|
|
39
42
|
type: 'LOGIN',
|
|
40
43
|
user: this.options.auth.user,
|
|
@@ -107,7 +110,8 @@ class PoolResource extends EventEmitter {
|
|
|
107
110
|
return;
|
|
108
111
|
}
|
|
109
112
|
returned = true;
|
|
110
|
-
|
|
113
|
+
|
|
114
|
+
let timer = setTimeout(() => {
|
|
111
115
|
if (returned) {
|
|
112
116
|
return;
|
|
113
117
|
}
|
|
@@ -118,7 +122,13 @@ class PoolResource extends EventEmitter {
|
|
|
118
122
|
err.code = 'ETLS';
|
|
119
123
|
}
|
|
120
124
|
callback(err);
|
|
121
|
-
}, 1000)
|
|
125
|
+
}, 1000);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
timer.unref();
|
|
129
|
+
} catch (E) {
|
|
130
|
+
// Ignore. Happens on envs with non-node timer implementation
|
|
131
|
+
}
|
|
122
132
|
});
|
|
123
133
|
|
|
124
134
|
this.connection.connect(() => {
|
|
@@ -179,7 +179,8 @@ class SMTPTransport extends EventEmitter {
|
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
returned = true;
|
|
182
|
-
|
|
182
|
+
|
|
183
|
+
let timer = setTimeout(() => {
|
|
183
184
|
if (returned) {
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
@@ -190,7 +191,13 @@ class SMTPTransport extends EventEmitter {
|
|
|
190
191
|
err.code = 'ETLS';
|
|
191
192
|
}
|
|
192
193
|
callback(err);
|
|
193
|
-
}, 1000)
|
|
194
|
+
}, 1000);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
timer.unref();
|
|
198
|
+
} catch (E) {
|
|
199
|
+
// Ignore. Happens on envs with non-node timer implementation
|
|
200
|
+
}
|
|
194
201
|
});
|
|
195
202
|
|
|
196
203
|
let sendMessage = () => {
|
package/lib/xoauth2/index.js
CHANGED
|
@@ -41,7 +41,8 @@ class XOAuth2 extends Stream {
|
|
|
41
41
|
|
|
42
42
|
if (options && options.serviceClient) {
|
|
43
43
|
if (!options.privateKey || !options.user) {
|
|
44
|
-
|
|
44
|
+
setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
|
|
45
|
+
return;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
let serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
|
|
@@ -300,7 +301,10 @@ class XOAuth2 extends Stream {
|
|
|
300
301
|
*/
|
|
301
302
|
jwtSignRS256(payload) {
|
|
302
303
|
payload = ['{"alg":"RS256","typ":"JWT"}', JSON.stringify(payload)].map(val => this.toBase64URL(val)).join('.');
|
|
303
|
-
let signature = crypto
|
|
304
|
+
let signature = crypto
|
|
305
|
+
.createSign('RSA-SHA256')
|
|
306
|
+
.update(payload)
|
|
307
|
+
.sign(this.options.privateKey);
|
|
304
308
|
return payload + '.' + this.toBase64URL(signature);
|
|
305
309
|
}
|
|
306
310
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
|
5
5
|
"main": "lib/nodemailer.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"grunt": "^1.0.1",
|
|
25
25
|
"grunt-cli": "^1.2.0",
|
|
26
26
|
"grunt-eslint": "^20.1.0",
|
|
27
|
-
"grunt-mocha-test": "^0.13.
|
|
27
|
+
"grunt-mocha-test": "^0.13.3",
|
|
28
28
|
"libbase64": "^0.2.0",
|
|
29
29
|
"libmime": "^3.1.0",
|
|
30
30
|
"libqp": "^1.1.0",
|
|
31
|
-
"mocha": "^
|
|
31
|
+
"mocha": "^4.0.1",
|
|
32
32
|
"proxy": "^0.2.4",
|
|
33
33
|
"proxy-test-server": "^1.0.0",
|
|
34
|
-
"sinon": "^
|
|
35
|
-
"smtp-server": "^3.
|
|
34
|
+
"sinon": "^4.1.2",
|
|
35
|
+
"smtp-server": "^3.3.0"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": ">=6.0.0"
|
package/.github/stale.yml
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Number of days of inactivity before an issue becomes stale
|
|
2
|
-
daysUntilStale: 60
|
|
3
|
-
# Number of days of inactivity before a stale issue is closed
|
|
4
|
-
daysUntilClose: 7
|
|
5
|
-
# Issues with these labels will never be considered stale
|
|
6
|
-
exemptLabels:
|
|
7
|
-
- pinned
|
|
8
|
-
- security
|
|
9
|
-
# Label to use when marking an issue as stale
|
|
10
|
-
staleLabel: wontfix
|
|
11
|
-
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
12
|
-
markComment: >
|
|
13
|
-
This issue has been automatically marked as stale because it has not had
|
|
14
|
-
recent activity. It will be closed if no further activity occurs. Thank you
|
|
15
|
-
for your contributions.
|
|
16
|
-
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
17
|
-
closeComment: false
|