nodemailer 10.0.3 → 10.0.5

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.5](https://github.com/nodemailer/nodemailer/compare/v10.0.4...v10.0.5) (2026-09-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **addressparser:** parse comment-joined addresses in linear time ([c07f175](https://github.com/nodemailer/nodemailer/commit/c07f17518d25aca8ab2ad66968dcbca538c24b89))
9
+
10
+ ## [10.0.4](https://github.com/nodemailer/nodemailer/compare/v10.0.3...v10.0.4) (2026-09-11)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **fetch:** scope a cookie without a Path to the RFC 6265 default path ([2f907cb](https://github.com/nodemailer/nodemailer/commit/2f907cb71396860d0b2534dc72c3d252ef16339d))
16
+ * **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))
17
+ * **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))
18
+ * resolve well-known services by their primary domains ([#1859](https://github.com/nodemailer/nodemailer/issues/1859)) ([085f525](https://github.com/nodemailer/nodemailer/commit/085f525e6b292f0ca32ab1b20d4a4ee82fc40e2d))
19
+ * **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))
20
+
3
21
  ## [10.0.3](https://github.com/nodemailer/nodemailer/compare/v10.0.2...v10.0.3) (2026-09-10)
4
22
 
5
23
 
@@ -134,6 +134,12 @@ function _handleAddress(tokens, depth) {
134
134
  textWasQuoted: []
135
135
  };
136
136
  let insideQuotes = false;
137
+ // Last character of the run each state is currently accumulating. Reading it back off
138
+ // the accumulator with slice(-1) makes the engine flatten the whole growing string on
139
+ // every token, which is quadratic over an address built from many comment-joined atoms
140
+ // (GHSA-prgh-xp8r-p3m5). A run only ever grows by the token appended below, so the
141
+ // character is carried along instead of re-read.
142
+ const lastChars = { address: '', comment: '', group: '', text: '' };
137
143
  // Filter out <addresses>, (comments) and regular text
138
144
  for (let i = 0, len = tokens.length; i < len; i++) {
139
145
  const token = tokens[i];
@@ -177,15 +183,19 @@ function _handleAddress(tokens, depth) {
177
183
  const joins = prevToken &&
178
184
  prevToken.noBreak &&
179
185
  parts.length &&
180
- (prevToken.value !== ')' || parts[parts.length - 1].slice(-1) === '@' || token.value.charAt(0) === '@');
186
+ (prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
181
187
  if (joins) {
182
188
  data[state][data[state].length - 1] += token.value;
189
+ if (token.value) {
190
+ lastChars[state] = token.value.charAt(token.value.length - 1);
191
+ }
183
192
  if (state === 'text' && insideQuotes) {
184
193
  data.textWasQuoted[data.textWasQuoted.length - 1] = true;
185
194
  }
186
195
  }
187
196
  else {
188
197
  data[state].push(token.value);
198
+ lastChars[state] = token.value.charAt(token.value.length - 1);
189
199
  if (state === 'text') {
190
200
  data.textWasQuoted.push(insideQuotes);
191
201
  }
@@ -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
  }
@@ -195,9 +195,13 @@ class Cookies {
195
195
  ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
196
196
  return false;
197
197
  }
198
- // check if path matches
199
- const path = this.getPath(urlparts.pathname);
200
- 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) {
201
205
  return false;
202
206
  }
203
207
  // check secure argument
@@ -254,22 +258,21 @@ class Cookies {
254
258
  return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
255
259
  }
256
260
  /**
257
- * 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
258
264
  *
259
265
  * @param pathname
260
- * @returns Normalized path
266
+ * @returns Default path
261
267
  */
262
268
  getPath(pathname) {
263
269
  const pathParts = (pathname || '/').split('/');
264
270
  pathParts.pop(); // remove filename part
265
- let path = pathParts.join('/').trim();
266
- // 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
267
274
  if (path.charAt(0) !== '/') {
268
- path = '/' + path;
269
- }
270
- // ensure path suffix /
271
- if (path.substr(-1) !== '/') {
272
- path += '/';
275
+ return '/';
273
276
  }
274
277
  return path;
275
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.3";
2
+ export declare const version = "10.0.5";
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.3';
6
+ exports.version = '10.0.5';
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
  ],
@@ -131,6 +131,12 @@ function _handleAddress(tokens, depth) {
131
131
  textWasQuoted: []
132
132
  };
133
133
  let insideQuotes = false;
134
+ // Last character of the run each state is currently accumulating. Reading it back off
135
+ // the accumulator with slice(-1) makes the engine flatten the whole growing string on
136
+ // every token, which is quadratic over an address built from many comment-joined atoms
137
+ // (GHSA-prgh-xp8r-p3m5). A run only ever grows by the token appended below, so the
138
+ // character is carried along instead of re-read.
139
+ const lastChars = { address: '', comment: '', group: '', text: '' };
134
140
  // Filter out <addresses>, (comments) and regular text
135
141
  for (let i = 0, len = tokens.length; i < len; i++) {
136
142
  const token = tokens[i];
@@ -174,15 +180,19 @@ function _handleAddress(tokens, depth) {
174
180
  const joins = prevToken &&
175
181
  prevToken.noBreak &&
176
182
  parts.length &&
177
- (prevToken.value !== ')' || parts[parts.length - 1].slice(-1) === '@' || token.value.charAt(0) === '@');
183
+ (prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
178
184
  if (joins) {
179
185
  data[state][data[state].length - 1] += token.value;
186
+ if (token.value) {
187
+ lastChars[state] = token.value.charAt(token.value.length - 1);
188
+ }
180
189
  if (state === 'text' && insideQuotes) {
181
190
  data.textWasQuoted[data.textWasQuoted.length - 1] = true;
182
191
  }
183
192
  }
184
193
  else {
185
194
  data[state].push(token.value);
195
+ lastChars[state] = token.value.charAt(token.value.length - 1);
186
196
  if (state === 'text') {
187
197
  data.textWasQuoted.push(insideQuotes);
188
198
  }
@@ -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
  }
@@ -157,9 +157,13 @@ export default class Cookies {
157
157
  ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
158
158
  return false;
159
159
  }
160
- // check if path matches
161
- const path = this.getPath(urlparts.pathname);
162
- 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) {
163
167
  return false;
164
168
  }
165
169
  // check secure argument
@@ -216,22 +220,21 @@ export default class Cookies {
216
220
  return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
217
221
  }
218
222
  /**
219
- * 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
220
226
  *
221
227
  * @param pathname
222
- * @returns Normalized path
228
+ * @returns Default path
223
229
  */
224
230
  getPath(pathname) {
225
231
  const pathParts = (pathname || '/').split('/');
226
232
  pathParts.pop(); // remove filename part
227
- let path = pathParts.join('/').trim();
228
- // 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
229
236
  if (path.charAt(0) !== '/') {
230
- path = '/' + path;
231
- }
232
- // ensure path suffix /
233
- if (path.substr(-1) !== '/') {
234
- path += '/';
237
+ return '/';
235
238
  }
236
239
  return path;
237
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.3";
2
+ export declare const version = "10.0.5";
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.3';
3
+ export const version = '10.0.5';
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.3",
3
+ "version": "10.0.5",
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",