nodemailer 10.0.2 → 10.0.4

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,23 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.0.4](https://github.com/nodemailer/nodemailer/compare/v10.0.3...v10.0.4) (2026-09-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **fetch:** scope a cookie without a Path to the RFC 6265 default path ([2f907cb](https://github.com/nodemailer/nodemailer/commit/2f907cb71396860d0b2534dc72c3d252ef16339d))
9
+ * **fetch:** send cookies set with Path back to the exact path ([#1861](https://github.com/nodemailer/nodemailer/issues/1861)) ([d557113](https://github.com/nodemailer/nodemailer/commit/d5571139750de47abbac73a3b07ae90ff39a7bdd))
10
+ * **mail-composer:** keep httpHeaders and tls for href alternatives and icalEvent ([#1862](https://github.com/nodemailer/nodemailer/issues/1862)) ([7f502be](https://github.com/nodemailer/nodemailer/commit/7f502bee4a6b0386099c66c0a35d92850ec462f7))
11
+ * resolve well-known services by their primary domains ([#1859](https://github.com/nodemailer/nodemailer/issues/1859)) ([085f525](https://github.com/nodemailer/nodemailer/commit/085f525e6b292f0ca32ab1b20d4a4ee82fc40e2d))
12
+ * **ses-transport:** throw a configuration error when the SES client is missing ([#1863](https://github.com/nodemailer/nodemailer/issues/1863)) ([4d9c4c9](https://github.com/nodemailer/nodemailer/commit/4d9c4c966aaca61ff6d7649a1ac0aa0ceac6ef1e))
13
+
14
+ ## [10.0.3](https://github.com/nodemailer/nodemailer/compare/v10.0.2...v10.0.3) (2026-09-10)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **fetch:** honor the cookie Domain attribute without accepting public suffixes ([1608391](https://github.com/nodemailer/nodemailer/commit/1608391ff4ccd5422a88e4cf760b26068aad6d39)), closes [#1856](https://github.com/nodemailer/nodemailer/issues/1856)
20
+
3
21
  ## [10.0.2](https://github.com/nodemailer/nodemailer/compare/v10.0.1...v10.0.2) (2026-09-09)
4
22
 
5
23
 
@@ -85,10 +85,12 @@ export default class Cookies {
85
85
  */
86
86
  isExpired(cookie: Cookie): boolean;
87
87
  /**
88
- * Returns normalized cookie path for an URL path argument
88
+ * Returns the default path for an URL path argument, the default-path of
89
+ * RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
90
+ * to the directory of the URL it was set from
89
91
  *
90
92
  * @param pathname
91
- * @returns Normalized path
93
+ * @returns Default path
92
94
  */
93
95
  getPath(pathname?: string | null): string;
94
96
  }
@@ -33,7 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  return result;
34
34
  };
35
35
  })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
36
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
+ const node_net_1 = __importDefault(require("node:net"));
37
41
  const urllib = __importStar(require("../shared/url.js"));
38
42
  const SESSION_TIMEOUT = 1800; // 30 min
39
43
  /**
@@ -59,12 +63,19 @@ class Cookies {
59
63
  let domain;
60
64
  if (cookie.domain) {
61
65
  domain = cookie.domain.replace(/^\./, '');
62
- // do not allow cross origin cookies
66
+ // do not allow cross origin cookies. There is no public suffix list here, so a
67
+ // multi-label suffix like 'co.uk' can not be told apart from a registrable domain
63
68
  if (
64
69
  // can't be valid if the requested domain is shorter than current hostname
65
70
  urlparts.hostname.length < domain.length ||
71
+ // a top level domain is not a valid scope, 'Domain=com' would otherwise be
72
+ // sent to every .com host. A trailing dot does not make 'com.' any better
73
+ domain.indexOf('.') < 0 ||
74
+ domain.endsWith('.') ||
75
+ // an IP address has no subdomains, so cookies set on it stay host-only
76
+ node_net_1.default.isIP(urlparts.hostname) ||
66
77
  // prefix domains with dot to be sure that partial matches are not used
67
- ('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + domain) {
78
+ !('.' + urlparts.hostname).endsWith('.' + domain)) {
68
79
  cookie.domain = urlparts.hostname;
69
80
  }
70
81
  }
@@ -184,9 +195,13 @@ class Cookies {
184
195
  ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
185
196
  return false;
186
197
  }
187
- // check if path matches
188
- const path = this.getPath(urlparts.pathname);
189
- if (path.substr(0, cookie.path.length) !== cookie.path) {
198
+ // check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
199
+ // identical paths match, otherwise the cookie path must be a directory prefix
200
+ const pathname = urlparts.pathname || '/';
201
+ const cookiePath = cookie.path;
202
+ const pathMatches = pathname === cookiePath ||
203
+ (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
204
+ if (!pathMatches) {
190
205
  return false;
191
206
  }
192
207
  // check secure argument
@@ -243,22 +258,21 @@ class Cookies {
243
258
  return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
244
259
  }
245
260
  /**
246
- * Returns normalized cookie path for an URL path argument
261
+ * Returns the default path for an URL path argument, the default-path of
262
+ * RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
263
+ * to the directory of the URL it was set from
247
264
  *
248
265
  * @param pathname
249
- * @returns Normalized path
266
+ * @returns Default path
250
267
  */
251
268
  getPath(pathname) {
252
269
  const pathParts = (pathname || '/').split('/');
253
270
  pathParts.pop(); // remove filename part
254
- let path = pathParts.join('/').trim();
255
- // ensure path prefix /
271
+ const path = pathParts.join('/').trim();
272
+ // a path that holds no more than one '/' is scoped to the root path, and so
273
+ // is one that does not start with '/' at all
256
274
  if (path.charAt(0) !== '/') {
257
- path = '/' + path;
258
- }
259
- // ensure path suffix /
260
- if (path.substr(-1) !== '/') {
261
- path += '/';
275
+ return '/';
262
276
  }
263
277
  return path;
264
278
  }
@@ -253,7 +253,8 @@ class MailComposer {
253
253
  else if (icalEvent.href) {
254
254
  icalEvent.content = {
255
255
  href: icalEvent.href,
256
- httpHeaders: icalEvent.httpHeaders
256
+ httpHeaders: icalEvent.httpHeaders,
257
+ tls: icalEvent.tls
257
258
  };
258
259
  icalEvent.href = undefined;
259
260
  }
@@ -362,7 +363,9 @@ class MailComposer {
362
363
  }
363
364
  else if (alternative.href) {
364
365
  data.content = {
365
- href: alternative.href
366
+ href: alternative.href,
367
+ httpHeaders: alternative.httpHeaders,
368
+ tls: alternative.tls
366
369
  };
367
370
  }
368
371
  else {
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.2";
2
+ export declare const version = "10.0.4";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = 'nodemailer';
6
- exports.version = '10.0.2';
6
+ exports.version = '10.0.4';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -62,7 +62,11 @@ function tagSesError(err) {
62
62
  class SESTransport extends node_events_1.default {
63
63
  constructor(options) {
64
64
  super();
65
- options = options || {};
65
+ if (!options || !options.SES || !options.SES.sesClient) {
66
+ const error = new Error('Missing SES configuration, expecting { sesClient, SendEmailCommand } from @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/');
67
+ error.code = errors.ECONFIG;
68
+ throw error;
69
+ }
66
70
  this.options = options;
67
71
  this.ses = this.options.SES;
68
72
  this.name = 'SESTransport';
@@ -119,6 +119,7 @@ exports.services = {
119
119
  "FastMail": {
120
120
  "description": "FastMail",
121
121
  "domains": [
122
+ "fastmail.com",
122
123
  "fastmail.fm"
123
124
  ],
124
125
  "host": "smtp.fastmail.com",
@@ -232,6 +233,7 @@ exports.services = {
232
233
  "Mac"
233
234
  ],
234
235
  "domains": [
236
+ "icloud.com",
235
237
  "me.com",
236
238
  "mac.com"
237
239
  ],
@@ -85,10 +85,12 @@ export default class Cookies {
85
85
  */
86
86
  isExpired(cookie: Cookie): boolean;
87
87
  /**
88
- * Returns normalized cookie path for an URL path argument
88
+ * Returns the default path for an URL path argument, the default-path of
89
+ * RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
90
+ * to the directory of the URL it was set from
89
91
  *
90
92
  * @param pathname
91
- * @returns Normalized path
93
+ * @returns Default path
92
94
  */
93
95
  getPath(pathname?: string | null): string;
94
96
  }
@@ -1,4 +1,5 @@
1
1
  // module to handle cookies
2
+ import net from 'node:net';
2
3
  import * as urllib from '../shared/url.js';
3
4
  const SESSION_TIMEOUT = 1800; // 30 min
4
5
  /**
@@ -24,12 +25,19 @@ export default class Cookies {
24
25
  let domain;
25
26
  if (cookie.domain) {
26
27
  domain = cookie.domain.replace(/^\./, '');
27
- // do not allow cross origin cookies
28
+ // do not allow cross origin cookies. There is no public suffix list here, so a
29
+ // multi-label suffix like 'co.uk' can not be told apart from a registrable domain
28
30
  if (
29
31
  // can't be valid if the requested domain is shorter than current hostname
30
32
  urlparts.hostname.length < domain.length ||
33
+ // a top level domain is not a valid scope, 'Domain=com' would otherwise be
34
+ // sent to every .com host. A trailing dot does not make 'com.' any better
35
+ domain.indexOf('.') < 0 ||
36
+ domain.endsWith('.') ||
37
+ // an IP address has no subdomains, so cookies set on it stay host-only
38
+ net.isIP(urlparts.hostname) ||
31
39
  // prefix domains with dot to be sure that partial matches are not used
32
- ('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + domain) {
40
+ !('.' + urlparts.hostname).endsWith('.' + domain)) {
33
41
  cookie.domain = urlparts.hostname;
34
42
  }
35
43
  }
@@ -149,9 +157,13 @@ export default class Cookies {
149
157
  ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
150
158
  return false;
151
159
  }
152
- // check if path matches
153
- const path = this.getPath(urlparts.pathname);
154
- if (path.substr(0, cookie.path.length) !== cookie.path) {
160
+ // check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
161
+ // identical paths match, otherwise the cookie path must be a directory prefix
162
+ const pathname = urlparts.pathname || '/';
163
+ const cookiePath = cookie.path;
164
+ const pathMatches = pathname === cookiePath ||
165
+ (pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
166
+ if (!pathMatches) {
155
167
  return false;
156
168
  }
157
169
  // check secure argument
@@ -208,22 +220,21 @@ export default class Cookies {
208
220
  return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
209
221
  }
210
222
  /**
211
- * Returns normalized cookie path for an URL path argument
223
+ * Returns the default path for an URL path argument, the default-path of
224
+ * RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
225
+ * to the directory of the URL it was set from
212
226
  *
213
227
  * @param pathname
214
- * @returns Normalized path
228
+ * @returns Default path
215
229
  */
216
230
  getPath(pathname) {
217
231
  const pathParts = (pathname || '/').split('/');
218
232
  pathParts.pop(); // remove filename part
219
- let path = pathParts.join('/').trim();
220
- // ensure path prefix /
233
+ const path = pathParts.join('/').trim();
234
+ // a path that holds no more than one '/' is scoped to the root path, and so
235
+ // is one that does not start with '/' at all
221
236
  if (path.charAt(0) !== '/') {
222
- path = '/' + path;
223
- }
224
- // ensure path suffix /
225
- if (path.substr(-1) !== '/') {
226
- path += '/';
237
+ return '/';
227
238
  }
228
239
  return path;
229
240
  }
@@ -215,7 +215,8 @@ class MailComposer {
215
215
  else if (icalEvent.href) {
216
216
  icalEvent.content = {
217
217
  href: icalEvent.href,
218
- httpHeaders: icalEvent.httpHeaders
218
+ httpHeaders: icalEvent.httpHeaders,
219
+ tls: icalEvent.tls
219
220
  };
220
221
  icalEvent.href = undefined;
221
222
  }
@@ -324,7 +325,9 @@ class MailComposer {
324
325
  }
325
326
  else if (alternative.href) {
326
327
  data.content = {
327
- href: alternative.href
328
+ href: alternative.href,
329
+ httpHeaders: alternative.httpHeaders,
330
+ tls: alternative.tls
328
331
  };
329
332
  }
330
333
  else {
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.2";
2
+ export declare const version = "10.0.4";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = 'nodemailer';
3
- export const version = '10.0.2';
3
+ export const version = '10.0.4';
4
4
  export const homepage = 'https://nodemailer.com/';
@@ -24,7 +24,11 @@ function tagSesError(err) {
24
24
  class SESTransport extends EventEmitter {
25
25
  constructor(options) {
26
26
  super();
27
- options = options || {};
27
+ if (!options || !options.SES || !options.SES.sesClient) {
28
+ const error = new Error('Missing SES configuration, expecting { sesClient, SendEmailCommand } from @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/');
29
+ error.code = errors.ECONFIG;
30
+ throw error;
31
+ }
28
32
  this.options = options;
29
33
  this.ses = this.options.SES;
30
34
  this.name = 'SESTransport';
@@ -116,6 +116,7 @@ export const services = {
116
116
  "FastMail": {
117
117
  "description": "FastMail",
118
118
  "domains": [
119
+ "fastmail.com",
119
120
  "fastmail.fm"
120
121
  ],
121
122
  "host": "smtp.fastmail.com",
@@ -229,6 +230,7 @@ export const services = {
229
230
  "Mac"
230
231
  ],
231
232
  "domains": [
233
+ "icloud.com",
232
234
  "me.com",
233
235
  "mac.com"
234
236
  ],
@@ -107,7 +107,7 @@
107
107
 
108
108
  "FastMail": {
109
109
  "description": "FastMail",
110
- "domains": ["fastmail.fm"],
110
+ "domains": ["fastmail.com", "fastmail.fm"],
111
111
  "host": "smtp.fastmail.com",
112
112
  "port": 465,
113
113
  "secure": true
@@ -196,7 +196,7 @@
196
196
  "iCloud": {
197
197
  "description": "iCloud Mail",
198
198
  "aliases": ["Me", "Mac"],
199
- "domains": ["me.com", "mac.com"],
199
+ "domains": ["icloud.com", "me.com", "mac.com"],
200
200
  "host": "smtp.mail.me.com",
201
201
  "port": 587
202
202
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "10.0.2",
3
+ "version": "10.0.4",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/nodemailer.js",