nodemailer 6.4.18 → 6.6.2

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,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 6.6.1 2021-05-23
4
+
5
+ - Fixed address formatting issue where newlines in an email address, if provided via address object, were not properly removed. Reported by tmazeika (#1289)
6
+
7
+ ## 6.6.0 2021-04-28
8
+
9
+ - Added new option `newline` for MailComposer
10
+ - aws ses connection verification (Ognjen Jevremovic)
11
+
12
+ ## 6.5.0 2021-02-26
13
+
14
+ - Pass through textEncoding to subnodes
15
+ - Added support for AWS SES v3 SDK
16
+ - Fixed tests
17
+
3
18
  ## 6.4.18 2021-02-11
4
19
 
5
20
  - Updated README
package/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  [![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)](https://nodemailer.com/about/)
4
4
 
5
- > Sponsored by [Forward Email](https://forwardemail.net/?ref=nodemailer) – free email forwarding + custom domains + 100% open-source!
6
-
7
5
  Send e-mails from Node.js – easy as cake! 🍰✉️
8
6
 
9
7
  [![NPM](https://nodei.co/npm/nodemailer.png?downloads=true&downloadRank=true&stars=true)](https://nodemailer.com/about/)
@@ -30,8 +28,8 @@ Check your firewall settings. Timeout usually occurs when you try to open a conn
30
28
 
31
29
  #### I get TLS errors
32
30
 
33
- * If you are running the code in your own machine, then check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
34
- * Latest Node versions allow only TLS versions 1.2 and higher, some servers might still use TLS 1.1 or lower. Check Node.js docs how to get correct TLS support for your app.
31
+ - If you are running the code in your own machine, then check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
32
+ - Latest Node versions allow only TLS versions 1.2 and higher, some servers might still use TLS 1.1 or lower. Check Node.js docs how to get correct TLS support for your app.
35
33
 
36
34
  #### I have a different problem
37
35
 
@@ -31,7 +31,7 @@ class MailComposer {
31
31
 
32
32
  // Compose MIME tree
33
33
  if (this.mail.raw) {
34
- this.message = new MimeNode().setRaw(this.mail.raw);
34
+ this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
35
35
  } else if (this._useMixed) {
36
36
  this.message = this._createMixed();
37
37
  } else if (this._useAlternative) {
@@ -345,13 +345,15 @@ class MailComposer {
345
345
  boundaryPrefix: this.mail.boundaryPrefix,
346
346
  disableUrlAccess: this.mail.disableUrlAccess,
347
347
  disableFileAccess: this.mail.disableFileAccess,
348
- normalizeHeaderKey: this.mail.normalizeHeaderKey
348
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
349
+ newline: this.mail.newline
349
350
  });
350
351
  } else {
351
352
  node = parentNode.createChild('multipart/mixed', {
352
353
  disableUrlAccess: this.mail.disableUrlAccess,
353
354
  disableFileAccess: this.mail.disableFileAccess,
354
- normalizeHeaderKey: this.mail.normalizeHeaderKey
355
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
356
+ newline: this.mail.newline
355
357
  });
356
358
  }
357
359
 
@@ -391,13 +393,15 @@ class MailComposer {
391
393
  boundaryPrefix: this.mail.boundaryPrefix,
392
394
  disableUrlAccess: this.mail.disableUrlAccess,
393
395
  disableFileAccess: this.mail.disableFileAccess,
394
- normalizeHeaderKey: this.mail.normalizeHeaderKey
396
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
397
+ newline: this.mail.newline
395
398
  });
396
399
  } else {
397
400
  node = parentNode.createChild('multipart/alternative', {
398
401
  disableUrlAccess: this.mail.disableUrlAccess,
399
402
  disableFileAccess: this.mail.disableFileAccess,
400
- normalizeHeaderKey: this.mail.normalizeHeaderKey
403
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
404
+ newline: this.mail.newline
401
405
  });
402
406
  }
403
407
 
@@ -428,13 +432,15 @@ class MailComposer {
428
432
  boundaryPrefix: this.mail.boundaryPrefix,
429
433
  disableUrlAccess: this.mail.disableUrlAccess,
430
434
  disableFileAccess: this.mail.disableFileAccess,
431
- normalizeHeaderKey: this.mail.normalizeHeaderKey
435
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
436
+ newline: this.mail.newline
432
437
  });
433
438
  } else {
434
439
  node = parentNode.createChild('multipart/related; type="text/html"', {
435
440
  disableUrlAccess: this.mail.disableUrlAccess,
436
441
  disableFileAccess: this.mail.disableFileAccess,
437
- normalizeHeaderKey: this.mail.normalizeHeaderKey
442
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
443
+ newline: this.mail.newline
438
444
  });
439
445
  }
440
446
 
@@ -470,14 +476,17 @@ class MailComposer {
470
476
  boundaryPrefix: this.mail.boundaryPrefix,
471
477
  disableUrlAccess: this.mail.disableUrlAccess,
472
478
  disableFileAccess: this.mail.disableFileAccess,
473
- normalizeHeaderKey: this.mail.normalizeHeaderKey
479
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
480
+ newline: this.mail.newline
474
481
  });
475
482
  } else {
476
483
  node = parentNode.createChild(element.contentType, {
477
484
  filename: element.filename,
485
+ textEncoding: this.mail.textEncoding,
478
486
  disableUrlAccess: this.mail.disableUrlAccess,
479
487
  disableFileAccess: this.mail.disableFileAccess,
480
- normalizeHeaderKey: this.mail.normalizeHeaderKey
488
+ normalizeHeaderKey: this.mail.normalizeHeaderKey,
489
+ newline: this.mail.newline
481
490
  });
482
491
  }
483
492
 
@@ -16,6 +16,9 @@ const addressparser = require('../addressparser');
16
16
  const fetch = require('../fetch');
17
17
  const LastNewline = require('./last-newline');
18
18
 
19
+ const LeWindows = require('./le-windows');
20
+ const LeUnix = require('./le-unix');
21
+
19
22
  /**
20
23
  * Creates a new mime tree node. Assumes 'multipart/*' as the content type
21
24
  * if it is a branch, anything else counts as leaf. If rootNode is missing from
@@ -92,6 +95,11 @@ class MimeNode {
92
95
  */
93
96
  this.hostname = options.hostname;
94
97
 
98
+ /**
99
+ * 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.
100
+ */
101
+ this.newline = options.newline;
102
+
95
103
  /**
96
104
  * An array for possible child nodes
97
105
  */
@@ -451,7 +459,7 @@ class MimeNode {
451
459
  transferEncoding = this._getTextEncoding(this.content) === 'Q' ? 'quoted-printable' : 'base64';
452
460
  } else {
453
461
  // we can not check content for a stream, so either use preferred encoding or fallback to QP
454
- transferEncoding = this.transferEncoding === 'B' ? 'base64' : 'quoted-printable';
462
+ transferEncoding = this.textEncoding === 'B' ? 'base64' : 'quoted-printable';
455
463
  }
456
464
  } else if (!/^(multipart|message)\//i.test(contentType)) {
457
465
  transferEncoding = transferEncoding || 'base64';
@@ -626,6 +634,15 @@ class MimeNode {
626
634
  outputStream = transform(outputStream);
627
635
  }
628
636
 
637
+ if (this.newline) {
638
+ const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
639
+ const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
640
+
641
+ const stream = outputStream.pipe(newlineTransform);
642
+ outputStream.on('error', err => stream.emit('error', err));
643
+ return stream;
644
+ }
645
+
629
646
  return outputStream;
630
647
  }
631
648
 
@@ -1130,9 +1147,9 @@ class MimeNode {
1130
1147
  address.address = this._normalizeAddress(address.address);
1131
1148
 
1132
1149
  if (!address.name) {
1133
- values.push(address.address);
1150
+ values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
1134
1151
  } else if (address.name) {
1135
- values.push(this._encodeAddressName(address.name) + ' <' + address.address + '>');
1152
+ values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
1136
1153
  }
1137
1154
 
1138
1155
  if (address.address) {
@@ -1141,9 +1158,8 @@ class MimeNode {
1141
1158
  }
1142
1159
  }
1143
1160
  } else if (address.group) {
1144
- values.push(
1145
- this._encodeAddressName(address.name) + ':' + (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim() + ';'
1146
- );
1161
+ let groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
1162
+ values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
1147
1163
  }
1148
1164
  });
