@nocobase/plugin-workflow-mailer 2.2.16 → 2.2.18

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 (53) hide show
  1. package/dist/externalVersion.js +6 -6
  2. package/dist/node_modules/joi/dist/joi-browser.min.js +1 -1
  3. package/dist/node_modules/joi/lib/index.js +1 -1
  4. package/dist/node_modules/joi/lib/messages.js +35 -6
  5. package/dist/node_modules/joi/lib/types/keys.js +18 -1
  6. package/dist/node_modules/joi/lib/types/link.js +18 -2
  7. package/dist/node_modules/joi/lib/types/string.js +2 -2
  8. package/dist/node_modules/joi/package.json +1 -1
  9. package/dist/node_modules/nodemailer/.ncurc.js +2 -0
  10. package/dist/node_modules/nodemailer/.prettierignore +8 -0
  11. package/dist/node_modules/nodemailer/.prettierrc +12 -0
  12. package/dist/node_modules/nodemailer/.prettierrc.js +2 -0
  13. package/dist/node_modules/nodemailer/.release-please-config.json +9 -0
  14. package/dist/node_modules/nodemailer/eslint.config.js +88 -0
  15. package/dist/node_modules/nodemailer/lib/addressparser/index.js +314 -71
  16. package/dist/node_modules/nodemailer/lib/base64/index.js +9 -13
  17. package/dist/node_modules/nodemailer/lib/dkim/index.js +17 -18
  18. package/dist/node_modules/nodemailer/lib/dkim/message-parser.js +12 -13
  19. package/dist/node_modules/nodemailer/lib/dkim/relaxed-body.js +3 -3
  20. package/dist/node_modules/nodemailer/lib/dkim/sign.js +22 -18
  21. package/dist/node_modules/nodemailer/lib/errors.js +59 -0
  22. package/dist/node_modules/nodemailer/lib/fetch/cookies.js +14 -19
  23. package/dist/node_modules/nodemailer/lib/fetch/index.js +164 -27
  24. package/dist/node_modules/nodemailer/lib/json-transport/index.js +4 -4
  25. package/dist/node_modules/nodemailer/lib/mail-composer/index.js +229 -155
  26. package/dist/node_modules/nodemailer/lib/mailer/index.js +106 -54
  27. package/dist/node_modules/nodemailer/lib/mailer/mail-message.js +106 -68
  28. package/dist/node_modules/nodemailer/lib/mime-funcs/index.js +105 -59
  29. package/dist/node_modules/nodemailer/lib/mime-funcs/mime-types.js +21 -14
  30. package/dist/node_modules/nodemailer/lib/mime-node/index.js +389 -137
  31. package/dist/node_modules/nodemailer/lib/mime-node/last-newline.js +1 -1
  32. package/dist/node_modules/nodemailer/lib/mime-node/le-unix.js +4 -7
  33. package/dist/node_modules/nodemailer/lib/mime-node/le-windows.js +1 -4
  34. package/dist/node_modules/nodemailer/lib/nodemailer.js +1 -1
  35. package/dist/node_modules/nodemailer/lib/punycode/index.js +1 -1
  36. package/dist/node_modules/nodemailer/lib/qp/index.js +33 -22
  37. package/dist/node_modules/nodemailer/lib/sendmail-transport/index.js +54 -44
  38. package/dist/node_modules/nodemailer/lib/ses-transport/index.js +110 -193
  39. package/dist/node_modules/nodemailer/lib/shared/index.js +299 -240
  40. package/dist/node_modules/nodemailer/lib/shared/objects.js +42 -0
  41. package/dist/node_modules/nodemailer/lib/shared/url.js +151 -0
  42. package/dist/node_modules/nodemailer/lib/smtp-connection/data-stream.js +3 -6
  43. package/dist/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +50 -19
  44. package/dist/node_modules/nodemailer/lib/smtp-connection/index.js +415 -223
  45. package/dist/node_modules/nodemailer/lib/smtp-pool/index.js +27 -34
  46. package/dist/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +19 -16
  47. package/dist/node_modules/nodemailer/lib/smtp-transport/index.js +66 -53
  48. package/dist/node_modules/nodemailer/lib/stream-transport/index.js +16 -7
  49. package/dist/node_modules/nodemailer/lib/well-known/index.js +7 -7
  50. package/dist/node_modules/nodemailer/lib/well-known/services.json +328 -38
  51. package/dist/node_modules/nodemailer/lib/xoauth2/index.js +105 -33
  52. package/dist/node_modules/nodemailer/package.json +1 -1
  53. package/package.json +4 -4
