mailer-deliverability.test.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import assert from 'node:assert/strict';
  2. import { test } from 'node:test';
  3. import {
  4. buildDeliverabilityHeaders,
  5. buildMessage,
  6. createFeedbackId
  7. } from '../src/mailer.js';
  8. test('builds optional deliverability headers without unsafe header injection', () => {
  9. const headers = buildDeliverabilityHeaders({
  10. from: 'sender@example.com',
  11. listUnsubscribeMailto: 'unsubscribe@example.com',
  12. listUnsubscribeUrl: 'https://example.com/unsubscribe/{eventId}?r={recipient}',
  13. listUnsubscribePostEnabled: true,
  14. feedbackId: 'mh.u1.d2.e3:MailHub',
  15. reportAbuseTo: 'abuse@example.com\r\nX-Bad: yes',
  16. csaComplaintsTo: 'csa@example.com',
  17. context: {
  18. eventId: 42,
  19. recipient: 'reader@example.net'
  20. }
  21. });
  22. assert.deepEqual(headers, [
  23. ['List-Unsubscribe', '<mailto:unsubscribe@example.com>, <https://example.com/unsubscribe/42?r=reader%40example.net>'],
  24. ['List-Unsubscribe-Post', 'List-Unsubscribe=One-Click'],
  25. ['Feedback-Id', 'mh.u1.d2.e3:MailHub'],
  26. ['X-Report-Abuse-To', 'abuse@example.com X-Bad: yes'],
  27. ['X-CSA-Complaints', 'csa@example.com'],
  28. ['X-Sender', 'sender@example.com']
  29. ]);
  30. const raw = buildMessage({
  31. from: 'sender@example.com',
  32. to: 'reader@example.net',
  33. subject: 'Deliverability',
  34. text: 'hello',
  35. headers
  36. });
  37. assert.match(raw, /^List-Unsubscribe: <mailto:unsubscribe@example\.com>,\r\n <https:\/\/example\.com\/unsubscribe\/42\?r=reader%40example\.net>$/m);
  38. assert.match(raw, /^List-Unsubscribe-Post: List-Unsubscribe=One-Click$/m);
  39. assert.match(raw, /^Feedback-Id: mh\.u1\.d2\.e3:MailHub$/m);
  40. assert.match(raw, /^X-Report-Abuse-To: abuse@example\.com X-Bad: yes$/m);
  41. assert.doesNotMatch(raw, /^X-Bad:/m);
  42. });
  43. test('folds long deliverability headers and only adds one-click for HTTPS URLs', () => {
  44. const headers = buildDeliverabilityHeaders({
  45. from: 'sender@example.com',
  46. listUnsubscribeMailto: 'unsubscribe@example.com',
  47. listUnsubscribeUrl: 'http://example.com/unsubscribe/{eventId}',
  48. listUnsubscribePostEnabled: true,
  49. context: { eventId: 42 }
  50. });
  51. assert.deepEqual(headers, [
  52. ['List-Unsubscribe', '<mailto:unsubscribe@example.com>, <http://example.com/unsubscribe/42>'],
  53. ['X-Sender', 'sender@example.com']
  54. ]);
  55. const longHeaders = buildDeliverabilityHeaders({
  56. from: 'sender@example.com',
  57. listUnsubscribeMailto: 'unsubscribe@example.com',
  58. listUnsubscribeUrl: `https://example.com/u/${'x'.repeat(35)}`,
  59. listUnsubscribePostEnabled: true
  60. });
  61. const raw = buildMessage({
  62. from: 'sender@example.com',
  63. to: 'reader@example.net',
  64. subject: 'Long header',
  65. text: 'hello',
  66. headers: longHeaders
  67. });
  68. assert.match(raw, /^List-Unsubscribe: <mailto:unsubscribe@example\.com>,\r\n <https:\/\/example\.com\/u\/x+/m);
  69. assert.match(raw, /\r\n <https:\/\/example\.com\/u\/x+>\r\nList-Unsubscribe-Post:/);
  70. for (const line of raw.split('\r\n')) {
  71. if (/^(List-Unsubscribe:| )/.test(line)) assert.ok(line.length <= 78, line);
  72. }
  73. });
  74. test('creates stable opaque Feedback-Id values', () => {
  75. const first = createFeedbackId({
  76. userId: 7,
  77. domainId: 9,
  78. eventId: 11,
  79. secret: 'feedback-secret'
  80. });
  81. const second = createFeedbackId({
  82. userId: 7,
  83. domainId: 9,
  84. eventId: 11,
  85. secret: 'feedback-secret'
  86. });
  87. const different = createFeedbackId({
  88. userId: 7,
  89. domainId: 9,
  90. eventId: 12,
  91. secret: 'feedback-secret'
  92. });
  93. assert.equal(first, second);
  94. assert.notEqual(first, different);
  95. assert.match(first, /^mh\.[a-f0-9]{12}\.[a-f0-9]{12}\.[a-f0-9]{12}:MailHub$/);
  96. });