nodemailer 7.0.4 → 7.0.6

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
+ ## [7.0.6](https://github.com/nodemailer/nodemailer/compare/v7.0.5...v7.0.6) (2025-08-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **encoder:** avoid silent data loss by properly flushing trailing base64 ([#1747](https://github.com/nodemailer/nodemailer/issues/1747)) ([01ae76f](https://github.com/nodemailer/nodemailer/commit/01ae76f2cfe991c0c3fe80170f236da60531496b))
9
+ * handle multiple XOAUTH2 token requests correctly ([#1754](https://github.com/nodemailer/nodemailer/issues/1754)) ([dbe0028](https://github.com/nodemailer/nodemailer/commit/dbe00286351cddf012726a41a96ae613d30a34ee))
10
+ * ReDoS vulnerability in parseDataURI and _processDataUrl ([#1755](https://github.com/nodemailer/nodemailer/issues/1755)) ([90b3e24](https://github.com/nodemailer/nodemailer/commit/90b3e24d23929ebf9f4e16261049b40ee4055a39))
11
+
12
+ ## [7.0.5](https://github.com/nodemailer/nodemailer/compare/v7.0.4...v7.0.5) (2025-07-07)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * updated well known delivery service list ([fa2724b](https://github.com/nodemailer/nodemailer/commit/fa2724b337eb8d8fdcdd788fe903980b061316b8))
18
+
3
19
  ## [7.0.4](https://github.com/nodemailer/nodemailer/compare/v7.0.3...v7.0.4) (2025-06-29)
4
20
 
5
21
 
@@ -35,15 +35,12 @@ 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
39
- .substr(pos, chunkLength)
40
- .replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n')
41
- .trim();
38
+ let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n');
42
39
  result.push(wrappedLines);
43
40
  pos += chunkLength;
44
41
  }
45
42
 
46
- return result.join('\r\n').trim();
43
+ return result.join('');
47
44
  }
48
45
 
49
46
  /**
@@ -56,7 +53,6 @@ function wrap(str, lineLength) {
56
53
  class Encoder extends Transform {
57
54
  constructor(options) {
58
55
  super();
59
- // init Transform
60
56
  this.options = options || {};
61
57
 
62
58
  if (this.options.lineLength !== false) {
@@ -98,17 +94,20 @@ class Encoder extends Transform {
98
94
  if (this.options.lineLength) {
99
95
  b64 = wrap(b64, this.options.lineLength);
100
96
 
101
- // remove last line as it is still most probably incomplete
102
97
  let lastLF = b64.lastIndexOf('\n');
103
98
  if (lastLF < 0) {
104
99
  this._curLine = b64;
105
100
  b64 = '';
106
- } else if (lastLF === b64.length - 1) {
107
- this._curLine = '';
108
101
  } else {
109
- this._curLine = b64.substr(lastLF + 1);
110
- b64 = b64.substr(0, lastLF + 1);
102
+ this._curLine = b64.substring(lastLF + 1);
103
+ b64 = b64.substring(0, lastLF + 1);
104
+
105
+ if (b64 && !b64.endsWith('\r\n')) {
106
+ b64 += '\r\n';
107
+ }
111
108
  }
109
+ } else {
110
+ this._curLine = '';
112
111
  }
113
112
 
114
113
  if (b64) {
@@ -125,16 +124,14 @@ class Encoder extends Transform {
125
124
  }
126
125
 
127
126
  if (this._curLine) {
128
- this._curLine = wrap(this._curLine, this.options.lineLength);
129
127
  this.outputBytes += this._curLine.length;
130
- this.push(this._curLine, 'ascii');
128
+ this.push(Buffer.from(this._curLine, 'ascii'));
131
129
  this._curLine = '';
132
130
  }
133
131
  done();
134
132
  }
135
133
  }
136
134
 
137
- // expose to the world
138
135
  module.exports = {
139
136
  encode,
140
137
  wrap,
@@ -550,9 +550,33 @@ class MailComposer {
550
550
  * @return {Object} Parsed element
551
551
  */
552
552
  _processDataUrl(element) {
553
+ const dataUrl = element.path || element.href;
554
+
555
+ // Early validation to prevent ReDoS
556
+ if (!dataUrl || typeof dataUrl !== 'string') {
557
+ return element;
558
+ }
559
+
560
+ if (!dataUrl.startsWith('data:')) {
561
+ return element;
562
+ }
563
+
564
+ if (dataUrl.length > 100000) {
565
+ // 100KB limit for data URL string
566
+ // Return empty content for excessively long data URLs
567
+ return Object.assign({}, element, {
568
+ path: false,
569
+ href: false,
570
+ content: Buffer.alloc(0),
571
+ contentType: element.contentType || 'application/octet-stream'
572
+ });
573
+ }
574
+
553
575
  let parsedDataUri;
554
- if ((element.path || element.href).match(/^data:/)) {
555
- parsedDataUri = parseDataURI(element.path || element.href);
576
+ try {
577
+ parsedDataUri = parseDataURI(dataUrl);
578
+ } catch (err) {
579
+ return element;
556
580
  }
557
581
 
558
582
  if (!parsedDataUri) {
@@ -269,7 +269,7 @@ module.exports = {
269
269
 
270
270
  // first line includes the charset and language info and needs to be encoded
271
271
  // even if it does not contain any unicode characters
272
- line = 'utf-8\x27\x27';
272
+ line = "utf-8''";
273
273
  let encoded = true;
274
274
  startPos = 0;
275
275
 
@@ -419,52 +419,74 @@ module.exports.callbackPromise = (resolve, reject) =>
419
419
  };
420
420
 
421
421
  module.exports.parseDataURI = uri => {
422
- let input = uri;
423
- let commaPos = input.indexOf(',');
424
- if (!commaPos) {
425
- return uri;
422
+ if (typeof uri !== 'string') {
423
+ return null;
426
424
  }
427
425
 
428
- let data = input.substring(commaPos + 1);
429
- let metaStr = input.substring('data:'.length, commaPos);
426
+ // Early return for non-data URIs to avoid unnecessary processing
427
+ if (!uri.startsWith('data:')) {
428
+ return null;
429
+ }
430
430
 
431
- let encoding;
431
+ // Find the first comma safely - this prevents ReDoS
432
+ const commaPos = uri.indexOf(',');
433
+ if (commaPos === -1) {
434
+ return null;
435
+ }
432
436
 
433
- let metaEntries = metaStr.split(';');
434
- let lastMetaEntry = metaEntries.length > 1 ? metaEntries[metaEntries.length - 1] : false;
435
- if (lastMetaEntry && lastMetaEntry.indexOf('=') < 0) {
436
- encoding = lastMetaEntry.toLowerCase();
437
- metaEntries.pop();
437
+ const data = uri.substring(commaPos + 1);
438
+ const metaStr = uri.substring('data:'.length, commaPos);
439
+
440
+ let encoding;
441
+ const metaEntries = metaStr.split(';');
442
+
443
+ if (metaEntries.length > 0) {
444
+ const lastEntry = metaEntries[metaEntries.length - 1].toLowerCase().trim();
445
+ // Only recognize valid encoding types to prevent manipulation
446
+ if (['base64', 'utf8', 'utf-8'].includes(lastEntry) && lastEntry.indexOf('=') === -1) {
447
+ encoding = lastEntry;
448
+ metaEntries.pop();
449
+ }
438
450
  }
439
451
 
440
- let contentType = metaEntries.shift() || 'application/octet-stream';
441
- let params = {};
442
- for (let entry of metaEntries) {
443
- let sep = entry.indexOf('=');
444
- if (sep >= 0) {
445
- let key = entry.substring(0, sep);
446
- let value = entry.substring(sep + 1);
447
- params[key] = value;
452
+ const contentType = metaEntries.length > 0 ? metaEntries.shift() : 'application/octet-stream';
453
+ const params = {};
454
+
455
+ for (let i = 0; i < metaEntries.length; i++) {
456
+ const entry = metaEntries[i];
457
+ const sepPos = entry.indexOf('=');
458
+ if (sepPos > 0) {
459
+ // Ensure there's a key before the '='
460
+ const key = entry.substring(0, sepPos).trim();
461
+ const value = entry.substring(sepPos + 1).trim();
462
+ if (key) {
463
+ params[key] = value;
464
+ }
448
465
  }
449
466
  }
450
467
 
451
- switch (encoding) {
452
- case 'base64':
453
- data = Buffer.from(data, 'base64');
454
- break;
455
- case 'utf8':
456
- data = Buffer.from(data);
457
- break;
458
- default:
468
+ // Decode data based on encoding with proper error handling
469
+ let bufferData;
470
+ try {
471
+ if (encoding === 'base64') {
472
+ bufferData = Buffer.from(data, 'base64');
473
+ } else {
459
474
  try {
460
- data = Buffer.from(decodeURIComponent(data));
461
- } catch (err) {
462
- data = Buffer.from(data);
475
+ bufferData = Buffer.from(decodeURIComponent(data));
476
+ } catch (decodeError) {
477
+ bufferData = Buffer.from(data);
463
478
  }
464
- data = Buffer.from(data);
479
+ }
480
+ } catch (bufferError) {
481
+ bufferData = Buffer.alloc(0);
465
482
  }
466
483
 
467
- return { data, encoding, contentType, params };
484
+ return {
485
+ data: bufferData,
486
+ encoding: encoding || null,
487
+ contentType: contentType || 'application/octet-stream',
488
+ params
489
+ };
468
490
  };
469
491
 
470
492
  /**
@@ -1619,7 +1619,7 @@ class SMTPConnection extends EventEmitter {
1619
1619
  }
1620
1620
 
1621
1621
  if (!this._envelope.rcptQueue.length) {
1622
- return callback(this._formatError('Can\x27t send mail - no recipients defined', 'EENVELOPE', false, 'API'));
1622
+ return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
1623
1623
  } else {
1624
1624
  this._recipientQueue = [];
1625
1625
 
@@ -1675,7 +1675,7 @@ class SMTPConnection extends EventEmitter {
1675
1675
  });
1676
1676
  this._sendCommand('DATA');
1677
1677
  } else {
1678
- err = this._formatError('Can\x27t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO');
1678
+ err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
1679
1679
  err.rejected = this._envelope.rejected;
1680
1680
  err.rejectedErrors = this._envelope.rejectedErrors;
1681
1681
  return callback(err);
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "1und1": {
3
+ "description": "1&1 Mail (German hosting provider)",
3
4
  "host": "smtp.1und1.de",
4
5
  "port": 465,
5
6
  "secure": true,
6
7
  "authMethod": "LOGIN"
7
8
  },
8
9
 
10
+ "126": {
11
+ "description": "126 Mail (NetEase)",
12
+ "host": "smtp.126.com",
13
+ "port": 465,
14
+ "secure": true
15
+ },
16
+
17
+ "163": {
18
+ "description": "163 Mail (NetEase)",
19
+ "host": "smtp.163.com",
20
+ "port": 465,
21
+ "secure": true
22
+ },
23
+
9
24
  "Aliyun": {
25
+ "description": "Alibaba Cloud Mail",
10
26
  "domains": ["aliyun.com"],
11
27
  "host": "smtp.aliyun.com",
12
28
  "port": 465,
@@ -14,56 +30,91 @@
14
30
  },
15
31
 
16
32
  "AliyunQiye": {
33
+ "description": "Alibaba Cloud Enterprise Mail",
17
34
  "host": "smtp.qiye.aliyun.com",
18
35
  "port": 465,
19
36
  "secure": true
20
37
  },
21
38
 
22
39
  "AOL": {
40
+ "description": "AOL Mail",
23
41
  "domains": ["aol.com"],
24
42
  "host": "smtp.aol.com",
25
43
  "port": 587
26
44
  },
27
45
 
46
+ "Aruba": {
47
+ "description": "Aruba PEC (Italian email provider)",
48
+ "domains": ["aruba.it", "pec.aruba.it"],
49
+ "aliases": ["Aruba PEC"],
50
+ "host": "smtps.aruba.it",
51
+ "port": 465,
52
+ "secure": true,
53
+ "authMethod": "LOGIN"
54
+ },
55
+
28
56
  "Bluewin": {
57
+ "description": "Bluewin (Swiss email provider)",
29
58
  "host": "smtpauths.bluewin.ch",
30
59
  "domains": ["bluewin.ch"],
31
60
  "port": 465
32
61
  },
33
62
 
63
+ "BOL": {
64
+ "description": "BOL Mail (Brazilian provider)",
65
+ "domains": ["bol.com.br"],
66
+ "host": "smtp.bol.com.br",
67
+ "port": 587,
68
+ "requireTLS": true
69
+ },
70
+
34
71
  "DebugMail": {
72
+ "description": "DebugMail (email testing service)",
35
73
  "host": "debugmail.io",
36
74
  "port": 25
37
75
  },
38
76
 
77
+ "Disroot": {
78
+ "description": "Disroot (privacy-focused provider)",
79
+ "domains": ["disroot.org"],
80
+ "host": "disroot.org",
81
+ "port": 587,
82
+ "secure": false,
83
+ "authMethod": "LOGIN"
84
+ },
85
+
39
86
  "DynectEmail": {
87
+ "description": "Dyn Email Delivery",
40
88
  "aliases": ["Dynect"],
41
89
  "host": "smtp.dynect.net",
42
90
  "port": 25
43
91
  },
44
92
 
93
+ "ElasticEmail": {
94
+ "description": "Elastic Email",
95
+ "aliases": ["Elastic Email"],
96
+ "host": "smtp.elasticemail.com",
97
+ "port": 465,
98
+ "secure": true
99
+ },
100
+
45
101
  "Ethereal": {
102
+ "description": "Ethereal Email (email testing service)",
46
103
  "aliases": ["ethereal.email"],
47
104
  "host": "smtp.ethereal.email",
48
105
  "port": 587
49
106
  },
50
107
 
51
108
  "FastMail": {
109
+ "description": "FastMail",
52
110
  "domains": ["fastmail.fm"],
53
111
  "host": "smtp.fastmail.com",
54
112
  "port": 465,
55
113
  "secure": true
56
114
  },
57
115
 
58
- "Forward Email": {
59
- "aliases": ["FE", "ForwardEmail"],
60
- "domains": ["forwardemail.net"],
61
- "host": "smtp.forwardemail.net",
62
- "port": 465,
63
- "secure": true
64
- },
65
-
66
116
  "Feishu Mail": {
117
+ "description": "Feishu Mail (Lark)",
67
118
  "aliases": ["Feishu", "FeishuMail"],
68
119
  "domains": ["www.feishu.cn"],
69
120
  "host": "smtp.feishu.cn",
@@ -71,13 +122,24 @@
71
122
  "secure": true
72
123
  },
73
124
 
125
+ "Forward Email": {
126
+ "description": "Forward Email (email forwarding service)",
127
+ "aliases": ["FE", "ForwardEmail"],
128
+ "domains": ["forwardemail.net"],
129
+ "host": "smtp.forwardemail.net",
130
+ "port": 465,
131
+ "secure": true
132
+ },
133
+
74
134
  "GandiMail": {
135
+ "description": "Gandi Mail",
75
136
  "aliases": ["Gandi", "Gandi Mail"],
76
137
  "host": "mail.gandi.net",
77
138
  "port": 587
78
139
  },
79
140
 
80
141
  "Gmail": {
142
+ "description": "Gmail",
81
143
  "aliases": ["Google Mail"],
82
144
  "domains": ["gmail.com", "googlemail.com"],
83
145
  "host": "smtp.gmail.com",
@@ -85,26 +147,38 @@
85
147
  "secure": true
86
148
  },
87
149
 
150
+ "GMX": {
151
+ "description": "GMX Mail",
152
+ "domains": ["gmx.com", "gmx.net", "gmx.de"],
153
+ "host": "mail.gmx.com",
154
+ "port": 587
155
+ },
156
+
88
157
  "Godaddy": {
158
+ "description": "GoDaddy Email (US)",
89
159
  "host": "smtpout.secureserver.net",
90
160
  "port": 25
91
161
  },
92
162
 
93
163
  "GodaddyAsia": {
164
+ "description": "GoDaddy Email (Asia)",
94
165
  "host": "smtp.asia.secureserver.net",
95
166
  "port": 25
96
167
  },
97
168
 
98
169
  "GodaddyEurope": {
170
+ "description": "GoDaddy Email (Europe)",
99
171
  "host": "smtp.europe.secureserver.net",
100
172
  "port": 25
101
173
  },
102
174
 
103
175
  "hot.ee": {
176
+ "description": "Hot.ee (Estonian email provider)",
104
177
  "host": "mail.hot.ee"
105
178
  },
106
179
 
107
180
  "Hotmail": {
181
+ "description": "Outlook.com / Hotmail",
108
182
  "aliases": ["Outlook", "Outlook.com", "Hotmail.com"],
109
183
  "domains": ["hotmail.com", "outlook.com"],
110
184
  "host": "smtp-mail.outlook.com",
@@ -112,6 +186,7 @@
112
186
  },
113
187
 
114
188
  "iCloud": {
189
+ "description": "iCloud Mail",
115
190
  "aliases": ["Me", "Mac"],
116
191
  "domains": ["me.com", "mac.com"],
117
192
  "host": "smtp.mail.me.com",
@@ -119,72 +194,117 @@
119
194
  },
120
195
 
121
196
  "Infomaniak": {
197
+ "description": "Infomaniak Mail (Swiss hosting provider)",
122
198
  "host": "mail.infomaniak.com",
123
199
  "domains": ["ik.me", "ikmail.com", "etik.com"],
124
200
  "port": 587
125
201
  },
202
+
203
+ "KolabNow": {
204
+ "description": "KolabNow (secure email service)",
205
+ "domains": ["kolabnow.com"],
206
+ "aliases": ["Kolab"],
207
+ "host": "smtp.kolabnow.com",
208
+ "port": 465,
209
+ "secure": true,
210
+ "authMethod": "LOGIN"
211
+ },
212
+
126
213
  "Loopia": {
214
+ "description": "Loopia (Swedish hosting provider)",
127
215
  "host": "mailcluster.loopia.se",
128
216
  "port": 465
129
217
  },
218
+
219
+ "Loops": {
220
+ "description": "Loops",
221
+ "host": "smtp.loops.so",
222
+ "port": 587
223
+ },
224
+
130
225
  "mail.ee": {
226
+ "description": "Mail.ee (Estonian email provider)",
131
227
  "host": "smtp.mail.ee"
132
228
  },
133
229
 
134
230
  "Mail.ru": {
231
+ "description": "Mail.ru",
135
232
  "host": "smtp.mail.ru",
136
233
  "port": 465,
137
234
  "secure": true
138
235
  },
139
236
 
140
237
  "Mailcatch.app": {
238
+ "description": "Mailcatch (email testing service)",
141
239
  "host": "sandbox-smtp.mailcatch.app",
142
240
  "port": 2525
143
241
  },
144
242
 
145
243
  "Maildev": {
244
+ "description": "MailDev (local email testing)",
146
245
  "port": 1025,
147
246
  "ignoreTLS": true
148
247
  },
149
248
 
249
+ "MailerSend": {
250
+ "description": "MailerSend",
251
+ "host": "smtp.mailersend.net",
252
+ "port": 587
253
+ },
254
+
150
255
  "Mailgun": {
256
+ "description": "Mailgun",
151
257
  "host": "smtp.mailgun.org",
152
258
  "port": 465,
153
259
  "secure": true
154
260
  },
155
261
 
156
262
  "Mailjet": {
263
+ "description": "Mailjet",
157
264
  "host": "in.mailjet.com",
158
265
  "port": 587
159
266
  },
160
267
 
161
268
  "Mailosaur": {
269
+ "description": "Mailosaur (email testing service)",
162
270
  "host": "mailosaur.io",
163
271
  "port": 25
164
272
  },
165
273
 
166
274
  "Mailtrap": {
275
+ "description": "Mailtrap",
167
276
  "host": "live.smtp.mailtrap.io",
168
277
  "port": 587
169
278
  },
170
279
 
171
280
  "Mandrill": {
281
+ "description": "Mandrill (by Mailchimp)",
172
282
  "host": "smtp.mandrillapp.com",
173
283
  "port": 587
174
284
  },
175
285
 
176
286
  "Naver": {
287
+ "description": "Naver Mail (Korean email provider)",
177
288
  "host": "smtp.naver.com",
178
289
  "port": 587
179
290
  },
180
291
 
292
+ "OhMySMTP": {
293
+ "description": "OhMySMTP (email delivery service)",
294
+ "host": "smtp.ohmysmtp.com",
295
+ "port": 587,
296
+ "secure": false
297
+ },
298
+
181
299
  "One": {
300
+ "description": "One.com Email",
182
301
  "host": "send.one.com",
183
302
  "port": 465,
184
303
  "secure": true
185
304
  },
186
305
 
187
306
  "OpenMailBox": {
307
+ "description": "OpenMailBox",
188
308
  "aliases": ["OMB", "openmailbox.org"],
189
309
  "host": "smtp.openmailbox.org",
190
310
  "port": 465,
@@ -192,24 +312,21 @@
192
312
  },
193
313
 
194
314
  "Outlook365": {
315
+ "description": "Microsoft 365 / Office 365",
195
316
  "host": "smtp.office365.com",
196
317
  "port": 587,
197
318
  "secure": false
198
319
  },
199
320
 
200
- "OhMySMTP": {
201
- "host": "smtp.ohmysmtp.com",
202
- "port": 587,
203
- "secure": false
204
- },
205
-
206
321
  "Postmark": {
322
+ "description": "Postmark",
207
323
  "aliases": ["PostmarkApp"],
208
324
  "host": "smtp.postmarkapp.com",
209
325
  "port": 2525
210
326
  },
211
327
 
212
328
  "Proton": {
329
+ "description": "Proton Mail",
213
330
  "aliases": ["ProtonMail", "Proton.me", "Protonmail.com", "Protonmail.ch"],
214
331
  "domains": ["proton.me", "protonmail.com", "pm.me", "protonmail.ch"],
215
332
  "host": "smtp.protonmail.ch",
@@ -218,12 +335,14 @@
218
335
  },
219
336
 
220
337
  "qiye.aliyun": {
338
+ "description": "Alibaba Mail Enterprise Edition",
221
339
  "host": "smtp.mxhichina.com",
222
340
  "port": "465",
223
341
  "secure": true
224
342
  },
225
343
 
226
344
  "QQ": {
345
+ "description": "QQ Mail",
227
346
  "domains": ["qq.com"],
228
347
  "host": "smtp.qq.com",
229
348
  "port": 465,
@@ -231,6 +350,7 @@
231
350
  },
232
351
 
233
352
  "QQex": {
353
+ "description": "QQ Enterprise Mail",
234
354
  "aliases": ["QQ Enterprise"],
235
355
  "domains": ["exmail.qq.com"],
236
356
  "host": "smtp.exmail.qq.com",
@@ -238,89 +358,189 @@
238
358
  "secure": true
239
359
  },
240
360
 
361
+ "Resend": {
362
+ "description": "Resend",
363
+ "host": "smtp.resend.com",
364
+ "port": 465,
365
+ "secure": true
366
+ },
367
+
368
+ "Runbox": {
369
+ "description": "Runbox (Norwegian email provider)",
370
+ "domains": ["runbox.com"],
371
+ "host": "smtp.runbox.com",
372
+ "port": 465,
373
+ "secure": true
374
+ },
375
+
241
376
  "SendCloud": {
377
+ "description": "SendCloud (Chinese email delivery)",
242
378
  "host": "smtp.sendcloud.net",
243
379
  "port": 2525
244
380
  },
245
381
 
246
382
  "SendGrid": {
383
+ "description": "SendGrid",
247
384
  "host": "smtp.sendgrid.net",
248
385
  "port": 587
249
386
  },
250
387
 
251
388
  "SendinBlue": {
389
+ "description": "Brevo (formerly Sendinblue)",
252
390
  "aliases": ["Brevo"],
253
391
  "host": "smtp-relay.brevo.com",
254
392
  "port": 587
255
393
  },
256
394
 
257
395
  "SendPulse": {
396
+ "description": "SendPulse",
258
397
  "host": "smtp-pulse.com",
259
398
  "port": 465,
260
399
  "secure": true
261
400
  },
262
401
 
263
402
  "SES": {
403
+ "description": "AWS SES US East (N. Virginia)",
264
404
  "host": "email-smtp.us-east-1.amazonaws.com",
265
405
  "port": 465,
266
406
  "secure": true
267
407
  },
268
408
 
269
- "SES-US-EAST-1": {
270
- "host": "email-smtp.us-east-1.amazonaws.com",
409
+ "SES-AP-NORTHEAST-1": {
410
+ "description": "AWS SES Asia Pacific (Tokyo)",
411
+ "host": "email-smtp.ap-northeast-1.amazonaws.com",
271
412
  "port": 465,
272
413
  "secure": true
273
414
  },
274
415
 
275
- "SES-US-WEST-2": {
276
- "host": "email-smtp.us-west-2.amazonaws.com",
416
+ "SES-AP-NORTHEAST-2": {
417
+ "description": "AWS SES Asia Pacific (Seoul)",
418
+ "host": "email-smtp.ap-northeast-2.amazonaws.com",
277
419
  "port": 465,
278
420
  "secure": true
279
421
  },
280
422
 
281
- "SES-EU-WEST-1": {
282
- "host": "email-smtp.eu-west-1.amazonaws.com",
423
+ "SES-AP-NORTHEAST-3": {
424
+ "description": "AWS SES Asia Pacific (Osaka)",
425
+ "host": "email-smtp.ap-northeast-3.amazonaws.com",
283
426
  "port": 465,
284
427
  "secure": true
285
428
  },
286
429
 
287
430
  "SES-AP-SOUTH-1": {
431
+ "description": "AWS SES Asia Pacific (Mumbai)",
288
432
  "host": "email-smtp.ap-south-1.amazonaws.com",
289
433
  "port": 465,
290
434
  "secure": true
291
435
  },
292
436
 
293
- "SES-AP-NORTHEAST-1": {
294
- "host": "email-smtp.ap-northeast-1.amazonaws.com",
437
+ "SES-AP-SOUTHEAST-1": {
438
+ "description": "AWS SES Asia Pacific (Singapore)",
439
+ "host": "email-smtp.ap-southeast-1.amazonaws.com",
295
440
  "port": 465,
296
441
  "secure": true
297
442
  },
298
443
 
299
- "SES-AP-NORTHEAST-2": {
300
- "host": "email-smtp.ap-northeast-2.amazonaws.com",
444
+ "SES-AP-SOUTHEAST-2": {
445
+ "description": "AWS SES Asia Pacific (Sydney)",
446
+ "host": "email-smtp.ap-southeast-2.amazonaws.com",
301
447
  "port": 465,
302
448
  "secure": true
303
449
  },
304
450
 
305
- "SES-AP-NORTHEAST-3": {
306
- "host": "email-smtp.ap-northeast-3.amazonaws.com",
451
+ "SES-CA-CENTRAL-1": {
452
+ "description": "AWS SES Canada (Central)",
453
+ "host": "email-smtp.ca-central-1.amazonaws.com",
307
454
  "port": 465,
308
455
  "secure": true
309
456
  },
310
457
 
311
- "SES-AP-SOUTHEAST-1": {
312
- "host": "email-smtp.ap-southeast-1.amazonaws.com",
458
+ "SES-EU-CENTRAL-1": {
459
+ "description": "AWS SES Europe (Frankfurt)",
460
+ "host": "email-smtp.eu-central-1.amazonaws.com",
313
461
  "port": 465,
314
462
  "secure": true
315
463
  },
316
464
 
317
- "SES-AP-SOUTHEAST-2": {
318
- "host": "email-smtp.ap-southeast-2.amazonaws.com",
465
+ "SES-EU-NORTH-1": {
466
+ "description": "AWS SES Europe (Stockholm)",
467
+ "host": "email-smtp.eu-north-1.amazonaws.com",
468
+ "port": 465,
469
+ "secure": true
470
+ },
471
+
472
+ "SES-EU-WEST-1": {
473
+ "description": "AWS SES Europe (Ireland)",
474
+ "host": "email-smtp.eu-west-1.amazonaws.com",
475
+ "port": 465,
476
+ "secure": true
477
+ },
478
+
479
+ "SES-EU-WEST-2": {
480
+ "description": "AWS SES Europe (London)",
481
+ "host": "email-smtp.eu-west-2.amazonaws.com",
482
+ "port": 465,
483
+ "secure": true
484
+ },
485
+
486
+ "SES-EU-WEST-3": {
487
+ "description": "AWS SES Europe (Paris)",
488
+ "host": "email-smtp.eu-west-3.amazonaws.com",
489
+ "port": 465,
490
+ "secure": true
491
+ },
492
+
493
+ "SES-SA-EAST-1": {
494
+ "description": "AWS SES South America (São Paulo)",
495
+ "host": "email-smtp.sa-east-1.amazonaws.com",
496
+ "port": 465,
497
+ "secure": true
498
+ },
499
+
500
+ "SES-US-EAST-1": {
501
+ "description": "AWS SES US East (N. Virginia)",
502
+ "host": "email-smtp.us-east-1.amazonaws.com",
503
+ "port": 465,
504
+ "secure": true
505
+ },
506
+
507
+ "SES-US-EAST-2": {
508
+ "description": "AWS SES US East (Ohio)",
509
+ "host": "email-smtp.us-east-2.amazonaws.com",
510
+ "port": 465,
511
+ "secure": true
512
+ },
513
+
514
+ "SES-US-GOV-EAST-1": {
515
+ "description": "AWS SES GovCloud (US-East)",
516
+ "host": "email-smtp.us-gov-east-1.amazonaws.com",
517
+ "port": 465,
518
+ "secure": true
519
+ },
520
+
521
+ "SES-US-GOV-WEST-1": {
522
+ "description": "AWS SES GovCloud (US-West)",
523
+ "host": "email-smtp.us-gov-west-1.amazonaws.com",
524
+ "port": 465,
525
+ "secure": true
526
+ },
527
+
528
+ "SES-US-WEST-1": {
529
+ "description": "AWS SES US West (N. California)",
530
+ "host": "email-smtp.us-west-1.amazonaws.com",
531
+ "port": 465,
532
+ "secure": true
533
+ },
534
+
535
+ "SES-US-WEST-2": {
536
+ "description": "AWS SES US West (Oregon)",
537
+ "host": "email-smtp.us-west-2.amazonaws.com",
319
538
  "port": 465,
320
539
  "secure": true
321
540
  },
322
541
 
323
542
  "Seznam": {
543
+ "description": "Seznam Email (Czech email provider)",
324
544
  "aliases": ["Seznam Email"],
325
545
  "domains": ["seznam.cz", "email.cz", "post.cz", "spoluzaci.cz"],
326
546
  "host": "smtp.seznam.cz",
@@ -328,7 +548,14 @@
328
548
  "secure": true
329
549
  },
330
550
 
551
+ "SMTP2GO": {
552
+ "description": "SMTP2GO",
553
+ "host": "mail.smtp2go.com",
554
+ "port": 2525
555
+ },
556
+
331
557
  "Sparkpost": {
558
+ "description": "SparkPost",
332
559
  "aliases": ["SparkPost", "SparkPost Mail"],
333
560
  "domains": ["sparkpost.com"],
334
561
  "host": "smtp.sparkpostmail.com",
@@ -337,11 +564,21 @@
337
564
  },
338
565
 
339
566
  "Tipimail": {
567
+ "description": "Tipimail (email delivery service)",
340
568
  "host": "smtp.tipimail.com",
341
569
  "port": 587
342
570
  },
343
571
 
572
+ "Tutanota": {
573
+ "description": "Tutanota (Tuta Mail)",
574
+ "domains": ["tutanota.com", "tuta.com", "tutanota.de", "tuta.io"],
575
+ "host": "smtp.tutanota.com",
576
+ "port": 465,
577
+ "secure": true
578
+ },
579
+
344
580
  "Yahoo": {
581
+ "description": "Yahoo Mail",
345
582
  "domains": ["yahoo.com"],
346
583
  "host": "smtp.mail.yahoo.com",
347
584
  "port": 465,
@@ -349,28 +586,26 @@
349
586
  },
350
587
 
351
588
  "Yandex": {
589
+ "description": "Yandex Mail",
352
590
  "domains": ["yandex.ru"],
353
591
  "host": "smtp.yandex.ru",
354
592
  "port": 465,
355
593
  "secure": true
356
594
  },
357
595
 
596
+ "Zimbra": {
597
+ "description": "Zimbra Mail Server",
598
+ "aliases": ["Zimbra Collaboration"],
599
+ "host": "smtp.zimbra.com",
600
+ "port": 587,
601
+ "requireTLS": true
602
+ },
603
+
358
604
  "Zoho": {
605
+ "description": "Zoho Mail",
359
606
  "host": "smtp.zoho.com",
360
607
  "port": 465,
361
608
  "secure": true,
362
609
  "authMethod": "LOGIN"
363
- },
364
-
365
- "126": {
366
- "host": "smtp.126.com",
367
- "port": 465,
368
- "secure": true
369
- },
370
-
371
- "163": {
372
- "host": "smtp.163.com",
373
- "port": 465,
374
- "secure": true
375
610
  }
376
611
  }
@@ -72,6 +72,9 @@ class XOAuth2 extends Stream {
72
72
  let timeout = Math.max(Number(this.options.timeout) || 0, 0);
73
73
  this.expires = (timeout && Date.now() + timeout * 1000) || 0;
74
74
  }
75
+
76
+ this.renewing = false; // Track if renewal is in progress
77
+ this.renewalQueue = []; // Queue for pending requests during renewal
75
78
  }
76
79
 
77
80
  /**
@@ -82,14 +85,61 @@ class XOAuth2 extends Stream {
82
85
  */
83
86
  getToken(renew, callback) {
84
87
  if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
88
+ this.logger.debug(
89
+ {
90
+ tnx: 'OAUTH2',
91
+ user: this.options.user,
92
+ action: 'reuse'
93
+ },
94
+ 'Reusing existing access token for %s',
95
+ this.options.user
96
+ );
85
97
  return callback(null, this.accessToken);
86
98
  }
87
99
 
88
- let generateCallback = (...args) => {
89
- if (args[0]) {
100
+ // check if it is possible to renew, if not, return the current token or error
101
+ if (!this.provisionCallback && !this.options.refreshToken && !this.options.serviceClient) {
102
+ if (this.accessToken) {
103
+ this.logger.debug(
104
+ {
105
+ tnx: 'OAUTH2',
106
+ user: this.options.user,
107
+ action: 'reuse'
108
+ },
109
+ 'Reusing existing access token (no refresh capability) for %s',
110
+ this.options.user
111
+ );
112
+ return callback(null, this.accessToken);
113
+ }
114
+ this.logger.error(
115
+ {
116
+ tnx: 'OAUTH2',
117
+ user: this.options.user,
118
+ action: 'renew'
119
+ },
120
+ 'Cannot renew access token for %s: No refresh mechanism available',
121
+ this.options.user
122
+ );
123
+ return callback(new Error("Can't create new access token for user"));
124
+ }
125
+
126
+ // If renewal already in progress, queue this request instead of starting another
127
+ if (this.renewing) {
128
+ return this.renewalQueue.push({ renew, callback });
129
+ }
130
+
131
+ this.renewing = true;
132
+
133
+ // Handles token renewal completion - processes queued requests and cleans up
134
+ const generateCallback = (err, accessToken) => {
135
+ this.renewalQueue.forEach(item => item.callback(err, accessToken));
136
+ this.renewalQueue = [];
137
+ this.renewing = false;
138
+
139
+ if (err) {
90
140
  this.logger.error(
91
141
  {
92
- err: args[0],
142
+ err,
93
143
  tnx: 'OAUTH2',
94
144
  user: this.options.user,
95
145
  action: 'renew'
@@ -108,7 +158,8 @@ class XOAuth2 extends Stream {
108
158
  this.options.user
109
159
  );
110
160
  }
111
- callback(...args);
161
+ // Complete original request
162
+ callback(err, accessToken);
112
163
  };
113
164
 
114
165
  if (this.provisionCallback) {
@@ -167,7 +218,7 @@ class XOAuth2 extends Stream {
167
218
  try {
168
219
  token = this.jwtSignRS256(tokenData);
169
220
  } catch (err) {
170
- return callback(new Error('Can\x27t generate token. Check your auth options'));
221
+ return callback(new Error("Can't generate token. Check your auth options"));
171
222
  }
172
223
 
173
224
  urlOptions = {
@@ -181,7 +232,7 @@ class XOAuth2 extends Stream {
181
232
  };
182
233
  } else {
183
234
  if (!this.options.refreshToken) {
184
- return callback(new Error('Can\x27t create new access token for user'));
235
+ return callback(new Error("Can't create new access token for user"));
185
236
  }
186
237
 
187
238
  // web app - https://developers.google.com/identity/protocols/OAuth2WebServer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "7.0.4",
3
+ "version": "7.0.6",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "homepage": "https://nodemailer.com/",
25
25
  "devDependencies": {
26
- "@aws-sdk/client-sesv2": "3.839.0",
26
+ "@aws-sdk/client-sesv2": "3.876.0",
27
27
  "bunyan": "1.8.15",
28
28
  "c8": "10.1.3",
29
29
  "eslint": "8.57.0",