nodemailer 8.0.8 → 8.0.10

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,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [8.0.10](https://github.com/nodemailer/nodemailer/compare/v8.0.9...v8.0.10) (2026-05-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * fall back to lower-severity handler when custom logger lacks a level method ([6d849df](https://github.com/nodemailer/nodemailer/commit/6d849df59a56184b48844ed10b5fb7b8e9f74634))
9
+
10
+ ## [8.0.9](https://github.com/nodemailer/nodemailer/compare/v8.0.8...v8.0.9) (2026-05-26)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * 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))
16
+
3
17
  ## [8.0.8](https://github.com/nodemailer/nodemailer/compare/v8.0.7...v8.0.8) (2026-05-23)
4
18
 
5
19
 
package/SECURITY.md ADDED
@@ -0,0 +1,59 @@
1
+ # Security Policy
2
+
3
+ Nodemailer is a widely deployed, zero-dependency e-mail library. We take security
4
+ reports seriously and aim to respond quickly.
5
+
6
+ ## Supported Versions
7
+
8
+ Security fixes are released only against the latest major version. We do not
9
+ backport patches to older majors — upgrading to the current release line is the
10
+ supported way to receive security updates.
11
+
12
+ | Version | Supported |
13
+ | ------- | ------------------ |
14
+ | 8.x | :white_check_mark: |
15
+ | < 8.0 | :x: |
16
+
17
+ If you are on an older major, please upgrade. See the migration notes at
18
+ <https://nodemailer.com/> before updating.
19
+
20
+ ## Reporting a Vulnerability
21
+
22
+ **Please do not report security vulnerabilities through public GitHub issues,
23
+ pull requests, or discussions.**
24
+
25
+ Report privately through one of the following channels:
26
+
27
+ 1. **GitHub Security Advisories (preferred).** Open a private report at
28
+ <https://github.com/nodemailer/nodemailer/security/advisories/new>. This keeps
29
+ the discussion private until a fix is published and lets us coordinate a CVE
30
+ and credit you.
31
+ 2. **Email.** Send details to **andris@reinman.eu** (the contact listed in
32
+ [`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible.
33
+
34
+ When reporting, please include as much of the following as you can:
35
+
36
+ - The affected version(s) and environment (Node.js version, OS).
37
+ - The component involved (e.g. SMTP connection, address parsing, MIME/header
38
+ generation, DKIM).
39
+ - A clear description of the issue and its impact (e.g. header/SMTP command
40
+ injection, information disclosure, DoS).
41
+ - A minimal proof of concept or reproduction steps.
42
+ - Any suggested remediation, if you have one.
43
+
44
+ Nodemailer is maintained by a single person, so there is no guaranteed response
45
+ time — sometimes reports are handled within hours, sometimes they take longer.
46
+ Accepted issues are fixed in a new release and coordinated through a GitHub
47
+ Security Advisory / CVE, and reporters who wish to be named are credited.
48
+
49
+ ## Scope
50
+
51
+ In scope: the `nodemailer` package source in this repository — message and MIME
52
+ generation, SMTP/LMTP client behaviour, address parsing, header handling, DKIM
53
+ signing, and the bundled transports.
54
+
55
+ Out of scope: vulnerabilities in your own application code, misconfiguration of
56
+ your mail server or credentials, social-engineering reports, and issues in
57
+ third-party services Nodemailer connects to.
58
+
59
+ Thank you for helping keep Nodemailer and its users safe.
@@ -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');
@@ -366,7 +367,16 @@ module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
366
367
  const entry = Object.assign({}, defaults || {}, data || {});
367
368
  delete entry.level;
368
369
 
369
- logger[level](entry, message, ...args);
370
+ let logLevel = level;
371
+ if (typeof logger[logLevel] !== 'function') {
372
+ // Provided logger does not implement this level. Fall back to a
373
+ // lower-severity handler instead of throwing.
374
+ logLevel = ['info', 'debug', 'log', 'trace', 'warn', 'error'].find(name => typeof logger[name] === 'function');
375
+ }
376
+
377
+ if (logLevel) {
378
+ logger[logLevel](entry, message, ...args);
379
+ }
370
380
  };
371
381
 
372
382
  /**
@@ -501,9 +511,17 @@ module.exports.parseDataURI = uri => {
501
511
  *
502
512
  * @param {Object} data An object or an Array you want to resolve an element for
503
513
  * @param {String|Number} key Property name or an Array index
514
+ * @param {Object} [options] Optional access policy: { disableFileAccess, disableUrlAccess }
504
515
  * @param {Function} callback Callback function with (err, value)
505
516
  */
506
- module.exports.resolveContent = (data, key, callback) => {
517
+ module.exports.resolveContent = (data, key, options, callback) => {
518
+ // options is optional; support the legacy resolveContent(data, key, callback) signature
519
+ if (!callback && typeof options === 'function') {
520
+ callback = options;
521
+ options = false;
522
+ }
523
+ options = options || {};
524
+
507
525
  let promise;
508
526
 
509
527
  if (!callback) {
@@ -538,6 +556,13 @@ module.exports.resolveContent = (data, key, callback) => {
538
556
  callback(null, value);
539
557
  });
540
558
  } else if (/^https?:\/\//i.test(content.path || content.href)) {
559
+ if (options.disableUrlAccess) {
560
+ return setImmediate(() => {
561
+ const err = new Error('Url access rejected for ' + (content.path || content.href));
562
+ err.code = errors.EURLACCESS;
563
+ callback(err);
564
+ });
565
+ }
541
566
  return resolveStream(nmfetch(content.path || content.href), callback);
542
567
  } else if (/^data:/i.test(content.path || content.href)) {
543
568
  const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
@@ -547,6 +572,13 @@ module.exports.resolveContent = (data, key, callback) => {
547
572
  }
548
573
  return callback(null, parsedDataUri.data);
549
574
  } else if (content.path) {
575
+ if (options.disableFileAccess) {
576
+ return setImmediate(() => {
577
+ const err = new Error('File access rejected for ' + content.path);
578
+ err.code = errors.EFILEACCESS;
579
+ callback(err);
580
+ });
581
+ }
550
582
  return resolveStream(fs.createReadStream(content.path), callback);
551
583
  }
552
584
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "8.0.8",
3
+ "version": "8.0.10",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {