@mavogel/awscdk-rootmail 0.1.15 → 0.1.16
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/.jsii +4 -4
- package/lib/rootmail.js +1 -1
- package/lib/ses-receive.js +1 -1
- package/node_modules/mailparser/.release-please-manifest.json +1 -1
- package/node_modules/mailparser/CHANGELOG.md +7 -0
- package/node_modules/mailparser/package.json +2 -2
- package/node_modules/nodemailer/CHANGELOG.md +27 -0
- package/node_modules/nodemailer/dist/cjs/base64/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/cjs/base64/index.js +80 -41
- package/node_modules/nodemailer/dist/cjs/dkim/index.js +43 -29
- package/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +2 -2
- package/node_modules/nodemailer/dist/cjs/errors.d.ts +17 -0
- package/node_modules/nodemailer/dist/cjs/errors.js +12 -1
- package/node_modules/nodemailer/dist/cjs/fetch/index.js +23 -3
- package/node_modules/nodemailer/dist/cjs/mailer/index.js +23 -7
- package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -0
- package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +20 -0
- package/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +1 -1
- package/node_modules/nodemailer/dist/cjs/mime-node/index.js +165 -105
- package/node_modules/nodemailer/dist/cjs/nodemailer.js +12 -9
- package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
- package/node_modules/nodemailer/dist/cjs/qp/index.d.ts +2 -0
- package/node_modules/nodemailer/dist/cjs/qp/index.js +103 -33
- package/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +54 -14
- package/node_modules/nodemailer/dist/cjs/shared/index.d.ts +32 -1
- package/node_modules/nodemailer/dist/cjs/shared/index.js +212 -115
- package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +3 -1
- package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +88 -68
- package/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +7 -1
- package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +348 -136
- package/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +7 -1
- package/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +106 -17
- package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +8 -2
- package/node_modules/nodemailer/dist/esm/base64/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/esm/base64/index.js +80 -41
- package/node_modules/nodemailer/dist/esm/dkim/index.js +43 -29
- package/node_modules/nodemailer/dist/esm/dkim/message-parser.js +2 -2
- package/node_modules/nodemailer/dist/esm/errors.d.ts +17 -0
- package/node_modules/nodemailer/dist/esm/errors.js +10 -0
- package/node_modules/nodemailer/dist/esm/fetch/index.js +23 -3
- package/node_modules/nodemailer/dist/esm/mailer/index.js +23 -7
- package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -0
- package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +20 -0
- package/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +1 -1
- package/node_modules/nodemailer/dist/esm/mime-node/index.js +166 -106
- package/node_modules/nodemailer/dist/esm/nodemailer.js +12 -9
- package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
- package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
- package/node_modules/nodemailer/dist/esm/qp/index.d.ts +2 -0
- package/node_modules/nodemailer/dist/esm/qp/index.js +103 -33
- package/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +54 -14
- package/node_modules/nodemailer/dist/esm/shared/index.d.ts +32 -1
- package/node_modules/nodemailer/dist/esm/shared/index.js +207 -115
- package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +3 -1
- package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +88 -68
- package/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +7 -1
- package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +348 -136
- package/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +3 -0
- package/node_modules/nodemailer/dist/esm/smtp-pool/index.js +7 -1
- package/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.js +106 -17
- package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +8 -2
- package/node_modules/nodemailer/package.json +1 -1
- package/package.json +3 -3
|
@@ -119,6 +119,11 @@ class Mail extends EventEmitter {
|
|
|
119
119
|
this.getSocket = false;
|
|
120
120
|
}
|
|
121
121
|
const mail = new MailMessage(this, data);
|
|
122
|
+
// a failed message is not going to be read anymore, release its content streams
|
|
123
|
+
const fail = (err) => {
|
|
124
|
+
mail.releaseStreams();
|
|
125
|
+
done(err);
|
|
126
|
+
};
|
|
122
127
|
this.logger.debug({
|
|
123
128
|
tnx: 'transport',
|
|
124
129
|
name: this.transporter.name,
|
|
@@ -132,7 +137,7 @@ class Mail extends EventEmitter {
|
|
|
132
137
|
tnx: 'plugin',
|
|
133
138
|
action: 'compile'
|
|
134
139
|
}, 'PluginCompile Error: %s', err.message);
|
|
135
|
-
return
|
|
140
|
+
return fail(err);
|
|
136
141
|
}
|
|
137
142
|
let recipientCount;
|
|
138
143
|
try {
|
|
@@ -149,7 +154,7 @@ class Mail extends EventEmitter {
|
|
|
149
154
|
tnx: 'transport',
|
|
150
155
|
action: 'send'
|
|
151
156
|
}, 'Compile Error: %s', err.message);
|
|
152
|
-
return
|
|
157
|
+
return fail(err);
|
|
153
158
|
}
|
|
154
159
|
const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
|
|
155
160
|
if (maxRecipients && recipientCount > maxRecipients) {
|
|
@@ -160,7 +165,7 @@ class Mail extends EventEmitter {
|
|
|
160
165
|
tnx: 'transport',
|
|
161
166
|
action: 'send'
|
|
162
167
|
}, 'Send Error: %s', err.message);
|
|
163
|
-
return
|
|
168
|
+
return fail(err);
|
|
164
169
|
}
|
|
165
170
|
this._processPlugins('stream', mail, err => {
|
|
166
171
|
if (err) {
|
|
@@ -169,7 +174,7 @@ class Mail extends EventEmitter {
|
|
|
169
174
|
tnx: 'plugin',
|
|
170
175
|
action: 'stream'
|
|
171
176
|
}, 'PluginStream Error: %s', err.message);
|
|
172
|
-
return
|
|
177
|
+
return fail(err);
|
|
173
178
|
}
|
|
174
179
|
if (mail.data.dkim || this.dkim) {
|
|
175
180
|
mail.message.processFunc(input => {
|
|
@@ -184,6 +189,7 @@ class Mail extends EventEmitter {
|
|
|
184
189
|
}
|
|
185
190
|
this.transporter.send(mail, (...args) => {
|
|
186
191
|
if (args[0]) {
|
|
192
|
+
mail.releaseStreams();
|
|
187
193
|
this.logger.error({
|
|
188
194
|
err: args[0],
|
|
189
195
|
tnx: 'transport',
|
|
@@ -251,6 +257,11 @@ class Mail extends EventEmitter {
|
|
|
251
257
|
// setup socket handler for the mailer object
|
|
252
258
|
this.getSocket = (options, callback) => {
|
|
253
259
|
const protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
|
|
260
|
+
// The proxy handshake is a part of connecting and draws from the same deadline as the
|
|
261
|
+
// SMTP connection that follows it: whatever the proxy takes, the connection does not get
|
|
262
|
+
const configuredTimeout = Number(this.options.connectionTimeout) || 0;
|
|
263
|
+
const connectStartedAt = Number(options.connectStartedAt) || Date.now();
|
|
264
|
+
const connectionTimeout = configuredTimeout ? Math.max(connectStartedAt + configuredTimeout - Date.now(), 1) : undefined;
|
|
254
265
|
if (this.meta.has('proxy_handler_' + protocol)) {
|
|
255
266
|
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
|
|
256
267
|
}
|
|
@@ -258,7 +269,7 @@ class Mail extends EventEmitter {
|
|
|
258
269
|
// Connect using a HTTP CONNECT method
|
|
259
270
|
case 'http':
|
|
260
271
|
case 'https':
|
|
261
|
-
httpProxyClient(proxy.href, options.port, options.host, this.options.tls || {}, (err, socket) => {
|
|
272
|
+
httpProxyClient(proxy.href, options.port, options.host, connectionTimeout ? Object.assign({}, this.options.tls, { timeout: connectionTimeout }) : this.options.tls || {}, (err, socket) => {
|
|
262
273
|
if (err) {
|
|
263
274
|
return callback(err);
|
|
264
275
|
}
|
|
@@ -292,6 +303,9 @@ class Mail extends EventEmitter {
|
|
|
292
303
|
},
|
|
293
304
|
command: 'connect'
|
|
294
305
|
};
|
|
306
|
+
if (connectionTimeout) {
|
|
307
|
+
connectionOpts.timeout = connectionTimeout;
|
|
308
|
+
}
|
|
295
309
|
if (proxy.username || proxy.password) {
|
|
296
310
|
const username = proxy.username || '';
|
|
297
311
|
const password = proxy.password || '';
|
|
@@ -321,11 +335,13 @@ class Mail extends EventEmitter {
|
|
|
321
335
|
if (net.isIP(proxy.hostname)) {
|
|
322
336
|
return connect(proxy.hostname);
|
|
323
337
|
}
|
|
324
|
-
|
|
338
|
+
// lookup goes through the hosts file and returns an IPv6 address as well,
|
|
339
|
+
// the same way a connection to the proxy host would be resolved otherwise
|
|
340
|
+
return dns.lookup(proxy.hostname, (err, address) => {
|
|
325
341
|
if (err) {
|
|
326
342
|
return callback(err);
|
|
327
343
|
}
|
|
328
|
-
connect(
|
|
344
|
+
connect(address);
|
|
329
345
|
});
|
|
330
346
|
}
|
|
331
347
|
}
|
|
@@ -90,6 +90,11 @@ export default class MailMessage<T = SentMessageInfo> {
|
|
|
90
90
|
[key: string]: any;
|
|
91
91
|
}, key: string | number, options?: ResolveContentOptions | false): Promise<any>;
|
|
92
92
|
resolveAll(callback: MailMessageDataCallback): void;
|
|
93
|
+
/**
|
|
94
|
+
* Destroys the content streams of the message once sending failed. A stream that was not
|
|
95
|
+
* read to the end would otherwise keep the file or the socket behind it open
|
|
96
|
+
*/
|
|
97
|
+
releaseStreams(): void;
|
|
93
98
|
normalize(callback: MailMessageDataCallback): void;
|
|
94
99
|
setMailerHeader(): void;
|
|
95
100
|
setPriorityHeaders(): void;
|
|
@@ -136,6 +136,26 @@ export default class MailMessage {
|
|
|
136
136
|
};
|
|
137
137
|
setImmediate(() => resolveNext());
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Destroys the content streams of the message once sending failed. A stream that was not
|
|
141
|
+
* read to the end would otherwise keep the file or the socket behind it open
|
|
142
|
+
*/
|
|
143
|
+
releaseStreams() {
|
|
144
|
+
const data = this.data;
|
|
145
|
+
const values = [data.html, data.text, data.watchHtml, data.amp, data.raw, data.icalEvent];
|
|
146
|
+
for (const key of ['attachments', 'alternatives']) {
|
|
147
|
+
if (Array.isArray(data[key])) {
|
|
148
|
+
values.push(...data[key]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const value of values) {
|
|
152
|
+
if (value && typeof value === 'object') {
|
|
153
|
+
shared.destroyStream(value);
|
|
154
|
+
shared.destroyStream(value.content);
|
|
155
|
+
shared.destroyStream(value.raw);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
139
159
|
normalize(callback) {
|
|
140
160
|
const envelope = this.message.getEnvelope();
|
|
141
161
|
const messageId = this.message.messageId();
|
|
@@ -181,7 +181,7 @@ declare class MimeNode {
|
|
|
181
181
|
childNodes: MimeNode[];
|
|
182
182
|
/** Filename for this node. Useful with attachments */
|
|
183
183
|
filename?: string | undefined;
|
|
184
|
-
/** Body content
|
|
184
|
+
/** Body content */
|
|
185
185
|
content?: MimeNodeContent | Error | undefined;
|
|
186
186
|
/** Lowercase content type, set when the headers are built */
|
|
187
187
|
contentType?: string | undefined;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import * as punycode from '../punycode/index.js';
|
|
5
|
-
import { PassThrough } from 'node:stream';
|
|
5
|
+
import { PassThrough, finished, pipeline } from 'node:stream';
|
|
6
6
|
import * as shared from '../shared/index.js';
|
|
7
7
|
import urlModule from 'node:url';
|
|
8
8
|
import * as mimeFuncs from '../mime-funcs/index.js';
|
|
@@ -33,6 +33,14 @@ const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
|
|
|
33
33
|
// unroutable garbage into mail for a domain the sender never named. None of these
|
|
34
34
|
// characters are legal in a domain, so keep them away from the mapper.
|
|
35
35
|
const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
|
|
36
|
+
// pipeline() needs a callback to not throw, the errors it sees reach the last stream anyway
|
|
37
|
+
const PIPELINE_NOOP = () => false;
|
|
38
|
+
// The error a node streaming into an output that was destroyed stops with
|
|
39
|
+
function abortedError() {
|
|
40
|
+
const err = new Error('Message stream was closed before the message was generated');
|
|
41
|
+
err.code = errors.ESTREAM;
|
|
42
|
+
return err;
|
|
43
|
+
}
|
|
36
44
|
/**
|
|
37
45
|
* Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
|
|
38
46
|
* which is with UTS-46 mapping applied before the Punycode step.
|
|
@@ -52,9 +60,14 @@ const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
|
|
|
52
60
|
*/
|
|
53
61
|
function normalizeDomain(domain, toUnicode) {
|
|
54
62
|
// domainToASCII and domainToUnicode landed in Node 7, the bundled codec covers Node 6
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
if (typeof urlModule.domainToASCII === 'function' &&
|
|
64
|
+
typeof urlModule.domainToUnicode === 'function' &&
|
|
65
|
+
!URL_PARSER_UNSAFE.test(domain)) {
|
|
66
|
+
// The U-label form is decoded from the A-label form rather than from the input:
|
|
67
|
+
// Deno's domainToASCII returns an empty string for a label it cannot decode, while
|
|
68
|
+
// its domainToUnicode returns the label with U+FFFD in place of the bad part
|
|
69
|
+
const ascii = urlModule.domainToASCII(domain);
|
|
70
|
+
const mapped = ascii && toUnicode ? urlModule.domainToUnicode(ascii) : ascii;
|
|
58
71
|
if (mapped) {
|
|
59
72
|
return mapped;
|
|
60
73
|
}
|
|
@@ -406,11 +419,7 @@ class MimeNode {
|
|
|
406
419
|
if (typeof this.content.pipe === 'function') {
|
|
407
420
|
// pre-stream handler. might be triggered if a stream is set as content
|
|
408
421
|
// and 'error' fires before anything is done with this stream
|
|
409
|
-
this.
|
|
410
|
-
this.content.removeListener('error', this._contentErrorHandler);
|
|
411
|
-
this.content = err;
|
|
412
|
-
};
|
|
413
|
-
this.content.once('error', this._contentErrorHandler);
|
|
422
|
+
shared.recordStreamErrors(this.content);
|
|
414
423
|
}
|
|
415
424
|
else if (typeof this.content === 'string') {
|
|
416
425
|
this._isPlainText = mimeFuncs.isPlainText(this.content);
|
|
@@ -622,40 +631,45 @@ class MimeNode {
|
|
|
622
631
|
createReadStream(options) {
|
|
623
632
|
options = options || {};
|
|
624
633
|
const stream = new PassThrough(options);
|
|
625
|
-
let outputStream = stream;
|
|
626
|
-
let transform;
|
|
627
634
|
this.stream(stream, options, err => {
|
|
628
635
|
if (err) {
|
|
629
|
-
|
|
636
|
+
stream.destroy(err);
|
|
630
637
|
return;
|
|
631
638
|
}
|
|
632
639
|
stream.end();
|
|
633
640
|
});
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
+
// the content streams of the nodes the message did not get to are not going to be read
|
|
642
|
+
stream.once('close', () => {
|
|
643
|
+
if (!stream.writableFinished) {
|
|
644
|
+
this._destroyContentStreams();
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
// The stages are joined with pipeline, which destroys all of them when any one fails
|
|
648
|
+
// or is destroyed. An error anywhere reaches the returned stream exactly once, and a
|
|
649
|
+
// consumer that destroys the returned stream stops the tree from reading its sources
|
|
650
|
+
const stages = [stream];
|
|
651
|
+
for (const transform of this._transforms) {
|
|
652
|
+
stages.push(typeof transform === 'function' ? transform() : transform);
|
|
641
653
|
}
|
|
642
654
|
// ensure terminating newline after possible user transforms
|
|
643
|
-
|
|
644
|
-
outputStream
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
655
|
+
stages.push(new LastNewline());
|
|
656
|
+
let outputStream = pipeline(stages, PIPELINE_NOOP);
|
|
657
|
+
// dkim and stuff. A process function reads its input itself and reports the errors of
|
|
658
|
+
// that input on its output, so only a consumer abort has to be carried back upstream
|
|
659
|
+
for (const processFunc of this._processFuncs) {
|
|
660
|
+
const input = outputStream;
|
|
661
|
+
outputStream = processFunc(input);
|
|
662
|
+
if (outputStream !== input) {
|
|
663
|
+
finished(outputStream, err => {
|
|
664
|
+
if (err) {
|
|
665
|
+
input.destroy();
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
652
669
|
}
|
|
653
670
|
if (this.newline) {
|
|
654
671
|
const winbreak = ['win', 'windows', 'dos', '\r\n'].includes(this.newline.toString().toLowerCase());
|
|
655
|
-
|
|
656
|
-
const stream = outputStream.pipe(newlineTransform);
|
|
657
|
-
outputStream.on('error', err => stream.emit('error', err));
|
|
658
|
-
return stream;
|
|
672
|
+
outputStream = pipeline(outputStream, winbreak ? new LeWindows() : new LeUnix(), PIPELINE_NOOP);
|
|
659
673
|
}
|
|
660
674
|
return outputStream;
|
|
661
675
|
}
|
|
@@ -681,8 +695,10 @@ class MimeNode {
|
|
|
681
695
|
}
|
|
682
696
|
stream(outputStream, options, done) {
|
|
683
697
|
const transferEncoding = this.getTransferEncoding();
|
|
684
|
-
|
|
685
|
-
|
|
698
|
+
// the streams this node is reading from. A consumer that goes away mid-message destroys
|
|
699
|
+
// the output, and these are released with it instead of staying paused with a file or a
|
|
700
|
+
// socket open behind them
|
|
701
|
+
let activeStreams = [];
|
|
686
702
|
// protect actual callback against multiple triggering
|
|
687
703
|
let returned = false;
|
|
688
704
|
const callback = (err) => {
|
|
@@ -690,13 +706,39 @@ class MimeNode {
|
|
|
690
706
|
return;
|
|
691
707
|
}
|
|
692
708
|
returned = true;
|
|
709
|
+
outputStream.removeListener('close', onOutputClose);
|
|
693
710
|
done(err);
|
|
694
711
|
};
|
|
712
|
+
function onOutputClose() {
|
|
713
|
+
for (const stream of activeStreams) {
|
|
714
|
+
stream.destroy();
|
|
715
|
+
}
|
|
716
|
+
return callback(abortedError());
|
|
717
|
+
}
|
|
718
|
+
// reads the streams into the output. The listener is only attached while the node reads
|
|
719
|
+
// something, a deeply nested tree would otherwise stack one per level on the output
|
|
720
|
+
const readInto = (...streams) => {
|
|
721
|
+
if (!activeStreams.length) {
|
|
722
|
+
outputStream.once('close', onOutputClose);
|
|
723
|
+
}
|
|
724
|
+
activeStreams = streams;
|
|
725
|
+
};
|
|
726
|
+
// stops here if the output was destroyed in the meantime
|
|
727
|
+
const aborted = () => {
|
|
728
|
+
if (outputStream.destroyed) {
|
|
729
|
+
callback(abortedError());
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
return false;
|
|
733
|
+
};
|
|
695
734
|
// for multipart nodes, push child nodes
|
|
696
735
|
// for content nodes end the stream
|
|
697
736
|
const finalize = () => {
|
|
698
737
|
let childId = 0;
|
|
699
738
|
const processChildNode = () => {
|
|
739
|
+
if (aborted()) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
700
742
|
if (childId >= this.childNodes.length) {
|
|
701
743
|
outputStream.write('\r\n--' + this.boundary + '--\r\n');
|
|
702
744
|
return callback();
|
|
@@ -719,89 +761,80 @@ class MimeNode {
|
|
|
719
761
|
};
|
|
720
762
|
// pushes node content
|
|
721
763
|
const sendContent = () => {
|
|
722
|
-
if (
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
764
|
+
if (aborted()) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (!this.content) {
|
|
768
|
+
return setImmediate(finalize);
|
|
769
|
+
}
|
|
770
|
+
const createStream = () => {
|
|
771
|
+
if (aborted()) {
|
|
772
|
+
return;
|
|
726
773
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
this.content.once('error', this._contentErrorHandler);
|
|
774
|
+
const contentError = this._takeStreamContent(this.content);
|
|
775
|
+
if (contentError) {
|
|
776
|
+
return callback(contentError);
|
|
731
777
|
}
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
// anything that is not QP or Base54 passes as-is
|
|
745
|
-
localStream = this._getStream(this.content);
|
|
746
|
-
localStream.pipe(outputStream, {
|
|
747
|
-
end: false
|
|
748
|
-
});
|
|
749
|
-
localStream.once('end', finalize);
|
|
750
|
-
}
|
|
751
|
-
localStream.once('error', err => callback(err));
|
|
752
|
-
};
|
|
753
|
-
if (this.content._resolve) {
|
|
754
|
-
const chunks = [];
|
|
755
|
-
let chunklen = 0;
|
|
756
|
-
let returned = false;
|
|
757
|
-
const sourceStream = this._getStream(this.content);
|
|
758
|
-
sourceStream.on('error', err => {
|
|
759
|
-
if (returned) {
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
returned = true;
|
|
763
|
-
callback(err);
|
|
764
|
-
});
|
|
765
|
-
sourceStream.on('readable', () => {
|
|
766
|
-
let chunk;
|
|
767
|
-
while ((chunk = sourceStream.read()) !== null) {
|
|
768
|
-
chunks.push(chunk);
|
|
769
|
-
chunklen += chunk.length;
|
|
770
|
-
}
|
|
771
|
-
});
|
|
772
|
-
sourceStream.on('end', () => {
|
|
773
|
-
if (returned) {
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
returned = true;
|
|
777
|
-
this.content._resolve = false;
|
|
778
|
-
this.content._resolvedValue = Buffer.concat(chunks, chunklen);
|
|
779
|
-
setImmediate(createStream);
|
|
778
|
+
const localStream = this._getStream(this.content);
|
|
779
|
+
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
|
|
780
|
+
const contentStream = transferEncoding === 'base64'
|
|
781
|
+
? new base64.Encoder(options)
|
|
782
|
+
: // outside of text a lone CR or LF is data, encoding it keeps it from being
|
|
783
|
+
// turned into a line break by a newline transform or the receiving side
|
|
784
|
+
new qp.Encoder(Object.assign({}, options, { binary: !/^text\//i.test(this.contentType || '') }));
|
|
785
|
+
readInto(localStream, contentStream);
|
|
786
|
+
contentStream.pipe(outputStream, {
|
|
787
|
+
end: false
|
|
780
788
|
});
|
|
789
|
+
// pipeline reports errors and tears both streams down. It calls back once the
|
|
790
|
+
// encoder took all of its input, the encoded output may still be waiting to
|
|
791
|
+
// be read, so the node is only done once the encoder's readable side ended
|
|
792
|
+
pipeline(localStream, contentStream, err => err && callback(err));
|
|
793
|
+
finished(contentStream, { writable: false }, err => (err ? callback(err) : finalize()));
|
|
781
794
|
}
|
|
782
795
|
else {
|
|
783
|
-
|
|
796
|
+
// anything that is not QP or Base54 passes as-is
|
|
797
|
+
readInto(localStream);
|
|
798
|
+
localStream.pipe(outputStream, {
|
|
799
|
+
end: false
|
|
800
|
+
});
|
|
801
|
+
finished(localStream, { writable: false }, err => (err ? callback(err) : finalize()));
|
|
784
802
|
}
|
|
785
|
-
|
|
803
|
+
};
|
|
804
|
+
if (this.content._resolve) {
|
|
805
|
+
const sourceStream = this._getStream(this.content);
|
|
806
|
+
readInto(sourceStream);
|
|
807
|
+
shared.resolveStream(sourceStream, (err, value) => {
|
|
808
|
+
if (err) {
|
|
809
|
+
return callback(err);
|
|
810
|
+
}
|
|
811
|
+
if (returned) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
this.content._resolve = false;
|
|
815
|
+
this.content._resolvedValue = value;
|
|
816
|
+
setImmediate(createStream);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
setImmediate(createStream);
|
|
786
821
|
}
|
|
787
|
-
return setImmediate(finalize);
|
|
788
822
|
};
|
|
789
823
|
if (this._raw) {
|
|
790
824
|
setImmediate(() => {
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
return callback(this._raw);
|
|
825
|
+
if (aborted()) {
|
|
826
|
+
return;
|
|
794
827
|
}
|
|
795
|
-
|
|
796
|
-
if (
|
|
797
|
-
|
|
828
|
+
const rawError = this._takeStreamContent(this._raw);
|
|
829
|
+
if (rawError) {
|
|
830
|
+
return callback(rawError);
|
|
798
831
|
}
|
|
799
832
|
const raw = this._getStream(this._raw);
|
|
833
|
+
readInto(raw);
|
|
800
834
|
raw.pipe(outputStream, {
|
|
801
835
|
end: false
|
|
802
836
|
});
|
|
803
|
-
raw
|
|
804
|
-
raw.on('end', finalize);
|
|
837
|
+
finished(raw, { writable: false }, err => (err ? callback(err) : finalize()));
|
|
805
838
|
});
|
|
806
839
|
}
|
|
807
840
|
else {
|
|
@@ -917,11 +950,7 @@ class MimeNode {
|
|
|
917
950
|
if (this._raw && typeof this._raw.pipe === 'function') {
|
|
918
951
|
// pre-stream handler. might be triggered if a stream is set as content
|
|
919
952
|
// and 'error' fires before anything is done with this stream
|
|
920
|
-
this.
|
|
921
|
-
this._raw.removeListener('error', this._contentErrorHandler);
|
|
922
|
-
this._raw = err;
|
|
923
|
-
};
|
|
924
|
-
this._raw.once('error', this._contentErrorHandler);
|
|
953
|
+
shared.recordStreamErrors(this._raw);
|
|
925
954
|
}
|
|
926
955
|
return this;
|
|
927
956
|
}
|
|
@@ -947,6 +976,37 @@ class MimeNode {
|
|
|
947
976
|
}
|
|
948
977
|
return false;
|
|
949
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Destroys the content streams of this node and of every node below it
|
|
981
|
+
*
|
|
982
|
+
* @internal
|
|
983
|
+
*/
|
|
984
|
+
_destroyContentStreams() {
|
|
985
|
+
shared.destroyStream(this.content);
|
|
986
|
+
shared.destroyStream(this._raw);
|
|
987
|
+
for (const child of this.childNodes) {
|
|
988
|
+
child._destroyContentStreams();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Checks that a content value can still be read before the node streams it. A stream is
|
|
993
|
+
* refused once it errored, ended or was destroyed: piping it would either never finish or
|
|
994
|
+
* produce an empty body without any error
|
|
995
|
+
*
|
|
996
|
+
* @param content Node content or raw value
|
|
997
|
+
* @returns The error to stop with, or null when the content can be read
|
|
998
|
+
* @internal
|
|
999
|
+
*/
|
|
1000
|
+
_takeStreamContent(content) {
|
|
1001
|
+
if (Object.prototype.toString.call(content) === '[object Error]') {
|
|
1002
|
+
return content;
|
|
1003
|
+
}
|
|
1004
|
+
if (!content || typeof content.pipe !== 'function' || content._resolvedValue) {
|
|
1005
|
+
// the value of a resolved stream is read from the buffered copy
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
return shared.unreadableStreamError(content);
|
|
1009
|
+
}
|
|
950
1010
|
/**
|
|
951
1011
|
* Detects and returns handle to a stream related with the content.
|
|
952
1012
|
*
|
|
@@ -9,10 +9,12 @@ import SESTransport from './ses-transport/index.js';
|
|
|
9
9
|
import * as errors from './errors.js';
|
|
10
10
|
import nmfetch from './fetch/index.js';
|
|
11
11
|
import * as packageData from './package-info.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
12
|
+
// Read on use rather than at load, so that importing the module does not touch the
|
|
13
|
+
// environment (Deno refuses that without --allow-env, even for apps that never use Ethereal)
|
|
14
|
+
const etherealApi = () => (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
|
|
15
|
+
const etherealWeb = () => (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
|
|
16
|
+
const etherealApiKey = () => (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
|
|
17
|
+
const etherealCache = () => ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
|
|
16
18
|
let testAccount = false;
|
|
17
19
|
export function createTransport(transporter, defaults) {
|
|
18
20
|
let options;
|
|
@@ -73,11 +75,11 @@ export function createTestAccount(apiUrl, callback) {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
const done = callback;
|
|
76
|
-
if (
|
|
78
|
+
if (etherealCache() && testAccount) {
|
|
77
79
|
setImmediate(() => done(null, testAccount));
|
|
78
80
|
return promise;
|
|
79
81
|
}
|
|
80
|
-
apiUrl = apiUrl ||
|
|
82
|
+
apiUrl = apiUrl || etherealApi();
|
|
81
83
|
const chunks = [];
|
|
82
84
|
let chunklen = 0;
|
|
83
85
|
const requestHeaders = {};
|
|
@@ -85,8 +87,9 @@ export function createTestAccount(apiUrl, callback) {
|
|
|
85
87
|
requestor: packageData.name,
|
|
86
88
|
version: packageData.version
|
|
87
89
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
const apiKey = etherealApiKey();
|
|
91
|
+
if (apiKey) {
|
|
92
|
+
requestHeaders.Authorization = 'Bearer ' + apiKey;
|
|
90
93
|
}
|
|
91
94
|
const fetchOptions = {
|
|
92
95
|
contentType: 'application/json',
|
|
@@ -154,7 +157,7 @@ export function getTestMessageUrl(info) {
|
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
|
|
157
|
-
return ((testAccount && testAccount.web) ||
|
|
160
|
+
return ((testAccount && testAccount.web) || etherealWeb()) + '/message/' + infoProps.get('MSGID');
|
|
158
161
|
}
|
|
159
162
|
return false;
|
|
160
163
|
}
|
|
@@ -14,6 +14,8 @@ export declare function wrap(str: string, lineLength?: number): string;
|
|
|
14
14
|
export interface QPEncoderOptions {
|
|
15
15
|
/** Maximum length for lines, set to false to disable wrapping */
|
|
16
16
|
lineLength?: number | false | undefined;
|
|
17
|
+
/** The input is binary data: a CR or LF that is not part of a CRLF pair is encoded */
|
|
18
|
+
binary?: boolean | undefined;
|
|
17
19
|
}
|
|
18
20
|
/** The name @types/nodemailer used for QPEncoderOptions */
|
|
19
21
|
export type EncoderOptions = QPEncoderOptions;
|