nodemailer 8.0.10 → 9.0.0
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 +25 -0
- package/CLAUDE.md +1 -0
- package/SECURITY.md +9 -3
- package/lib/fetch/cookies.js +1 -1
- package/lib/fetch/index.js +26 -3
- package/lib/mail-composer/index.js +67 -33
- package/lib/mailer/index.js +2 -2
- package/lib/mailer/mail-message.js +5 -1
- package/lib/mime-funcs/mime-types.js +1 -1
- package/lib/mime-node/index.js +1 -1
- package/lib/nodemailer.js +18 -9
- package/lib/sendmail-transport/index.js +14 -2
- package/lib/ses-transport/index.js +19 -7
- package/lib/shared/index.js +10 -9
- package/lib/shared/url.js +151 -0
- package/lib/smtp-connection/http-proxy-client.js +15 -6
- package/lib/smtp-connection/index.js +4 -4
- package/lib/smtp-transport/index.js +7 -1
- package/lib/stream-transport/index.js +9 -0
- package/lib/xoauth2/index.js +5 -6
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [9.0.0](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0) (2026-06-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* HTTPS requests made while fetching remote content (attachment href/path URLs, OAuth2 token endpoints, HTTP/HTTPS proxy CONNECT) now validate the server's TLS certificate by default. Requests to hosts with self-signed, expired, or hostname-mismatched certificates that previously succeeded will now fail. Opt back out per request with tls.rejectUnauthorized=false (transport options, or a per-attachment `tls` option).
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* replace deprecated url.parse with a WHATWG URL wrapper ([0c080fb](https://github.com/nodemailer/nodemailer/commit/0c080fbf3278926f013a5c2ad06f5f6f0e18f5ed))
|
|
13
|
+
* validate TLS certificates by default when fetching remote content ([6a947ac](https://github.com/nodemailer/nodemailer/commit/6a947ac7114a16da1e6a50d9a6f4e17026ce145d))
|
|
14
|
+
|
|
15
|
+
## [8.0.11](https://github.com/nodemailer/nodemailer/compare/v8.0.10...v8.0.11) (2026-06-10)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* apply the transport-level newline option in stream and sendmail transports ([cb4f904](https://github.com/nodemailer/nodemailer/commit/cb4f904a53d2c2feeaf327203c92378d46304398))
|
|
21
|
+
* include icalEvent path/href content in the application/ics attachment ([b801c48](https://github.com/nodemailer/nodemailer/commit/b801c48fab8e9b71bc7e0ea1fb32ce6b34675b15))
|
|
22
|
+
* parse Ethereal response props without polynomial regex backtracking ([067aebe](https://github.com/nodemailer/nodemailer/commit/067aebec83b8cbe7682905e89b30ab19d260b503))
|
|
23
|
+
* resolve oauth2_provision_cb at send time for non-pooled SMTP transports ([203c8ec](https://github.com/nodemailer/nodemailer/commit/203c8ecf97594ac2e69919b0f3ba966c0f86750e))
|
|
24
|
+
* return the promise from every resolveContent branch ([07ffe8c](https://github.com/nodemailer/nodemailer/commit/07ffe8cfd97f0486b8c7b541f398922ddab47882))
|
|
25
|
+
* strip the url scheme from List-ID header values ([77e5885](https://github.com/nodemailer/nodemailer/commit/77e5885cfa0c6723ea7749c1ee74b1c11aeb78bd))
|
|
26
|
+
* tag AWS SES transport errors with the ESES code ([efa647a](https://github.com/nodemailer/nodemailer/commit/efa647a125dd698413a7cf6813b8e36881a06f91))
|
|
27
|
+
|
|
3
28
|
## [8.0.10](https://github.com/nodemailer/nodemailer/compare/v8.0.9...v8.0.10) (2026-05-29)
|
|
4
29
|
|
|
5
30
|
|
package/CLAUDE.md
CHANGED
|
@@ -50,6 +50,7 @@ Conventional Commit prefixes used in this repo: `fix:`, `feat:`, `chore:`, `docs
|
|
|
50
50
|
## Security
|
|
51
51
|
|
|
52
52
|
This is a widely-deployed library — security-sensitive changes get extra scrutiny:
|
|
53
|
+
|
|
53
54
|
- SMTP command injection: any user-controllable value that flows into a written SMTP command (envelope addresses, sizes, the `name`/EHLO option, headers) must be CRLF-stripped or rejected at the boundary. Sanitize at the assignment, not at every call site.
|
|
54
55
|
- Server reply parsing in `lib/smtp-connection/index.js` uses a `'binary'` byte-container intermediate to reassemble multi-byte UTF-8 across socket chunks; the actual decode happens at line boundaries via `decodeServerResponse`. Don't change the chunk-buffering encoding without understanding why.
|
|
55
56
|
- Reference the GHSA ID in commit messages for advisories.
|
package/SECURITY.md
CHANGED
|
@@ -26,8 +26,7 @@ Report privately through one of the following channels:
|
|
|
26
26
|
|
|
27
27
|
1. **GitHub Security Advisories (preferred).** Open a private report at
|
|
28
28
|
<https://github.com/nodemailer/nodemailer/security/advisories/new>. This keeps
|
|
29
|
-
the discussion private until a fix is published and lets us
|
|
30
|
-
and credit you.
|
|
29
|
+
the discussion private until a fix is published and lets us credit you.
|
|
31
30
|
2. **Email.** Send details to **andris@reinman.eu** (the contact listed in
|
|
32
31
|
[`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible.
|
|
33
32
|
|
|
@@ -44,7 +43,14 @@ When reporting, please include as much of the following as you can:
|
|
|
44
43
|
Nodemailer is maintained by a single person, so there is no guaranteed response
|
|
45
44
|
time — sometimes reports are handled within hours, sometimes they take longer.
|
|
46
45
|
Accepted issues are fixed in a new release and coordinated through a GitHub
|
|
47
|
-
Security Advisory
|
|
46
|
+
Security Advisory, and reporters who wish to be named are credited.
|
|
47
|
+
|
|
48
|
+
## CVEs
|
|
49
|
+
|
|
50
|
+
We track and disclose vulnerabilities through GitHub Security Advisories. We do
|
|
51
|
+
not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
|
|
52
|
+
reported issue, please request one yourself — for example, through GitHub's own
|
|
53
|
+
CVE request flow on the published advisory, or another CNA.
|
|
48
54
|
|
|
49
55
|
## Scope
|
|
50
56
|
|
package/lib/fetch/cookies.js
CHANGED
package/lib/fetch/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const https = require('https');
|
|
5
|
-
const urllib = require('url');
|
|
5
|
+
const urllib = require('../shared/url');
|
|
6
6
|
const zlib = require('zlib');
|
|
7
7
|
const { PassThrough } = require('stream');
|
|
8
8
|
const Cookies = require('./cookies');
|
|
@@ -123,7 +123,10 @@ function nmfetch(url, options) {
|
|
|
123
123
|
path: parsed.path,
|
|
124
124
|
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
|
125
125
|
headers,
|
|
126
|
-
|
|
126
|
+
// Validate TLS certificates by default. Callers that genuinely need to
|
|
127
|
+
// reach a self-signed/internal host opt out explicitly with
|
|
128
|
+
// options.tls = { rejectUnauthorized: false }.
|
|
129
|
+
rejectUnauthorized: true,
|
|
127
130
|
agent: false
|
|
128
131
|
};
|
|
129
132
|
|
|
@@ -212,7 +215,27 @@ function nmfetch(url, options) {
|
|
|
212
215
|
// redirect does not include POST body
|
|
213
216
|
options.method = 'GET';
|
|
214
217
|
options.body = false;
|
|
215
|
-
|
|
218
|
+
|
|
219
|
+
const redirectUrl = urllib.resolve(url, res.headers.location);
|
|
220
|
+
const redirectParsed = urllib.parse(redirectUrl);
|
|
221
|
+
|
|
222
|
+
// Do not forward credentials when the redirect leaves the original
|
|
223
|
+
// security context: a different host, or a downgrade from https to
|
|
224
|
+
// http (which would otherwise put them on the wire in cleartext).
|
|
225
|
+
// Strip sensitive request headers so an attacker who controls the
|
|
226
|
+
// redirect target cannot harvest them.
|
|
227
|
+
const crossHost = redirectParsed.hostname !== parsed.hostname;
|
|
228
|
+
const downgrade = parsed.protocol === 'https:' && redirectParsed.protocol === 'http:';
|
|
229
|
+
if (options.headers && (crossHost || downgrade)) {
|
|
230
|
+
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
|
231
|
+
Object.keys(options.headers).forEach(key => {
|
|
232
|
+
if (sensitive.includes(key.toLowerCase())) {
|
|
233
|
+
delete options.headers[key];
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return nmfetch(redirectUrl, options);
|
|
216
239
|
}
|
|
217
240
|
|
|
218
241
|
fetchRes.statusCode = res.statusCode;
|
|
@@ -83,7 +83,7 @@ class MailComposer {
|
|
|
83
83
|
* @returns {Object} An object of arrays (`related` and `attached`)
|
|
84
84
|
*/
|
|
85
85
|
getAttachments(findRelated) {
|
|
86
|
-
let
|
|
86
|
+
let eventObject;
|
|
87
87
|
const attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
|
|
88
88
|
if (/^data:/i.test(attachment.path || attachment.href)) {
|
|
89
89
|
attachment = this._processDataUrl(attachment);
|
|
@@ -142,7 +142,8 @@ class MailComposer {
|
|
|
142
142
|
} else if (attachment.href) {
|
|
143
143
|
data.content = {
|
|
144
144
|
href: attachment.href,
|
|
145
|
-
httpHeaders: attachment.httpHeaders
|
|
145
|
+
httpHeaders: attachment.httpHeaders,
|
|
146
|
+
tls: attachment.tls
|
|
146
147
|
};
|
|
147
148
|
} else {
|
|
148
149
|
data.content = attachment.content || '';
|
|
@@ -160,18 +161,7 @@ class MailComposer {
|
|
|
160
161
|
});
|
|
161
162
|
|
|
162
163
|
if (this.mail.icalEvent) {
|
|
163
|
-
|
|
164
|
-
typeof this.mail.icalEvent === 'object' &&
|
|
165
|
-
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
|
166
|
-
) {
|
|
167
|
-
icalEvent = this.mail.icalEvent;
|
|
168
|
-
} else {
|
|
169
|
-
icalEvent = {
|
|
170
|
-
content: this.mail.icalEvent
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
eventObject = Object.assign({}, icalEvent);
|
|
164
|
+
eventObject = Object.assign({}, this._getIcalEvent());
|
|
175
165
|
|
|
176
166
|
eventObject.contentType = 'application/ics';
|
|
177
167
|
if (!eventObject.headers) {
|
|
@@ -195,6 +185,67 @@ class MailComposer {
|
|
|
195
185
|
};
|
|
196
186
|
}
|
|
197
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Returns the icalEvent value with `path`/`href`/data uri input normalized into
|
|
190
|
+
* a `content` entry, the same way as for regular attachments. The same event is
|
|
191
|
+
* included twice (as a text/calendar alternative and as an application/ics
|
|
192
|
+
* attachment), so the shared content object is marked to be resolved just once
|
|
193
|
+
* and the buffered result is reused by the second node.
|
|
194
|
+
*
|
|
195
|
+
* @returns {Object} Normalized icalEvent data
|
|
196
|
+
*/
|
|
197
|
+
_getIcalEvent() {
|
|
198
|
+
if (!this._icalEvent) {
|
|
199
|
+
let icalEvent;
|
|
200
|
+
if (
|
|
201
|
+
typeof this.mail.icalEvent === 'object' &&
|
|
202
|
+
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
|
203
|
+
) {
|
|
204
|
+
icalEvent = Object.assign({}, this.mail.icalEvent);
|
|
205
|
+
} else {
|
|
206
|
+
icalEvent = {
|
|
207
|
+
content: this.mail.icalEvent
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (/^data:/i.test(icalEvent.path || icalEvent.href)) {
|
|
212
|
+
icalEvent = this._processDataUrl(icalEvent);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (/^https?:\/\//i.test(icalEvent.path)) {
|
|
216
|
+
icalEvent.href = icalEvent.path;
|
|
217
|
+
icalEvent.path = undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!icalEvent.raw) {
|
|
221
|
+
// map file path and URL values into `content`, otherwise the content
|
|
222
|
+
// nodes would render an empty body
|
|
223
|
+
if (icalEvent.path) {
|
|
224
|
+
icalEvent.content = {
|
|
225
|
+
path: icalEvent.path
|
|
226
|
+
};
|
|
227
|
+
icalEvent.path = undefined;
|
|
228
|
+
} else if (icalEvent.href) {
|
|
229
|
+
icalEvent.content = {
|
|
230
|
+
href: icalEvent.href,
|
|
231
|
+
httpHeaders: icalEvent.httpHeaders
|
|
232
|
+
};
|
|
233
|
+
icalEvent.href = undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (icalEvent.content && typeof icalEvent.content === 'object') {
|
|
238
|
+
// we are going to have the same attachment twice, so mark this to be
|
|
239
|
+
// resolved just once
|
|
240
|
+
icalEvent.content._resolve = true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this._icalEvent = icalEvent;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return this._icalEvent;
|
|
247
|
+
}
|
|
248
|
+
|
|
198
249
|
/**
|
|
199
250
|
* List alternatives. Resulting objects can be used as input for MimeNode nodes
|
|
200
251
|
*
|
|
@@ -202,7 +253,7 @@ class MailComposer {
|
|
|
202
253
|
*/
|
|
203
254
|
getAlternatives() {
|
|
204
255
|
const alternatives = [];
|
|
205
|
-
let text, html, watchHtml, amp,
|
|
256
|
+
let text, html, watchHtml, amp, eventObject;
|
|
206
257
|
|
|
207
258
|
if (this.mail.text) {
|
|
208
259
|
if (
|
|
@@ -248,24 +299,7 @@ class MailComposer {
|
|
|
248
299
|
|
|
249
300
|
// NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
|
|
250
301
|
if (this.mail.icalEvent) {
|
|
251
|
-
|
|
252
|
-
typeof this.mail.icalEvent === 'object' &&
|
|
253
|
-
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
|
254
|
-
) {
|
|
255
|
-
icalEvent = this.mail.icalEvent;
|
|
256
|
-
} else {
|
|
257
|
-
icalEvent = {
|
|
258
|
-
content: this.mail.icalEvent
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
eventObject = Object.assign({}, icalEvent);
|
|
263
|
-
|
|
264
|
-
if (eventObject.content && typeof eventObject.content === 'object') {
|
|
265
|
-
// we are going to have the same attachment twice, so mark this to be
|
|
266
|
-
// resolved just once
|
|
267
|
-
eventObject.content._resolve = true;
|
|
268
|
-
}
|
|
302
|
+
eventObject = Object.assign({}, this._getIcalEvent());
|
|
269
303
|
|
|
270
304
|
eventObject.filename = false;
|
|
271
305
|
eventObject.contentType =
|
package/lib/mailer/index.js
CHANGED
|
@@ -8,7 +8,7 @@ const DKIM = require('../dkim');
|
|
|
8
8
|
const httpProxyClient = require('../smtp-connection/http-proxy-client');
|
|
9
9
|
const errors = require('../errors');
|
|
10
10
|
const util = require('util');
|
|
11
|
-
const urllib = require('url');
|
|
11
|
+
const urllib = require('../shared/url');
|
|
12
12
|
const packageData = require('../../package.json');
|
|
13
13
|
const MailMessage = require('./mail-message');
|
|
14
14
|
const net = require('net');
|
|
@@ -324,7 +324,7 @@ class Mail extends EventEmitter {
|
|
|
324
324
|
// Connect using a HTTP CONNECT method
|
|
325
325
|
case 'http':
|
|
326
326
|
case 'https':
|
|
327
|
-
httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
|
|
327
|
+
httpProxyClient(proxy.href, options.port, options.host, this.options.tls || {}, (err, socket) => {
|
|
328
328
|
if (err) {
|
|
329
329
|
return callback(err);
|
|
330
330
|
}
|
|
@@ -281,7 +281,11 @@ class MailMessage {
|
|
|
281
281
|
comment = mimeFuncs.encodeWord(comment);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
284
|
+
// List-ID expects a bare domain-like identifier, so strip the
|
|
285
|
+
// scheme prefix that _formatListUrl adds or passes through
|
|
286
|
+
return (
|
|
287
|
+
(value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<')
|
|
288
|
+
);
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
// List-*: <http://domain> (comment)
|
|
@@ -2087,7 +2087,7 @@ module.exports = {
|
|
|
2087
2087
|
if (!mimeType) {
|
|
2088
2088
|
return defaultExtension;
|
|
2089
2089
|
}
|
|
2090
|
-
const parts =
|
|
2090
|
+
const parts = mimeType.toLowerCase().trim().split('/');
|
|
2091
2091
|
const rootType = parts.shift().trim();
|
|
2092
2092
|
const subType = parts.join('/').trim();
|
|
2093
2093
|
|
package/lib/mime-node/index.js
CHANGED
|
@@ -1006,7 +1006,7 @@ class MimeNode {
|
|
|
1006
1006
|
return contentStream;
|
|
1007
1007
|
}
|
|
1008
1008
|
// fetch URL
|
|
1009
|
-
return nmfetch(content.href, { headers: content.httpHeaders });
|
|
1009
|
+
return nmfetch(content.href, { headers: content.httpHeaders, tls: content.tls });
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
1012
1012
|
// pass string or buffer content as a stream
|
package/lib/nodemailer.js
CHANGED
|
@@ -102,10 +102,10 @@ module.exports.createTestAccount = function (apiUrl, callback) {
|
|
|
102
102
|
body: Buffer.from(JSON.stringify(requestBody))
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
// Credential-bearing request
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
105
|
+
// Credential-bearing request to the Ethereal API. lib/fetch already
|
|
106
|
+
// validates certs by default; pin rejectUnauthorized:true here so this
|
|
107
|
+
// call stays strict regardless of any future default change and is never
|
|
108
|
+
// relaxed for a real-cert endpoint.
|
|
109
109
|
if (/^https:/i.test(apiUrl)) {
|
|
110
110
|
fetchOptions.tls = { rejectUnauthorized: true };
|
|
111
111
|
}
|
|
@@ -147,11 +147,20 @@ module.exports.getTestMessageUrl = function (info) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
const infoProps = new Map();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
|
|
151
|
+
// Extract the trailing "[...]" part of the response (no "]" allowed inside)
|
|
152
|
+
// with linear string scanning; the equivalent regex /\[([^\]]+)\]$/ was
|
|
153
|
+
// flagged for polynomial backtracking on adversarial server responses
|
|
154
|
+
const response = info.response.toString();
|
|
155
|
+
if (response.length > 2 && response.charAt(response.length - 1) === ']') {
|
|
156
|
+
const open = response.indexOf('[', response.lastIndexOf(']', response.length - 2) + 1);
|
|
157
|
+
if (open >= 0 && open < response.length - 2) {
|
|
158
|
+
const props = response.substring(open + 1, response.length - 1);
|
|
159
|
+
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
|
|
160
|
+
infoProps.set(key, value);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
155
164
|
|
|
156
165
|
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
|
|
157
166
|
return (testAccount.web || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
|
|
@@ -4,6 +4,8 @@ const { spawn } = require('child_process');
|
|
|
4
4
|
const packageData = require('../../package.json');
|
|
5
5
|
const shared = require('../shared');
|
|
6
6
|
const errors = require('../errors');
|
|
7
|
+
const LeWindows = require('../mime-node/le-windows');
|
|
8
|
+
const LeUnix = require('../mime-node/le-unix');
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Generates a Transport object for Sendmail
|
|
@@ -46,6 +48,8 @@ class SendmailTransport {
|
|
|
46
48
|
this.args = options.args;
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -178,7 +182,15 @@ class SendmailTransport {
|
|
|
178
182
|
);
|
|
179
183
|
|
|
180
184
|
const sourceStream = mail.message.createReadStream();
|
|
181
|
-
|
|
185
|
+
let stream = sourceStream;
|
|
186
|
+
if (this.options.newline) {
|
|
187
|
+
// apply the transport-level line ending transform; the message-level
|
|
188
|
+
// `newline` option is handled by MimeNode in createReadStream()
|
|
189
|
+
stream = sourceStream.pipe(this.winbreak ? new LeWindows() : new LeUnix());
|
|
190
|
+
sourceStream.once('error', err => stream.emit('error', err));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
stream.once('error', err => {
|
|
182
194
|
this.logger.error(
|
|
183
195
|
{
|
|
184
196
|
err,
|
|
@@ -193,7 +205,7 @@ class SendmailTransport {
|
|
|
193
205
|
callback(err);
|
|
194
206
|
});
|
|
195
207
|
|
|
196
|
-
|
|
208
|
+
stream.pipe(sendmail.stdin);
|
|
197
209
|
} else {
|
|
198
210
|
const err = new Error('sendmail was not found');
|
|
199
211
|
err.code = errors.ESENDMAIL;
|
|
@@ -3,9 +3,22 @@
|
|
|
3
3
|
const EventEmitter = require('events');
|
|
4
4
|
const packageData = require('../../package.json');
|
|
5
5
|
const shared = require('../shared');
|
|
6
|
+
const errors = require('../errors');
|
|
6
7
|
const LeWindows = require('../mime-node/le-windows');
|
|
7
8
|
const MimeNode = require('../mime-node');
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Tags AWS SDK rejections that carry no `code` property (SDK v3 errors only
|
|
12
|
+
* have a `name`) with the generic SES transport error code, keeping the
|
|
13
|
+
* original error object intact
|
|
14
|
+
*/
|
|
15
|
+
function tagSesError(err) {
|
|
16
|
+
if (err && typeof err === 'object' && !err.code) {
|
|
17
|
+
err.code = errors.ESES;
|
|
18
|
+
}
|
|
19
|
+
return err;
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
/**
|
|
10
23
|
* Generates a Transport object for AWS SES
|
|
11
24
|
*
|
|
@@ -157,6 +170,7 @@ class SESTransport extends EventEmitter {
|
|
|
157
170
|
});
|
|
158
171
|
})
|
|
159
172
|
.catch(err => {
|
|
173
|
+
tagSesError(err);
|
|
160
174
|
this.logger.error(
|
|
161
175
|
{
|
|
162
176
|
err,
|
|
@@ -188,7 +202,7 @@ class SESTransport extends EventEmitter {
|
|
|
188
202
|
|
|
189
203
|
const cb = err => {
|
|
190
204
|
if (err && !['InvalidParameterValue', 'MessageRejected'].includes(err.code || err.Code || err.name)) {
|
|
191
|
-
return callback(err);
|
|
205
|
+
return callback(tagSesError(err));
|
|
192
206
|
}
|
|
193
207
|
return callback(null, true);
|
|
194
208
|
};
|
|
@@ -205,15 +219,13 @@ class SESTransport extends EventEmitter {
|
|
|
205
219
|
}
|
|
206
220
|
};
|
|
207
221
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
222
|
+
// the region value is not used for anything when verifying, but the lookup
|
|
223
|
+
// exercises the client configuration the same way as send() does
|
|
224
|
+
this.getRegion(() => {
|
|
213
225
|
const command = new this.ses.SendEmailCommand(sesMessage);
|
|
214
226
|
const sendPromise = this.ses.sesClient.send(command);
|
|
215
227
|
|
|
216
|
-
sendPromise.then(
|
|
228
|
+
sendPromise.then(() => cb(null)).catch(err => cb(err));
|
|
217
229
|
});
|
|
218
230
|
|
|
219
231
|
return promise;
|
package/lib/shared/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const urllib = require('url');
|
|
5
|
+
const urllib = require('./url');
|
|
6
6
|
const util = require('util');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const nmfetch = require('../fetch');
|
|
@@ -530,6 +530,12 @@ module.exports.resolveContent = (data, key, options, callback) => {
|
|
|
530
530
|
});
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
resolveContentValue(data, key, options, callback);
|
|
534
|
+
|
|
535
|
+
return promise;
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
function resolveContentValue(data, key, options, callback) {
|
|
533
539
|
let content = (data && data[key] && data[key].content) || data[key];
|
|
534
540
|
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
|
535
541
|
.toString()
|
|
@@ -563,14 +569,11 @@ module.exports.resolveContent = (data, key, options, callback) => {
|
|
|
563
569
|
callback(err);
|
|
564
570
|
});
|
|
565
571
|
}
|
|
566
|
-
return resolveStream(nmfetch(content.path || content.href), callback);
|
|
572
|
+
return resolveStream(nmfetch(content.path || content.href, { headers: content.httpHeaders, tls: content.tls }), callback);
|
|
567
573
|
} else if (/^data:/i.test(content.path || content.href)) {
|
|
568
574
|
const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
|
|
569
575
|
|
|
570
|
-
|
|
571
|
-
return callback(null, Buffer.from(0));
|
|
572
|
-
}
|
|
573
|
-
return callback(null, parsedDataUri.data);
|
|
576
|
+
return callback(null, parsedDataUri && parsedDataUri.data ? parsedDataUri.data : Buffer.alloc(0));
|
|
574
577
|
} else if (content.path) {
|
|
575
578
|
if (options.disableFileAccess) {
|
|
576
579
|
return setImmediate(() => {
|
|
@@ -589,9 +592,7 @@ module.exports.resolveContent = (data, key, options, callback) => {
|
|
|
589
592
|
|
|
590
593
|
// default action, return as is
|
|
591
594
|
setImmediate(() => callback(null, content));
|
|
592
|
-
|
|
593
|
-
return promise;
|
|
594
|
-
};
|
|
595
|
+
}
|
|
595
596
|
|
|
596
597
|
/**
|
|
597
598
|
* Copies properties from source objects to target objects
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// URL parsing wrapper. Prefers the WHATWG `URL` (a global on Node 10+, and
|
|
4
|
+
// available as `require('url').URL` since Node 6.13+) and only falls back to the
|
|
5
|
+
// legacy, deprecation-warning-emitting `url.parse()` / `url.resolve()` on ancient
|
|
6
|
+
// Node versions that predate the WHATWG implementation.
|
|
7
|
+
//
|
|
8
|
+
// The WHATWG `URL` exposes a different shape than the legacy parser, so results
|
|
9
|
+
// are normalized back into the legacy field names the rest of the codebase reads
|
|
10
|
+
// (`protocol`, `hostname`, `port`, `pathname`, `path`, `search`, `auth`, `query`,
|
|
11
|
+
// `href`). This keeps every existing call site unchanged.
|
|
12
|
+
//
|
|
13
|
+
// Known, accepted divergences from the legacy parser:
|
|
14
|
+
// - non-special schemes (smtp:/smtps:/direct:) are not host-lowercased by
|
|
15
|
+
// WHATWG; cosmetic only, SMTP/DNS hosts are case-insensitive. (IDNA mapping
|
|
16
|
+
// and IPv6 brackets are normalized back by normalizeHostname below.)
|
|
17
|
+
// - a literal unescaped ':' inside a password is percent-encoded by WHATWG;
|
|
18
|
+
// such passwords should be percent-encoded by the caller anyway.
|
|
19
|
+
|
|
20
|
+
const urllib = require('url');
|
|
21
|
+
const punycode = require('../punycode');
|
|
22
|
+
|
|
23
|
+
// WHATWG URL constructor if available, otherwise undefined (Node < 6.13).
|
|
24
|
+
const URLImpl = (typeof URL !== 'undefined' && URL) || urllib.URL;
|
|
25
|
+
|
|
26
|
+
// Matches a "scheme:" not followed by "//" (and with something after it), used
|
|
27
|
+
// to re-insert the authority separator the legacy parser did not require.
|
|
28
|
+
const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)(.+)$/;
|
|
29
|
+
|
|
30
|
+
// decodeURIComponent that never throws. Legacy url.parse() decodes the auth
|
|
31
|
+
// component but tolerates malformed percent sequences, so mirror that.
|
|
32
|
+
function safeDecode(str) {
|
|
33
|
+
try {
|
|
34
|
+
return decodeURIComponent(str);
|
|
35
|
+
} catch (_err) {
|
|
36
|
+
return str;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Derives the legacy-shaped bare hostname from a WHATWG URL. WHATWG keeps IPv6
|
|
41
|
+
// literals bracketed ('[::1]') and, for non-special schemes (smtp:/smtps:/socks:),
|
|
42
|
+
// percent-encodes a non-ASCII host instead of IDNA-mapping it. Both forms are
|
|
43
|
+
// un-resolvable when handed to net/dns/http.request — which is what every call
|
|
44
|
+
// site does — so map them back to what legacy url.parse() returned: the bare
|
|
45
|
+
// address and the punycode form. Idempotent on plain ASCII and already-punycode
|
|
46
|
+
// hosts, so special-scheme hosts (already IDNA-mapped by WHATWG) pass through.
|
|
47
|
+
function normalizeHostname(raw) {
|
|
48
|
+
let hostname = raw || '';
|
|
49
|
+
if (!hostname) {
|
|
50
|
+
// Host-less URL (e.g. 'direct:'): legacy returned '' here, not null;
|
|
51
|
+
// consumers do `hostname.length` / `'.' + hostname`, so keep it a string.
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
|
|
55
|
+
return hostname.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
return punycode.toASCII(safeDecode(hostname));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports.parse = (input, parseQueryString) => {
|
|
61
|
+
input = input || '';
|
|
62
|
+
|
|
63
|
+
if (!URLImpl) {
|
|
64
|
+
// Node < 6.13: no WHATWG URL available, use the legacy parser.
|
|
65
|
+
return urllib.parse(input, parseQueryString);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Legacy url.parse() parses a "user:pass@host:port" authority that follows
|
|
69
|
+
// the scheme even without the "//" separator, for schemes outside its
|
|
70
|
+
// built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
|
|
71
|
+
// parser instead treats a scheme not followed by "//" as an opaque path.
|
|
72
|
+
// Re-insert the "//" so slash-less connection/proxy URLs keep resolving to
|
|
73
|
+
// an authority, as they did before. This assumes a slash-authority scheme,
|
|
74
|
+
// which every consumer here uses (http/https/smtp/smtps/socks/direct); an
|
|
75
|
+
// opaque scheme like mailto:/data:/tel: would be mis-split, but none reach
|
|
76
|
+
// this module.
|
|
77
|
+
const slashless = SLASHLESS_AUTHORITY.exec(input);
|
|
78
|
+
const normalized = slashless ? slashless[1] + '//' + slashless[2] : input;
|
|
79
|
+
|
|
80
|
+
let u;
|
|
81
|
+
try {
|
|
82
|
+
u = new URLImpl(normalized);
|
|
83
|
+
} catch (_err) {
|
|
84
|
+
// WHATWG rejects some input the legacy parser tolerated (empty/relative
|
|
85
|
+
// strings, scheme-relative '//host/path', out-of-range ports, ...). Fall
|
|
86
|
+
// back to the legacy parser so behavior — including the downstream errors
|
|
87
|
+
// callers rely on — is preserved. This is the only path that can still
|
|
88
|
+
// emit a deprecation warning; it fires for anything WHATWG cannot
|
|
89
|
+
// represent, including legitimate relative URLs, not just malformed input.
|
|
90
|
+
return urllib.parse(input, parseQueryString);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hostname = normalizeHostname(u.hostname);
|
|
94
|
+
const port = u.port || null;
|
|
95
|
+
const pathname = u.pathname || null;
|
|
96
|
+
const search = u.search || null;
|
|
97
|
+
|
|
98
|
+
// Legacy `.auth` is the decoded "user[:pass]" string; WHATWG keeps the
|
|
99
|
+
// username/password percent-encoded, so decode to stay byte-compatible with
|
|
100
|
+
// existing consumers (parseConnectionUrl, Basic/Proxy-Authorization headers).
|
|
101
|
+
let auth = null;
|
|
102
|
+
if (u.username || u.password) {
|
|
103
|
+
// Gate on password too: legacy url.parse('smtps://:pass@host').auth was
|
|
104
|
+
// ':pass'. Dropping it would silently connect unauthenticated.
|
|
105
|
+
auth = safeDecode(u.username) + (u.password ? ':' + safeDecode(u.password) : '');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let query;
|
|
109
|
+
if (parseQueryString) {
|
|
110
|
+
// Mirror querystring.parse(): null-prototype object, repeated keys → array.
|
|
111
|
+
query = Object.create(null);
|
|
112
|
+
u.searchParams.forEach((value, key) => {
|
|
113
|
+
if (Object.prototype.hasOwnProperty.call(query, key)) {
|
|
114
|
+
if (Array.isArray(query[key])) {
|
|
115
|
+
query[key].push(value);
|
|
116
|
+
} else {
|
|
117
|
+
query[key] = [query[key], value];
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
query[key] = value;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
query = search ? search.slice(1) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
protocol: u.protocol || null,
|
|
129
|
+
host: u.host || null,
|
|
130
|
+
hostname,
|
|
131
|
+
port,
|
|
132
|
+
pathname,
|
|
133
|
+
search,
|
|
134
|
+
path: (pathname || '') + (search || '') || null,
|
|
135
|
+
href: u.href,
|
|
136
|
+
auth,
|
|
137
|
+
query
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
module.exports.resolve = (from, to) => {
|
|
142
|
+
if (!URLImpl) {
|
|
143
|
+
return urllib.resolve(from, to);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return new URLImpl(to, from).href;
|
|
147
|
+
} catch (_err) {
|
|
148
|
+
// Malformed target — fall back to the legacy resolver.
|
|
149
|
+
return urllib.resolve(from, to);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const net = require('net');
|
|
8
8
|
const tls = require('tls');
|
|
9
|
-
const urllib = require('url');
|
|
9
|
+
const urllib = require('../shared/url');
|
|
10
10
|
const errors = require('../errors');
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -19,20 +19,29 @@ const errors = require('../errors');
|
|
|
19
19
|
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
|
|
20
20
|
* @param {Number} destinationPort Port to open in destination host
|
|
21
21
|
* @param {String} destinationHost Destination hostname
|
|
22
|
+
* @param {Object} [tlsOptions] Optional TLS options for an HTTPS proxy (e.g. { rejectUnauthorized: false })
|
|
22
23
|
* @param {Function} callback Callback to run with the rocket object once connection is established
|
|
23
24
|
*/
|
|
24
|
-
function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
|
25
|
+
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
|
26
|
+
if (typeof tlsOptions === 'function') {
|
|
27
|
+
callback = tlsOptions;
|
|
28
|
+
tlsOptions = {};
|
|
29
|
+
}
|
|
30
|
+
tlsOptions = tlsOptions || {};
|
|
31
|
+
|
|
25
32
|
const proxy = urllib.parse(proxyUrl);
|
|
26
33
|
|
|
27
|
-
const
|
|
34
|
+
const connectOptions = {
|
|
28
35
|
host: proxy.hostname,
|
|
29
36
|
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
let connect;
|
|
33
40
|
if (proxy.protocol === 'https:') {
|
|
34
|
-
//
|
|
35
|
-
|
|
41
|
+
// Validate the proxy's TLS certificate by default. A caller that uses a
|
|
42
|
+
// self-signed proxy (e.g. integration tests) opts out explicitly with
|
|
43
|
+
// tls.rejectUnauthorized === false.
|
|
44
|
+
connectOptions.rejectUnauthorized = tlsOptions.rejectUnauthorized !== false;
|
|
36
45
|
connect = tls.connect.bind(tls);
|
|
37
46
|
} else {
|
|
38
47
|
connect = net.connect.bind(net);
|
|
@@ -62,7 +71,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
|
|
62
71
|
tempSocketErr(err);
|
|
63
72
|
};
|
|
64
73
|
|
|
65
|
-
socket = connect(
|
|
74
|
+
socket = connect(connectOptions, () => {
|
|
66
75
|
if (finished) {
|
|
67
76
|
return;
|
|
68
77
|
}
|
|
@@ -51,9 +51,9 @@ function decodeServerResponse(str) {
|
|
|
51
51
|
* * **requireTLS** - forces the client to use STARTTLS
|
|
52
52
|
* * **name** - the name of the client server
|
|
53
53
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
|
54
|
-
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to
|
|
55
|
-
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish
|
|
56
|
-
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to
|
|
54
|
+
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
|
55
|
+
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
|
56
|
+
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
|
57
57
|
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
|
58
58
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
|
59
59
|
* * **logger** - bunyan compatible logger interface
|
|
@@ -838,7 +838,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
838
838
|
return;
|
|
839
839
|
}
|
|
840
840
|
|
|
841
|
-
let data =
|
|
841
|
+
let data = chunk.toString('binary');
|
|
842
842
|
let lines = (this._remainder + data).split(/\r?\n/);
|
|
843
843
|
let lastline;
|
|
844
844
|
|
|
@@ -71,13 +71,19 @@ class SMTPTransport extends EventEmitter {
|
|
|
71
71
|
|
|
72
72
|
getAuth(authOpts) {
|
|
73
73
|
if (!authOpts) {
|
|
74
|
+
if (this.auth && this.auth.oauth2 && this.mailer) {
|
|
75
|
+
// Transport-level auth is resolved in the constructor, before the Mail wrapper
|
|
76
|
+
// assigns `this.mailer`, so a provision callback registered with
|
|
77
|
+
// `transporter.set('oauth2_provision_cb', ...)` has to be re-checked here
|
|
78
|
+
this.auth.oauth2.provisionCallback = this.mailer.get('oauth2_provision_cb') || this.auth.oauth2.provisionCallback;
|
|
79
|
+
}
|
|
74
80
|
return this.auth;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
const authData = Object.assign(
|
|
78
84
|
{},
|
|
79
85
|
this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {},
|
|
80
|
-
|
|
86
|
+
typeof authOpts === 'object' ? authOpts : {}
|
|
81
87
|
);
|
|
82
88
|
|
|
83
89
|
if (Object.keys(authData).length === 0) {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const packageData = require('../../package.json');
|
|
4
4
|
const shared = require('../shared');
|
|
5
|
+
const LeWindows = require('../mime-node/le-windows');
|
|
6
|
+
const LeUnix = require('../mime-node/le-unix');
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Generates a Transport object for streaming
|
|
@@ -63,6 +65,13 @@ class StreamTransport {
|
|
|
63
65
|
|
|
64
66
|
try {
|
|
65
67
|
stream = mail.message.createReadStream();
|
|
68
|
+
if (this.options.newline) {
|
|
69
|
+
// apply the transport-level line ending transform; the message-level
|
|
70
|
+
// `newline` option is handled by MimeNode in createReadStream()
|
|
71
|
+
const sourceStream = stream;
|
|
72
|
+
stream = sourceStream.pipe(this.winbreak ? new LeWindows() : new LeUnix());
|
|
73
|
+
sourceStream.once('error', err => stream.emit('error', err));
|
|
74
|
+
}
|
|
66
75
|
} catch (E) {
|
|
67
76
|
this.logger.error(
|
|
68
77
|
{
|
package/lib/xoauth2/index.js
CHANGED
|
@@ -378,12 +378,11 @@ class XOAuth2 extends Stream {
|
|
|
378
378
|
allowErrorResponse: true
|
|
379
379
|
};
|
|
380
380
|
|
|
381
|
-
// OAuth2 token endpoints are credential-bearing. lib/fetch
|
|
382
|
-
// rejectUnauthorized:
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// still override.
|
|
381
|
+
// OAuth2 token endpoints are credential-bearing. lib/fetch already
|
|
382
|
+
// validates certs by default; pin rejectUnauthorized:true here so the
|
|
383
|
+
// token fetch stays strict, while still layering params.tls (the
|
|
384
|
+
// user's options.tls) on top so callers with a self-hosted provider on
|
|
385
|
+
// a private CA can override.
|
|
387
386
|
if (/^https:/i.test(url)) {
|
|
388
387
|
fetchOptions.tls = Object.assign({ rejectUnauthorized: true }, params.tls || {});
|
|
389
388
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
|
5
5
|
"main": "lib/nodemailer.js",
|
|
6
6
|
"scripts": {
|
|
@@ -27,19 +27,19 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://nodemailer.com/",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@aws-sdk/client-sesv2": "3.
|
|
30
|
+
"@aws-sdk/client-sesv2": "3.1065.0",
|
|
31
31
|
"bunyan": "1.8.15",
|
|
32
32
|
"c8": "11.0.0",
|
|
33
|
-
"eslint": "10.
|
|
33
|
+
"eslint": "10.4.1",
|
|
34
34
|
"eslint-config-prettier": "10.1.8",
|
|
35
|
-
"globals": "17.
|
|
35
|
+
"globals": "17.6.0",
|
|
36
36
|
"libbase64": "1.3.0",
|
|
37
37
|
"libmime": "5.3.8",
|
|
38
38
|
"libqp": "2.1.1",
|
|
39
|
-
"prettier": "3.8.
|
|
39
|
+
"prettier": "3.8.4",
|
|
40
40
|
"proxy": "1.0.2",
|
|
41
41
|
"proxy-test-server": "1.0.0",
|
|
42
|
-
"smtp-server": "3.18.
|
|
42
|
+
"smtp-server": "3.18.5"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=6.0.0"
|