1149
1165
 
@@ -1157,13 +1173,17 @@ class MimeNode {
1157
1173
  * @return {String} address string
1158
1174
  */
1159
1175
  _normalizeAddress(address) {
1160
- address = (address || '').toString().trim();
1176
+ address = (address || '')
1177
+ .toString()
1178
+ .replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
1179
+ .trim();
1161
1180
 
1162
1181
  let lastAt = address.lastIndexOf('@');
1163
1182
  if (lastAt < 0) {
1164
1183
  // Bare username
1165
1184
  return address;
1166
1185
  }
1186
+
1167
1187
  let user = address.substr(0, lastAt);
1168
1188
  let domain = address.substr(lastAt + 1);
1169
1189
 
@@ -1172,7 +1192,24 @@ class MimeNode {
1172
1192
  // 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
1173
1193
  // non-unicode domains are left as is
1174
1194
 
1175
- return user + '@' + punycode.toASCII(domain.toLowerCase());
1195
+ let encodedDomain;
1196
+
1197
+ try {
1198
+ encodedDomain = punycode.toASCII(domain.toLowerCase());
1199
+ } catch (err) {
1200
+ // keep as is?
1201
+ }
1202
+
1203
+ if (user.indexOf(' ') >= 0) {
1204
+ if (user.charAt(0) !== '"') {
1205
+ user = '"' + user;
1206
+ }
1207
+ if (user.substr(-1) !== '"') {
1208
+ user = user + '"';
1209
+ }
1210
+ }
1211
+
1212
+ return `${user}@${encodedDomain}`;
1176
1213
  }
1177
1214
 
1178
1215
  /**
File without changes
@@ -2,8 +2,6 @@
2
2
 
3
3
  const spawn = require('child_process').spawn;
4
4
  const packageData = require('../../package.json');
5
- const LeWindows = require('./le-windows');
6
- const LeUnix = require('./le-unix');
7
5
  const shared = require('../shared');
8
6
 
9
7
  /**
@@ -68,7 +66,6 @@ class SendmailTransport {
68
66
  let args;
69
67
  let sendmail;
70
68
  let returned;
71
- let transform;
72
69
 
73
70
  const hasInvalidAddresses = []
74
71
  .concat(envelope.from || [])
@@ -187,10 +184,8 @@ class SendmailTransport {
187
184
  recipients.join(', ')
188
185
  );
189
186
 
190
- transform = this.winbreak ? new LeWindows() : new LeUnix();
191
187
  let sourceStream = mail.message.createReadStream();
192
-
193
- transform.once('error', err => {
188
+ sourceStream.once('error', err => {
194
189
  this.logger.error(
195
190
  {
196
191
  err,
@@ -205,8 +200,7 @@ class SendmailTransport {
205
200
  callback(err);
206
201
  });
207
202
 
208
- sourceStream.once('error', err => transform.emit('error', err));
209
- sourceStream.pipe(transform).pipe(sendmail.stdin);
203
+ sourceStream.pipe(sendmail.stdin);
210
204
  } else {
211
205
  return callback(new Error('sendmail was not found'));
212
206
  }
@@ -3,7 +3,7 @@
3
3
  const EventEmitter = require('events');
4
4
  const packageData = require('../../package.json');
5
5
  const shared = require('../shared');
6
- const LeWindows = require('../sendmail-transport/le-windows');
6
+ const LeWindows = require('../mime-node/le-windows');
7
7
 
8
8
  /**
9
9
  * Generates a Transport object for AWS SES
@@ -239,36 +239,64 @@ class SESTransport extends EventEmitter {
239
239
  sesMessage[key] = mail.data.ses[key];
240
240
  });
241
241
 
242
- this.ses.sendRawEmail(sesMessage, (err, data) => {
243
- if (err) {
244
- this.logger.error(
245
- {
246
- err,
247
- tnx: 'send'
248
- },
249
- 'Send error for %s: %s',
250
- messageId,
251
- err.message
252
- );
253
- statObject.pending = false;
254
- return callback(err);
242
+ let ses = (this.ses.aws ? this.ses.ses : this.ses) || {};
243
+ let aws = this.ses.aws || {};
244
+
245
+ let getRegion = cb => {
246
+ if (ses.config && typeof ses.config.region === 'function') {
247
+ // promise
248
+ return ses.config
249
+ .region()
250
+ .then(region => cb(null, region))
251
+ .catch(err => cb(err));
255
252
  }
253
+ return cb(null, (ses.config && ses.config.region) || 'us-east-1');
254
+ };
256
255
 
257
- let region = (this.ses.config && this.ses.config.region) || 'us-east-1';
258
- if (region === 'us-east-1') {
259
- region = 'email';
256
+ getRegion((err, region) => {
257
+ if (err || !region) {
258
+ region = 'us-east-1';
260
259
  }
261
260
 
262
- statObject.pending = false;
263
- callback(null, {
264
- envelope: {
265
- from: envelope.from,
266
- to: envelope.to
267
- },
268
- messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
269
- response: data.MessageId,
270
- raw
271
- });
261
+ let sendPromise;
262
+ if (typeof ses.send === 'function' && aws.SendRawEmailCommand) {
263
+ // v3 API
264
+ sendPromise = ses.send(new aws.SendRawEmailCommand(sesMessage));
265
+ } else {
266
+ // v2 API
267
+ sendPromise = ses.sendRawEmail(sesMessage).promise();
268
+ }
269
+
270
+ sendPromise
271
+ .then(data => {
272
+ if (region === 'us-east-1') {
273
+ region = 'email';
274
+ }
275
+
276
+ statObject.pending = false;
277
+ callback(null, {
278
+ envelope: {
279
+ from: envelope.from,
280
+ to: envelope.to
281
+ },
282
+ messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
283
+ response: data.MessageId,
284
+ raw
285
+ });
286
+ })
287
+ .catch(err => {
288
+ this.logger.error(
289
+ {
290
+ err,
291
+ tnx: 'send'
292
+ },
293
+ 'Send error for %s: %s',
294
+ messageId,
295
+ err.message
296
+ );
297
+ statObject.pending = false;
298
+ callback(err);
299
+ });
272
300
  });
273
301
  })
274
302
  );
@@ -281,6 +309,23 @@ class SESTransport extends EventEmitter {
281
309
  */
282
310
  verify(callback) {
283
311
  let promise;
312
+ let ses = (this.ses.aws ? this.ses.ses : this.ses) || {};
313
+ let aws = this.ses.aws || {};
314
+
315
+ const sesMessage = {
316
+ RawMessage: {
317
+ // required
318
+ Data: 'From: invalid@invalid\r\nTo: invalid@invalid\r\n Subject: Invalid\r\n\r\nInvalid'
319
+ },
320
+ Source: 'invalid@invalid',
321
+ Destinations: ['invalid@invalid']
322
+ };
323
+ const cb = err => {
324
+ if (err && err.code !== 'InvalidParameterValue') {
325
+ return callback(err);
326
+ }
327
+ return callback(null, true);
328
+ };
284
329
 
285
330
  if (!callback) {
286
331
  promise = new Promise((resolve, reject) => {
@@ -288,22 +333,13 @@ class SESTransport extends EventEmitter {
288
333
  });
289
334
  }
290
335
 
291
- this.ses.sendRawEmail(
292
- {
293
- RawMessage: {
294
- // required
295
- Data: 'From: invalid@invalid\r\nTo: invalid@invalid\r\n Subject: Invalid\r\n\r\nInvalid'
296
- },
297
- Source: 'invalid@invalid',
298
- Destinations: ['invalid@invalid']
299
- },
300
- err => {
301
- if (err && err.code !== 'InvalidParameterValue') {
302
- return callback(err);
303
- }
304
- return callback(null, true);
305
- }
306
- );
336
+ if (typeof ses.send === 'function' && aws.SendRawEmailCommand) {
337
+ // v3 API
338
+ ses.send(new aws.SendRawEmailCommand(sesMessage), cb);
339
+ } else {
340
+ // v2 API
341
+ ses.sendRawEmail(sesMessage, cb).promise();
342
+ }
307
343
 
308
344
  return promise;
309
345
  }
@@ -327,7 +327,11 @@ module.exports.resolveContent = (data, key, callback) => {
327
327
  }
328
328
  // we can't stream twice the same content, so we need
329
329
  // to replace the stream object with the streaming result
330
- data[key] = value;
330
+ if (data[key].content) {
331
+ data[key].content = value;
332
+ } else {
333
+ data[key] = value;
334
+ }
331
335
  callback(null, value);
332
336
  });
333
337
  } else if (/^https?:\/\//i.test(content.path || content.href)) {
@@ -873,16 +873,21 @@ class SMTPConnection extends EventEmitter {
873
873
  });
