nodemailer 8.0.8 → 8.0.9

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,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [8.0.9](https://github.com/nodemailer/nodemailer/compare/v8.0.8...v8.0.9) (2026-05-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * two pending security advisories (jsonTransport access bypass, List-* CRLF injection) ([#1820](https://github.com/nodemailer/nodemailer/issues/1820)) ([5f69497](https://github.com/nodemailer/nodemailer/commit/5f694977da2e0e13dc947037566e8e689a01217e))
9
+
3
10
  ## [8.0.8](https://github.com/nodemailer/nodemailer/compare/v8.0.7...v8.0.8) (2026-05-23)
4
11
 
5
12
 
@@ -407,31 +407,39 @@ class Mail extends EventEmitter {
407
407
  if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
408
408
  return callback();
409
409
  }
410
- mail.resolveContent(mail.data, 'html', (err, html) => {
411
- if (err) {
412
- return callback(err);
410
+ mail.resolveContent(
411
+ mail.data,
412
+ 'html',
413
+ { disableFileAccess: mail.data.disableFileAccess, disableUrlAccess: mail.data.disableUrlAccess },
414
+ (err, html) => {
415
+ if (err) {
416
+ return callback(err);
417
+ }
418
+ let cidCounter = 0;
419
+ html = (html || '')
420
+ .toString()
421
+ .replace(
422
+ /(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi,
423
+ (match, prefix, dataUri, mimeType) => {
424
+ const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
425
+ if (!mail.data.attachments) {
426
+ mail.data.attachments = [];
427
+ }
428
+ if (!Array.isArray(mail.data.attachments)) {
429
+ mail.data.attachments = [].concat(mail.data.attachments || []);
430
+ }
431
+ mail.data.attachments.push({
432
+ path: dataUri,
433
+ cid,
434
+ filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
435
+ });
436
+ return prefix + 'cid:' + cid;
437
+ }
438
+ );
439
+ mail.data.html = html;
440
+ callback();
413
441
  }
414
- let cidCounter = 0;
415
- html = (html || '')
416
- .toString()
417
- .replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
418
- const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
419
- if (!mail.data.attachments) {
420
- mail.data.attachments = [];
421
- }
422
- if (!Array.isArray(mail.data.attachments)) {
423
- mail.data.attachments = [].concat(mail.data.attachments || []);
424
- }
425
- mail.data.attachments.push({
426
- path: dataUri,
427
- cid,
428
- filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
429
- });
430
- return prefix + 'cid:' + cid;
431
- });
432
- mail.data.html = html;
433
- callback();
434
- });
442
+ );
435
443
  }
436
444
 
437
445
  set(key, value) {
@@ -111,25 +111,29 @@ class MailMessage {
111
111
  if (!args[0] || !args[0][args[1]]) {
112
112
  return resolveNext();
113
113
  }
114
- shared.resolveContent(...args, (err, value) => {
115
- if (err) {
116
- return callback(err);
117
- }
114
+ shared.resolveContent(
115
+ ...args,
116
+ { disableFileAccess: this.data.disableFileAccess, disableUrlAccess: this.data.disableUrlAccess },
117
+ (err, value) => {
118
+ if (err) {
119
+ return callback(err);
120
+ }
118
121
 
119
- const node = {
120
- content: value
121
- };
122
- if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
123
- Object.keys(args[0][args[1]]).forEach(key => {
124
- if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
125
- node[key] = args[0][args[1]][key];
126
- }
127
- });
128
- }
122
+ const node = {
123
+ content: value
124
+ };
125
+ if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
126
+ Object.keys(args[0][args[1]]).forEach(key => {
127
+ if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
128
+ node[key] = args[0][args[1]][key];
129
+ }
130
+ });
131
+ }
129
132
 
130
- args[0][args[1]] = node;
131
- resolveNext();
132
- });
133
+ args[0][args[1]] = node;
134
+ resolveNext();
135
+ }
136
+ );
133
137
  };
134
138
 
135
139
  setImmediate(() => resolveNext());
@@ -269,7 +273,8 @@ class MailMessage {
269
273
  if (value && value.url) {
270
274
  if (key.toLowerCase().trim() === 'id') {
271
275
  // List-ID: "comment" <domain>
272
- let comment = value.comment || '';
276
+ // strip CR/LF so a comment can't inject extra header lines
277
+ let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
273
278
  if (mimeFuncs.isPlainText(comment)) {
274
279
  comment = '"' + comment + '"';
275
280
  } else {
@@ -280,7 +285,8 @@ class MailMessage {
280
285
  }
281
286
 
282
287
  // List-*: <http://domain> (comment)
283
- let comment = value.comment || '';
288
+ // strip CR/LF so a comment can't inject extra header lines
289
+ let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
284
290
  if (!mimeFuncs.isPlainText(comment)) {
285
291
  comment = mimeFuncs.encodeWord(comment);
286
292
  }
@@ -6,6 +6,7 @@ const urllib = require('url');
6
6
  const util = require('util');
7
7
  const fs = require('fs');
8
8
  const nmfetch = require('../fetch');
9
+ const errors = require('../errors');
9
10
  const dns = require('dns');
10
11
  const net = require('net');
11
12
  const os = require('os');
@@ -501,9 +502,17 @@ module.exports.parseDataURI = uri => {
501
502
  *
502
503
  * @param {Object} data An object or an Array you want to resolve an element for
503
504
  * @param {String|Number} key Property name or an Array index
505
+ * @param {Object} [options] Optional access policy: { disableFileAccess, disableUrlAccess }
504
506
  * @param {Function} callback Callback function with (err, value)
505
507
  */
506
- module.exports.resolveContent = (data, key, callback) => {
508
+ module.exports.resolveContent = (data, key, options, callback) => {
509
+ // options is optional; support the legacy resolveContent(data, key, callback) signature
510
+ if (!callback && typeof options === 'function') {
511
+ callback = options;
512
+ options = false;
513
+ }
514
+ options = options || {};
515
+
507
516
  let promise;
508
517
 
509
518
  if (!callback) {
@@ -538,6 +547,13 @@ module.exports.resolveContent = (data, key, callback) => {
538
547
  callback(null, value);
539
548
  });
540
549
  } else if (/^https?:\/\//i.test(content.path || content.href)) {
550
+ if (options.disableUrlAccess) {
551
+ return setImmediate(() => {
552
+ const err = new Error('Url access rejected for ' + (content.path || content.href));
553
+ err.code = errors.EURLACCESS;
554
+ callback(err);
555
+ });
556
+ }
541
557
  return resolveStream(nmfetch(content.path || content.href), callback);
542
558
  } else if (/^data:/i.test(content.path || content.href)) {
543
559
  const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
@@ -547,6 +563,13 @@ module.exports.resolveContent = (data, key, callback) => {
547
563
  }
548
564
  return callback(null, parsedDataUri.data);
549
565
  } else if (content.path) {
566
+ if (options.disableFileAccess) {
567
+ return setImmediate(() => {
568
+ const err = new Error('File access rejected for ' + content.path);
569
+ err.code = errors.EFILEACCESS;
570
+ callback(err);
571
+ });
572
+ }
550
573
  return resolveStream(fs.createReadStream(content.path), callback);
551
574
  }
552
575
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "8.0.8",
3
+ "version": "8.0.9",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {