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 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
@@ -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.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n').trim();
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.substr(0, line.indexOf(':')).trim().toLowerCase(),
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 || '').toLowerCase().split(':').forEach(field => {
74
- skip.add(field.trim());
75
- });
76
-
77
- (fieldNames || '').toLowerCase().split(':').filter(field => !skip.has(field.trim())).forEach(field => {
78
- includedFields.add(field.trim());
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.substr(line.indexOf(':') + 1).replace(/\r?\n/g, '').replace(/\s+/g, ' ').trim();
112
+ return line
113
+ .substr(line.indexOf(':') + 1)
114
+ .replace(/\r?\n/g, '')
115
+ .replace(/\s+/g, ' ')
116
+ .trim();
106
117
  }
@@ -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).map(cookie => cookie.name + '=' + cookie.value).join('; ');
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 || '').toString().split(';').forEach(cookiePart => {
107
- let valueParts = cookiePart.split('=');
108
- let key = valueParts.shift().trim().toLowerCase();
109
- let value = valueParts.join('=').trim();
110
- let domain;
111
-
112
- if (!key) {
113
- // skip empty parts
114
- return;
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
- switch (key) {
118
- case 'expires':
119
- value = new Date(value);
120
- // ignore date if can not parse it
121
- if (value.toString() !== 'Invalid Date') {
122
- cookie.expires = value;
123
- }
124
- break;
125
-
126
- case 'path':
127
- cookie.path = value;
128
- break;
129
-
130
- case 'domain':
131
- domain = value.toLowerCase();
132
- if (domain.length && domain.charAt(0) !== '.') {
133
- domain = '.' + domain; // ensure preceeding dot for user set domains
134
- }
135
- cookie.domain = domain;
136
- break;
137
-
138
- case 'max-age':
139
- cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
140
- break;
141
-
142
- case 'secure':
143
- cookie.secure = true;
144
- break;
145
-
146
- case 'httponly':
147
- cookie.httponly = true;
148
- break;
149
-
150
- default:
151
- if (!cookie.name) {
152
- cookie.name = key;
153
- cookie.value = value;
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
  }
@@ -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 = (options.method || '').toString().trim().toUpperCase() || 'GET';
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 = (options.method || '').toString().trim().toUpperCase() || 'POST';
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.resolveAll((err, data) => {
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.messageId = messageId;
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: mail.data.envelope || mail.message.getEnvelope(),
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
- [].concat(this._alternatives || []).concat(this._attachments.attached || []).shift() || {
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 = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
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 = 'text/calendar; charset="utf-8"; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
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
- [].concat(text || []).concat(watchHtml || []).concat(html || []).concat(eventObject || []).concat(this.mail.alternatives || []).forEach(alternative => {
261
- let data;
262
-
263
- if (/^data:/i.test(alternative.path || alternative.href)) {
264
- alternative = this._processDataUrl(alternative);
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
- data = {
268
- contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
269
- contentTransferEncoding: alternative.contentTransferEncoding
270
- };
286
+ data = {
287
+ contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
288
+ contentTransferEncoding: alternative.contentTransferEncoding
289
+ };
271
290
 
272
- if (alternative.filename) {
273
- data.filename = alternative.filename;
274
- }
291
+ if (alternative.filename) {
292
+ data.filename = alternative.filename;
293
+ }
275
294
 
276
- if (/^https?:\/\//i.test(alternative.path)) {
277
- alternative.href = alternative.path;
278
- alternative.path = undefined;
279
- }
295
+ if (/^https?:\/\//i.test(alternative.path)) {
296
+ alternative.href = alternative.path;
297
+ alternative.path = undefined;
298
+ }
280
299
 
281
- if (alternative.raw) {
282
- data.raw = alternative.raw;
283
- } else if (alternative.path) {
284
- data.content = {
285
- path: alternative.path
286
- };
287
- } else if (alternative.href) {
288
- data.content = {
289
- href: alternative.href
290
- };
291
- } else {
292
- data.content = alternative.content || '';
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
- if (alternative.encoding) {
296
- data.encoding = alternative.encoding;
297
- }
314
+ if (alternative.encoding) {
315
+ data.encoding = alternative.encoding;
316
+ }
298
317
 
299
- if (alternative.headers) {
300
- data.headers = alternative.headers;
301
- }
318
+ if (alternative.headers) {
319
+ data.headers = alternative.headers;
320
+ }
302
321
 
303
- alternatives.push(data);
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
- [].concat((!this._useAlternative && this._alternatives) || []).concat(this._attachments.attached || []).forEach(element => {
341
- // if the element is a html node from related subpart then ignore it
342
- if (!this._useRelated || element !== this._htmlNode) {
343
- this._createContentNode(node, element);
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').toString().toLowerCase().replace(/[-_\s]/g, '');
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 = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
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;
@@ -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').toString().toUpperCase().trim().charAt(0);
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.charCodeAt(0).toString(16).toUpperCase();
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.charCodeAt(0).toString(16).toUpperCase();
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 || '').split('?').shift().trim().toLowerCase();
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 || '').toLowerCase().trim().split('/');
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
 
@@ -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 || '').toString().trim().charAt(0).toUpperCase();
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') || '').toString().toLowerCase().trim();
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') || '').toString().toLowerCase().trim();
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 || '').toString().replace(/\r?\n|\r/g, ' ').trim();
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
- this.sendingRateTTL.unref();
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
 
@@ -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').toString().toLowerCase().replace(/[-_\s]/g, '');
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('[%s] %s %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), levelNames.get(level), prefix + line);
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).map(key => key + ': ' + reqHeaders[key]).join('\r\n') +
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.randomBytes(8).toString('base64').replace(/\W/g, '');
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 = (this._auth.method || '').toString().trim().toUpperCase() || false;
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
- setTimeout(() => {
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).unref();
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
- setTimeout(() => {
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).unref();
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 = () => {
@@ -41,7 +41,8 @@ class XOAuth2 extends Stream {
41
41
 
42
42
  if (options && options.serviceClient) {
43
43
  if (!options.privateKey || !options.user) {
44
- return setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
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.createSign('RSA-SHA256').update(payload).sign(this.options.privateKey);
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.1.3",
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.2",
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": "^3.5.3",
31
+ "mocha": "^4.0.1",
32
32
  "proxy": "^0.2.4",
33
33
  "proxy-test-server": "^1.0.0",
34
- "sinon": "^3.3.0",
35
- "smtp-server": "^3.1.0"
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
package/.npmignore DELETED
@@ -1,9 +0,0 @@
1
- assets
2
- test
3
- examples
4
- .eslintrc
5
- .gitignore
6
- .travis.yml
7
- Gruntfile.js
8
- ISSUE_TEMPLATE.md
9
- CONTRIBUTING.md