874
874
 
875
875
  this.upgrading = true;
876
- this._socket = tls.connect(opts, () => {
877
- this.secure = true;
878
- this.upgrading = false;
879
- this._socket.on('data', this._onSocketData);
876
+ // tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
877
+ try {
878
+ this._socket = tls.connect(opts, () => {
879
+ this.secure = true;
880
+ this.upgrading = false;
881
+ this._socket.on('data', this._onSocketData);
880
882
 
881
- socketPlain.removeListener('close', this._onSocketClose);
882
- socketPlain.removeListener('end', this._onSocketEnd);
883
+ socketPlain.removeListener('close', this._onSocketClose);
884
+ socketPlain.removeListener('end', this._onSocketEnd);
883
885
 
884
- return callback(null, true);
885
- });
886
+ return callback(null, true);
887
+ });
888
+ } catch (err) {
889
+ return callback(err);
890
+ }
886
891
 
887
892
  this._socket.on('error', this._onSocketError);
888
893
  this._socket.once('close', this._onSocketClose);
@@ -2,8 +2,6 @@
2
2
 
3
3
  const packageData = require('../../package.json');
4
4
  const shared = require('../shared');
5
- const LeWindows = require('../sendmail-transport/le-windows');
6
- const LeUnix = require('../sendmail-transport/le-unix');
7
5
 
