@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.
Files changed (65) hide show
  1. package/.jsii +4 -4
  2. package/lib/rootmail.js +1 -1
  3. package/lib/ses-receive.js +1 -1
  4. package/node_modules/mailparser/.release-please-manifest.json +1 -1
  5. package/node_modules/mailparser/CHANGELOG.md +7 -0
  6. package/node_modules/mailparser/package.json +2 -2
  7. package/node_modules/nodemailer/CHANGELOG.md +27 -0
  8. package/node_modules/nodemailer/dist/cjs/base64/index.d.ts +3 -0
  9. package/node_modules/nodemailer/dist/cjs/base64/index.js +80 -41
  10. package/node_modules/nodemailer/dist/cjs/dkim/index.js +43 -29
  11. package/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +2 -2
  12. package/node_modules/nodemailer/dist/cjs/errors.d.ts +17 -0
  13. package/node_modules/nodemailer/dist/cjs/errors.js +12 -1
  14. package/node_modules/nodemailer/dist/cjs/fetch/index.js +23 -3
  15. package/node_modules/nodemailer/dist/cjs/mailer/index.js +23 -7
  16. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.d.ts +5 -0
  17. package/node_modules/nodemailer/dist/cjs/mailer/mail-message.js +20 -0
  18. package/node_modules/nodemailer/dist/cjs/mime-node/index.d.ts +1 -1
  19. package/node_modules/nodemailer/dist/cjs/mime-node/index.js +165 -105
  20. package/node_modules/nodemailer/dist/cjs/nodemailer.js +12 -9
  21. package/node_modules/nodemailer/dist/cjs/package-info.d.ts +1 -1
  22. package/node_modules/nodemailer/dist/cjs/package-info.js +1 -1
  23. package/node_modules/nodemailer/dist/cjs/qp/index.d.ts +2 -0
  24. package/node_modules/nodemailer/dist/cjs/qp/index.js +103 -33
  25. package/node_modules/nodemailer/dist/cjs/sendmail-transport/index.js +54 -14
  26. package/node_modules/nodemailer/dist/cjs/shared/index.d.ts +32 -1
  27. package/node_modules/nodemailer/dist/cjs/shared/index.js +212 -115
  28. package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +3 -1
  29. package/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +88 -68
  30. package/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +7 -1
  31. package/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +348 -136
  32. package/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +3 -0
  33. package/node_modules/nodemailer/dist/cjs/smtp-pool/index.js +7 -1
  34. package/node_modules/nodemailer/dist/cjs/smtp-pool/pool-resource.js +106 -17
  35. package/node_modules/nodemailer/dist/cjs/smtp-transport/index.js +8 -2
  36. package/node_modules/nodemailer/dist/esm/base64/index.d.ts +3 -0
  37. package/node_modules/nodemailer/dist/esm/base64/index.js +80 -41
  38. package/node_modules/nodemailer/dist/esm/dkim/index.js +43 -29
  39. package/node_modules/nodemailer/dist/esm/dkim/message-parser.js +2 -2
  40. package/node_modules/nodemailer/dist/esm/errors.d.ts +17 -0
  41. package/node_modules/nodemailer/dist/esm/errors.js +10 -0
  42. package/node_modules/nodemailer/dist/esm/fetch/index.js +23 -3
  43. package/node_modules/nodemailer/dist/esm/mailer/index.js +23 -7
  44. package/node_modules/nodemailer/dist/esm/mailer/mail-message.d.ts +5 -0
  45. package/node_modules/nodemailer/dist/esm/mailer/mail-message.js +20 -0
  46. package/node_modules/nodemailer/dist/esm/mime-node/index.d.ts +1 -1
  47. package/node_modules/nodemailer/dist/esm/mime-node/index.js +166 -106
  48. package/node_modules/nodemailer/dist/esm/nodemailer.js +12 -9
  49. package/node_modules/nodemailer/dist/esm/package-info.d.ts +1 -1
  50. package/node_modules/nodemailer/dist/esm/package-info.js +1 -1
  51. package/node_modules/nodemailer/dist/esm/qp/index.d.ts +2 -0
  52. package/node_modules/nodemailer/dist/esm/qp/index.js +103 -33
  53. package/node_modules/nodemailer/dist/esm/sendmail-transport/index.js +54 -14
  54. package/node_modules/nodemailer/dist/esm/shared/index.d.ts +32 -1
  55. package/node_modules/nodemailer/dist/esm/shared/index.js +207 -115
  56. package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +3 -1
  57. package/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +88 -68
  58. package/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +7 -1
  59. package/node_modules/nodemailer/dist/esm/smtp-connection/index.js +348 -136
  60. package/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +3 -0
  61. package/node_modules/nodemailer/dist/esm/smtp-pool/index.js +7 -1
  62. package/node_modules/nodemailer/dist/esm/smtp-pool/pool-resource.js +106 -17
  63. package/node_modules/nodemailer/dist/esm/smtp-transport/index.js +8 -2
  64. package/node_modules/nodemailer/package.json +1 -1
  65. 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 done(err);
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 done(err);
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 done(err);
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 done(err);
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
- return dns.resolve(proxy.hostname, (err, address) => {
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(Array.isArray(address) ? address[0] : address);
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, or the error a content stream emitted before it was read */
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
- const mapper = toUnicode ? urlModule.domainToUnicode : urlModule.domainToASCII;
56
- if (typeof mapper === 'function' && !URL_PARSER_UNSAFE.test(domain)) {
57
- const mapped = mapper(domain);
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._contentErrorHandler = err => {
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
- outputStream.emit('error', err);
636
+ stream.destroy(err);
630
637
  return;
631
638
  }
632
639
  stream.end();
633
640
  });
634
- for (let i = 0, len = this._transforms.length; i < len; i++) {
635
- transform =
636
- typeof this._transforms[i] === 'function' ? this._transforms[i]() : this._transforms[i];
637
- outputStream.once('error', err => {
638
- transform.emit('error', err);
639
- });
640
- outputStream = outputStream.pipe(transform);
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
- transform = new LastNewline();
644
- outputStream.once('error', err => {
645
- transform.emit('error', err);
646
- });
647
- outputStream = outputStream.pipe(transform);
648
- // dkim and stuff
649
- for (let i = 0, len = this._processFuncs.length; i < len; i++) {
650
- transform = this._processFuncs[i];
651
- outputStream = transform(outputStream);
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
- const newlineTransform = winbreak ? new LeWindows() : new LeUnix();
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
- let contentStream;
685
- let localStream;
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 (this.content) {
723
- if (Object.prototype.toString.call(this.content) === '[object Error]') {
724
- // content is already errored
725
- return callback(this.content);
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
- if (typeof this.content.pipe === 'function') {
728
- this.content.removeListener('error', this._contentErrorHandler);
729
- this._contentErrorHandler = err => callback(err);
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 createStream = () => {
733
- if (['quoted-printable', 'base64'].includes(transferEncoding)) {
734
- contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
735
- contentStream.pipe(outputStream, {
736
- end: false
737
- });
738
- contentStream.once('end', finalize);
739
- contentStream.once('error', err => callback(err));
740
- localStream = this._getStream(this.content);
741
- localStream.pipe(contentStream);
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
- setImmediate(createStream);
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
- return;
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 (Object.prototype.toString.call(this._raw) === '[object Error]') {
792
- // content is already errored
793
- return callback(this._raw);
825
+ if (aborted()) {
826
+ return;
794
827
  }
795
- // remove default error handler (if set)
796
- if (typeof this._raw.pipe === 'function') {
797
- this._raw.removeListener('error', this._contentErrorHandler);
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.on('error', err => outputStream.emit('error', err));
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._contentErrorHandler = err => {
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
- const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
13
- const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
14
- const ETHEREAL_API_KEY = (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
15
- const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
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 (ETHEREAL_CACHE && testAccount) {
78
+ if (etherealCache() && testAccount) {
77
79
  setImmediate(() => done(null, testAccount));
78
80
  return promise;
79
81
  }
80
- apiUrl = apiUrl || ETHEREAL_API;
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
- if (ETHEREAL_API_KEY) {
89
- requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
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) || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
160
+ return ((testAccount && testAccount.web) || etherealWeb()) + '/message/' + infoProps.get('MSGID');
158
161
  }
159
162
  return false;
160
163
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.16";
2
+ export declare const version = "10.1.0";
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.16';
3
+ export const version = '10.1.0';
4
4
  export const homepage = 'https://nodemailer.com/';
@@ -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;