@@ -6,14 +6,22 @@ const mimeTypes = require('../mime-funcs/mime-types');
6
6
  const MailComposer = require('../mail-composer');
7
7
  const DKIM = require('../dkim');
8
8
  const httpProxyClient = require('../smtp-connection/http-proxy-client');
9
+ const errors = require('../errors');
9
10
  const util = require('util');
10
- const urllib = require('url');
11
+ const urllib = require('../shared/url');
11
12
  const packageData = require('../../package.json');
12
13
  const MailMessage = require('./mail-message');
13
14
  const net = require('net');
14
15
  const dns = require('dns');
15
16
  const crypto = require('crypto');
16
17
 
18
+ /**
19
+ * Recipients allowed on one message unless the caller sets its own maxRecipients. A backstop
20
+ * against a runaway or hostile recipient list rather than a delivery policy: RFC 5321 only
21
+ * asks a server to accept 100, so a real send is bounded far below this.
22
+ */
23
+ const DEFAULT_MAX_RECIPIENTS = 100000;
24
+
17
25
  /**
18
26
  * Creates an object for exposing the Mail API
19
27
  *
@@ -87,6 +95,11 @@ class Mail extends EventEmitter {
87
95
  this.transporter.on('idle', (...args) => {
88
96
  this.emit('idle', ...args);
89
97
  });
98
+
99
+ // indicates if the sender has became idle and all connections are terminated
100
+ this.transporter.on('clear', (...args) => {
101
+ this.emit('clear', ...args);
102
+ });
90
103
  }
91
104
 
92
105
  /**
@@ -100,17 +113,17 @@ class Mail extends EventEmitter {
100
113
  this.getSocket = false;
101
114
  }
102
115
  return this.transporter[method](...args);
103
- } else {
104
- this.logger.warn(
105
- {
106
- tnx: 'transport',
107
- methodName: method
108
- },
109
- 'Non existing method %s called for transport',
110
- method
111
- );
112
- return false;
113
116
  }
117
+
118
+ this.logger.warn(
119
+ {
120
+ tnx: 'transport',
121
+ methodName: method
122
+ },
123
+ 'Non existing method %s called for transport',
124
+ method
125
+ );
126
+ return false;
114
127
  };
115
128
  });
116
129
 
@@ -151,7 +164,7 @@ class Mail extends EventEmitter {
151
164
  this.getSocket = false;
152
165
  }
153
166
 
154
- let mail = new MailMessage(this, data);
167
+ const mail = new MailMessage(this, data);
155
168
 
156
169
  this.logger.debug(
157
170
  {
@@ -185,6 +198,26 @@ class Mail extends EventEmitter {
185
198
  mail.setPriorityHeaders();
186
199
  mail.setListHeaders();
187
200
 
201
+ const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
202
+ const recipientCount = mail.message.getEnvelope().to.length;
203
+
204
+ if (maxRecipients && recipientCount > maxRecipients) {
205
+ const err = new Error(
206
+ `Message has ${recipientCount} recipients, which is over the ${maxRecipients} allowed by maxRecipients`
207
+ );
208
+ err.code = errors.EMAXRECIPIENTS;
209
+ this.logger.error(
210
+ {
211
+ err,
212
+ tnx: 'transport',
213
+ action: 'send'
214
+ },
215
+ 'Send Error: %s',
216
+ err.message
217
+ );
218
+ return callback(err);
219
+ }
220
+
188
221
  this._processPlugins('stream', mail, err => {
189
222
  if (err) {
190
223
  this.logger.error(
@@ -201,7 +234,7 @@ class Mail extends EventEmitter {
201
234
 
202
235
  if (mail.data.dkim || this.dkim) {
203
236
  mail.message.processFunc(input => {
204
- let dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
237
+ const dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
205
238
  this.logger.debug(
206
239
  {
207
240
  tnx: 'DKIM',
@@ -236,7 +269,14 @@ class Mail extends EventEmitter {
236
269
  }
237
270
 
238
271
  getVersionString() {
239
- return util.format('%s (%s; +%s; %s/%s)', packageData.name, packageData.version, packageData.homepage, this.transporter.name, this.transporter.version);
272
+ return util.format(
273
+ '%s (%s; +%s; %s/%s)',
274
+ packageData.name,
275
+ packageData.version,
276
+ packageData.homepage,
277
+ this.transporter.name,
278
+ this.transporter.version
279
+ );
240
280
  }
241
281
 
242
282
  _processPlugins(step, mail, callback) {
@@ -246,8 +286,8 @@ class Mail extends EventEmitter {
246
286
  return callback();
247
287
  }
248
288
 
249
- let userPlugins = this._userPlugins[step] || [];
250
- let defaultPlugins = this._defaultPlugins[step] || [];
289
+ const userPlugins = this._userPlugins[step] || [];
290
+ const defaultPlugins = this._defaultPlugins[step] || [];
251
291
 
252
292
  if (userPlugins.length) {
253
293
  this.logger.debug(
@@ -268,7 +308,7 @@ class Mail extends EventEmitter {
268
308
 
269
309
  let pos = 0;
270
310
  let block = 'default';
271
- let processPlugins = () => {
311
+ const processPlugins = () => {
272
312
  let curplugins = block === 'default' ? defaultPlugins : userPlugins;
273
313
  if (pos >= curplugins.length) {
274
314
  if (block === 'default' && userPlugins.length) {
@@ -279,7 +319,7 @@ class Mail extends EventEmitter {
279
319
  return callback();
280
320
  }
281
321
  }
282
- let plugin = curplugins[pos++];
322
+ const plugin = curplugins[pos++];
283
323
  plugin(mail, err => {
284
324
  if (err) {
285
325
  return callback(err);
@@ -297,11 +337,11 @@ class Mail extends EventEmitter {
297
337
  * @param {String} proxyUrl Proxy configuration url
298
338
  */