8
6
  /**
9
7
  * Generates a Transport object for streaming
@@ -61,15 +59,10 @@ class StreamTransport {
61
59
  );
62
60
 
63
61
  setImmediate(() => {
64
- let sourceStream;
65
62
  let stream;
66
- let transform;
67
63
 
68
64
  try {
69
- transform = this.winbreak ? new LeWindows() : new LeUnix();
70
- sourceStream = mail.message.createReadStream();
71
- stream = sourceStream.pipe(transform);
72
- sourceStream.on('error', err => stream.emit('error', err));
65
+ stream = mail.message.createReadStream();
73
66
  } catch (E) {
74
67
  this.logger.error(
75
68
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "6.4.18",
3
+ "version": "6.6.2",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {
@@ -21,25 +21,24 @@
21
21
  "homepage": "https://nodemailer.com/",
22
22
  "devDependencies": {
23
23
  "bunyan": "1.8.15",
24
- "chai": "4.3.0",
24
+ "chai": "4.3.4",
25
25
  "eslint-config-nodemailer": "1.2.0",
26
- "eslint-config-prettier": "7.2.0",
27
- "grunt": "1.3.0",
28
- "grunt-cli": "1.3.2",
26
+ "eslint-config-prettier": "8.3.0",
27
+ "grunt": "1.4.1",
28
+ "grunt-cli": "1.4.3",
29
29
  "grunt-eslint": "23.0.0",
30
30
  "grunt-mocha-test": "0.13.3",
31
31
  "libbase64": "1.2.1",
32
32
  "libmime": "5.0.0",
33
33
  "libqp": "1.1.0",
34
- "mocha": "8.2.1",
34
+ "mocha": "9.0.0",
35
35
  "nodemailer-ntlm-auth": "1.0.1",
36
36
  "proxy": "1.0.2",
37
37
  "proxy-test-server": "1.0.0",
38
- "sinon": "9.2.4",
39
- "smtp-server": "3.8.0"
38
+ "sinon": "11.1.1",
39
+ "smtp-server": "3.9.0"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=6.0.0"
43
- },
44
- "dependencies": {}
43
+ }
45
44
  }