299
339
  setupProxy(proxyUrl) {
300
- let proxy = urllib.parse(proxyUrl);
340
+ const proxy = urllib.parse(proxyUrl);
301
341
 
302
342
  // setup socket handler for the mailer object
303
343
  this.getSocket = (options, callback) => {
304
- let protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
344
+ const protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
305
345
 
306
346
  if (this.meta.has('proxy_handler_' + protocol)) {
307
347
  return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
@@ -311,7 +351,7 @@ class Mail extends EventEmitter {
311
351
  // Connect using a HTTP CONNECT method
312
352
  case 'http':
313
353
  case 'https':
314
- httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
354
+ httpProxyClient(proxy.href, options.port, options.host, this.options.tls || {}, (err, socket) => {
315
355
  if (err) {
316
356
  return callback(err);
317
357
  }
@@ -325,13 +365,15 @@ class Mail extends EventEmitter {
325
365
  case 'socks4':
326
366
  case 'socks4a': {
327
367
  if (!this.meta.has('proxy_socks_module')) {
328
- return callback(new Error('Socks module not loaded'));
368
+ let err = new Error('Socks module not loaded');
369
+ err.code = errors.EPROXY;
370
+ return callback(err);
329
371
  }
330
- let connect = ipaddress => {
331
- let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
332
- let socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
333
- let proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
334
- let connectionOpts = {
372
+ const connect = ipaddress => {
373
+ const proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
374
+ const socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
375
+ const proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
376
+ const connectionOpts = {
335
377
  proxy: {
336
378
  ipaddress,
337
379
  port: Number(proxy.port),
@@ -345,8 +387,8 @@ class Mail extends EventEmitter {
345
387
  };
346
388
 
347
389
  if (proxy.auth) {
348
- let username = decodeURIComponent(proxy.auth.split(':').shift());
349
- let password = decodeURIComponent(proxy.auth.split(':').pop());
390
+ const username = decodeURIComponent(proxy.auth.split(':').shift());
391
+ const password = decodeURIComponent(proxy.auth.split(':').pop());
350
392
  if (proxyV2) {
351
393
  connectionOpts.proxy.userId = username;
352
394
  connectionOpts.proxy.password = password;
@@ -382,7 +424,9 @@ class Mail extends EventEmitter {
382
424
  });
383
425
  }
384
426
  }
385
- callback(new Error('Unknown proxy configuration'));
427
+ let err = new Error('Unknown proxy configuration');
428
+ err.code = errors.EPROXY;
429
+ callback(err);
386
430
  };
387
431
  }
388
432
 
@@ -390,31 +434,39 @@ class Mail extends EventEmitter {
390
434
  if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
391
435
  return callback();
392
436
  }
393
- mail.resolveContent(mail.data, 'html', (err, html) => {
394
- if (err) {
395
- return callback(err);
437
+ mail.resolveContent(
438
+ mail.data,
439
+ 'html',
440
+ { disableFileAccess: mail.data.disableFileAccess, disableUrlAccess: mail.data.disableUrlAccess },
441
+ (err, html) => {
442
+ if (err) {
443
+ return callback(err);
444
+ }
445
+ let cidCounter = 0;
446
+ html = (html || '')
447
+ .toString()
448
+ .replace(
449
+ /(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi,
450
+ (match, prefix, dataUri, mimeType) => {
451
+ const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
452
+ if (!mail.data.attachments) {
453
+ mail.data.attachments = [];
454
+ }
455
+ if (!Array.isArray(mail.data.attachments)) {
456
+ mail.data.attachments = [].concat(mail.data.attachments || []);
457
+ }
458
+ mail.data.attachments.push({
459
+ path: dataUri,
460
+ cid,
461
+ filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
462
+ });
463
+ return prefix + 'cid:' + cid;
464
+ }
465
+ );
466
+ mail.data.html = html;
467
+ callback();
396
468
  }
397
- let cidCounter = 0;
398
- html = (html || '')
399
- .toString()
400
- .replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
401
- let cid = crypto.randomBytes(10).toString('hex') + '@localhost';
402
- if (!mail.data.attachments) {
403
- mail.data.attachments = [];
404
- }
405
- if (!Array.isArray(mail.data.attachments)) {
406
- mail.data.attachments = [].concat(mail.data.attachments || []);
407
- }
408
- mail.data.attachments.push({
409
- path: dataUri,
410
- cid,
411
- filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
412
- });
413
- return prefix + 'cid:' + cid;
414
- });
415
- mail.data.html = html;
416
- callback();
417
- });
469
+ );
418
470
  }
419
471
 
420
472
  set(key, value) {
@@ -4,6 +4,11 @@ const shared = require('../shared');
4
4
  const MimeNode = require('../mime-node');
5
5
  const mimeFuncs = require('../mime-funcs');
6
6
 
7
+ // Only an own key counts as already set. `key in obj` also matches every member of
8
+ // Object.prototype, which silently drops a transporter default legitimately named
9
+ // toString or constructor.
10
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
11
+
7
12
  class MailMessage {
8
13
  constructor(mailer, data) {
9
14
  this.mailer = mailer;
@@ -11,43 +16,60 @@ class MailMessage {
11
16
  this.message = null;
12
17
 
13
18
  data = data || {};
14
- let options = mailer.options || {};
15
- let defaults = mailer._defaults || {};
19
+ const options = mailer.options || {};
20
+ const defaults = mailer._defaults || {};
16
21
 
17
- Object.keys(data).forEach(key => {
18
- this.data[key] = data[key];
19
- });
22
+ shared.copyOwnKeys(this.data, data);
20
23
 
21
24
  this.data.headers = this.data.headers || {};
22
25
 
23
- // apply defaults
24
- Object.keys(defaults).forEach(key => {
25
- if (!(key in this.data)) {
26
- this.data[key] = defaults[key];
27
- } else if (key === 'headers') {
28
- // headers is a special case. Allow setting individual default headers
29
- Object.keys(defaults.headers).forEach(key => {
30
- if (!(key in this.data.headers)) {
31
- this.data.headers[key] = defaults.headers[key];
32
- }
33
- });
34
- }
35
- });
26
+ // Apply defaults. `_defaults` is caller supplied too, it is the second argument of
27
+ // createTransport, so it needs the same treatment as `data` above
28
+ shared.copyOwnKeys(this.data, defaults, key => hasOwn(this.data, key));
29
+
30
+ // headers is a special case. Allow setting individual default headers
31
+ shared.copyOwnKeys(this.data.headers, defaults.headers, key => hasOwn(this.data.headers, key));
36
32
 
37
33
  // force specific keys from transporter options
38
- ['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
34
+ ['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey', 'maxRecipients'].forEach(key => {
39
35
  if (key in options) {
40
36
  this.data[key] = options[key];
41
37
  }
42
38
  });
39
+
40
+ // The access flags are a sandbox rather than a message field, so `defaults` counts as
41
+ // transporter configuration for them. For a transporter plugin it is the only channel
42
+ // there is, createTransport leaves `options` undefined for one, and the defaults copy
43
+ // above yields to anything the message already set, which let message data switch the
44
+ // sandbox back off. Closing is one way here, same as in resolveContent below: either
45
+ // side may switch a flag on, neither can switch off what the other closed.
46
+ ['disableFileAccess', 'disableUrlAccess'].forEach(key => {
47
+ if (!(key in options) && hasOwn(defaults, key)) {
48
+ this.data[key] = this.data[key] || defaults[key];
49
+ }
50
+ });
43
51
  }
44
52
 
45
- resolveContent(...args) {
46
- return shared.resolveContent(...args);
53
+ resolveContent(data, key, options, callback) {
54
+ // Most plugins call this with the legacy (data, key, callback) signature, which carries
55
+ // no access policy. The policy belongs to the message, so apply it here. Explicit
56
+ // options may only tighten it, never reopen what the transporter closed.
57
+ if (!callback && typeof options === 'function') {
58
+ callback = options;
59
+ options = false;
60
+ }
61
+ options = options || {};
62
+
63
+ const policy = {
64
+ disableFileAccess: this.data.disableFileAccess || options.disableFileAccess,
65
+ disableUrlAccess: this.data.disableUrlAccess || options.disableUrlAccess
66
+ };
67
+
68
+ return shared.resolveContent(data, key, policy, callback);
47
69
  }
48
70
 
49
71
  resolveAll(callback) {
50
- let keys = [
72
+ const keys = [
51
73
  [this.data, 'html'],
52
74
  [this.data, 'text'],
53
75
  [this.data, 'watchHtml'],
@@ -64,7 +86,8 @@ class MailMessage {
64
86
  if (this.data.attachments && this.data.attachments.length) {
65
87
  this.data.attachments.forEach((attachment, i) => {
66
88
  if (!attachment.filename) {
67
- attachment.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
89
+ attachment.filename =
90
+ (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
68
91
  if (attachment.filename.indexOf('.') < 0) {
69
92
  attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
70
93
  }
@@ -78,9 +101,9 @@ class MailMessage {
78
101
  });
79
102
  }
80
103
 
81
- let mimeNode = new MimeNode();
104
+ const mimeNode = new MimeNode();
82
105
 
83
- let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
106
+ const addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
84
107
 
85
108
  addressKeys.forEach(address => {
86
109
  let value;
@@ -96,7 +119,7 @@ class MailMessage {
96
119
  }
97
120
  });
98
121
 
99
- let singleKeys = ['from', 'sender'];
122
+ const singleKeys = ['from', 'sender'];
100
123
  singleKeys.forEach(address => {
101
124
  if (this.data[address]) {
102
125
  this.data[address] = this.data[address].shift();
@@ -104,41 +127,46 @@ class MailMessage {
104
127
  });
105
128
 
106
129
  let pos = 0;
107
- let resolveNext = () => {
130
+ const resolveNext = () => {
108
131
  if (pos >= keys.length) {
109
132
  return callback(null, this.data);
110
133
  }
111
- let args = keys[pos++];
134
+ const args = keys[pos++];
112
135
  if (!args[0] || !args[0][args[1]]) {
113
136
  return resolveNext();
114
137
  }
115
- shared.resolveContent(...args, (err, value) => {
116
- if (err) {
117
- return callback(err);
118
- }
138
+ shared.resolveContent(
139
+ ...args,
140
+ { disableFileAccess: this.data.disableFileAccess, disableUrlAccess: this.data.disableUrlAccess },
141
+ (err, value) => {
142
+ if (err) {
143
+ return callback(err);
144
+ }
119
145
 
120
- let node = {
121
- content: value
122
- };
123
- if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
124
- Object.keys(args[0][args[1]]).forEach(key => {
125
- if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
126
- node[key] = args[0][args[1]][key];
127
- }
128
- });
129
- }
146
+ const node = {
147
+ content: value
148
+ };
149
+ if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
150
+ // The keys are the caller's, so copying them takes the same "__proto__"
151
+ // rule as the constructor. `key in node` stays as the already-set test
152
+ // here, unlike for the defaults: it also skips the Object.prototype
153
+ // member names, and letting message data land a `toString` string on a
154
+ // node only buys a TypeError the first time something stringifies it.
155
+ shared.copyOwnKeys(node, args[0][args[1]], key => key in node || ['content', 'path', 'href', 'raw'].includes(key));
156
+ }
130
157
 
131
- args[0][args[1]] = node;
132
- resolveNext();
133
- });
158
+ args[0][args[1]] = node;
159
+ resolveNext();
160
+ }
161
+ );
134
162
  };
135
163
 
136
164
  setImmediate(() => resolveNext());
137
165
  }
138
166
 
139
167
  normalize(callback) {
140
- let envelope = this.data.envelope || this.message.getEnvelope();
141
- let messageId = this.message.messageId();
168
+ const envelope = this.message.getEnvelope();
169
+ const messageId = this.message.messageId();
142
170
 
143
171
  this.resolveAll((err, data) => {
144
172
  if (err) {
@@ -183,6 +211,9 @@ class MailMessage {
183
211
 
184
212
  data.normalizedHeaders = {};
185
213
  Object.keys(data.headers || {}).forEach(key => {
214
+ if (shared.isProtoKey(key)) {
215
+ return;
216
+ }
186
217
  let value = [].concat(data.headers[key] || []).shift();
187
218
  value = (value && value.value) || value;
188
219
  if (value) {
@@ -194,7 +225,7 @@ class MailMessage {
194
225
  });
195
226
 
196
227
  if (data.list && typeof data.list === 'object') {
197
- let listHeaders = this._getListHeaders(data.list);
228
+ const listHeaders = this._getListHeaders(data.list);
198
229
  listHeaders.forEach(entry => {
199
230
  data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
200
231
  });
@@ -244,13 +275,11 @@ class MailMessage {
244
275
  return;
245
276
  }
246
277
  // add optional List-* headers
247
- if (this.data.list && typeof this.data.list === 'object') {
248
- this._getListHeaders(this.data.list).forEach(listHeader => {
249
- listHeader.value.forEach(value => {
250
- this.message.addHeader(listHeader.key, value);
251
- });
278
+ this._getListHeaders(this.data.list).forEach(listHeader => {
279
+ listHeader.value.forEach(value => {
280
+ this.message.addHeader(listHeader.key, value);
252
281
  });
253
- }
282
+ });
254
283
  }
255
284
 
256
285
  _getListHeaders(listData) {
@@ -270,23 +299,30 @@ class MailMessage {
270
299
  }
271
300
 
272
301
  if (value && value.url) {
302
+ // strip CR/LF so a comment can't inject extra header lines. DEL is neither
303
+ // qtext nor ctext, so it can not be carried literally by either construct
304
+ // and has to become an encoded word like any other non-plaintext value
305
+ let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
306
+ const needsEncoding = !mimeFuncs.isPlainText(comment) || /\x7f/.test(comment);
307
+
273
308
  if (key.toLowerCase().trim() === 'id') {
274
- // List-ID: "comment" <domain>
275
- let comment = value.comment || '';
276
- if (mimeFuncs.isPlainText(comment)) {
277
- comment = '"' + comment + '"';
278
- } else {
279
- comment = mimeFuncs.encodeWord(comment);
280
- }
281
-
282
- return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, '');
309
+ // List-ID: "comment" <domain>, where an unescaped quote or a trailing
310
+ // backslash in the comment would swallow the <domain> behind it
311
+ comment = needsEncoding ? mimeFuncs.encodeWord(comment) : mimeFuncs.quoteString(comment);
312
+
313
+ // List-ID expects a bare domain-like identifier, so strip the
314
+ // scheme prefix that _formatListUrl adds or passes through
315
+ return (
316
+ (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<')
317
+ );
283
318
  }
284
319
 
285
320
  // List-*: <http://domain> (comment)
286
- let comment = value.comment || '';
287
- if (!mimeFuncs.isPlainText(comment)) {
288
- comment = mimeFuncs.encodeWord(comment);
289
- }
321
+ // the ctext specials go out as quoted-pairs, otherwise a ")" closes the
322
+ // comment early and leaves the rest as junk, an unpaired "(" opens a
323
+ // nested comment that never closes, and a trailing backslash escapes
324
+ // the closing ")" so the comment swallows whatever follows it
325
+ comment = needsEncoding ? mimeFuncs.encodeWord(comment) : comment.replace(/[()\\]/g, '\\$&');
290
326
 
291
327
  return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : '');
292
328
  }
@@ -300,7 +336,9 @@ class MailMessage {
300
336
  }
301
337
 
302
338
  _formatListUrl(url) {
303
- url = url.replace(/[\s<]+|[\s>]+/g, '');
339
+ // a url has no way to carry a control char or DEL, and the angle brackets around it
340
+ // are not a quoting construct, so anything left here lands in the header raw
341
+ url = url.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').replace(/[\s<]+|[\s>]+/g, '');
304
342
  if (/^(https?|mailto|ftp):/.test(url)) {
305
343
  return '<' + url + '>';
306
344
  }