'use strict'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const crypto = require('crypto'); const { simpleParser } = require('mailparser'); const libmime = require('libmime'); const builder = require('../lib/email-builder'); const BASE = { from: 'sender@elektrine.com', to: 'recipient@example.com', subject: 'test', text_body: 'hello' }; // Every physical line of a built message must respect the RFC 5322 hard limit. function assert_line_limit(email_content) { for (const line of email_content.split('\r\n')) { assert.ok( Buffer.byteLength(line, 'utf8') <= 998, `line exceeds 998 octets: ${line.slice(0, 80)}...` ); } } test('round-trips display names, unicode subject and bodies', async () => { const { email_content, message_id } = builder.build({ from: '"Maxfield" ', to: 'Ada Lovelace ', cc: 'Grüße Müller ', subject: 'Grüße aus Zürich — 日本語テスト', text_body: 'Plain body with unicode ✓ and a long stretch ' + 'x'.repeat(300), html_body: '

HTML body ✓ — ünïcödé

' }); assert_line_limit(email_content); const parsed = await simpleParser(email_content); assert.equal(parsed.from.value[0].name, 'Maxfield'); assert.equal(parsed.from.value[0].address, 'maxfield@elektrine.com'); assert.equal(parsed.to.value[0].name, 'Ada Lovelace'); assert.equal(parsed.to.value[0].address, 'ada@example.com'); assert.equal(parsed.cc.value[0].name, 'Grüße Müller'); assert.equal(parsed.subject, 'Grüße aus Zürich — 日本語テスト'); assert.equal(parsed.text.trim(), 'Plain body with unicode ✓ and a long stretch ' + 'x'.repeat(300)); assert.equal(parsed.html.trim(), '

HTML body ✓ — ünïcödé

'); assert.ok(parsed.messageId.includes(message_id)); assert.ok(parsed.date instanceof Date && !Number.isNaN(parsed.date.getTime())); }); test('drops custom headers that could override structure or identity', async () => { const { email_content } = builder.build({ ...BASE, headers: { 'Content-Type': 'multipart/mixed; boundary=evil', 'Content-Transfer-Encoding': '7bit', 'Reply-To': 'attacker@evil.example', 'Sender': 'attacker@evil.example', 'Return-Path': 'attacker@evil.example', 'X-Campaign': 'summer-2026' } }); assert.ok(!email_content.includes('boundary=evil')); assert.ok(!email_content.includes('attacker@evil.example')); const parsed = await simpleParser(email_content); assert.equal(parsed.headers.get('x-campaign'), 'summer-2026'); // The body's real Content-Type must be the only one. assert.match(parsed.headers.get('content-type').value, /^text\/plain$/); }); test('MIME-encodes non-ASCII custom header values', async () => { const { email_content } = builder.build({ ...BASE, headers: { 'X-Note': 'wörld ünïcödé' } }); // The raw header block (before the blank line) must be pure ASCII. const header_block = email_content.split('\r\n\r\n')[0]; assert.match(header_block, /^[\x00-\x7F]*$/); // mailparser leaves unknown headers raw; decode the encoded-word ourselves. const parsed = await simpleParser(email_content); assert.equal(libmime.decodeWords(parsed.headers.get('x-note')), 'wörld ünïcödé'); }); test('neutralizes CRLF header injection in subject and display names', async () => { const { email_content } = builder.build({ ...BASE, from: 'Evil\r\nBcc: hidden@evil.example\r\n ', subject: 'Hello\r\nX-Injected: 1' }); const parsed = await simpleParser(email_content); assert.equal(parsed.headers.get('x-injected'), undefined); assert.equal(parsed.bcc, undefined); assert.equal(parsed.from.value[0].address, 'sender@elektrine.com'); }); test('folds long recipient lists under the line limit', async () => { const to = Array.from({ length: 40 }, (_, i) => `Recipient Number ${i} With A Fairly Long Display Name `); const { email_content } = builder.build({ ...BASE, to }); assert_line_limit(email_content); const parsed = await simpleParser(email_content); assert.equal(parsed.to.value.length, 40); assert.equal(parsed.to.value[39].address, 'recipient39@example.com'); }); test('re-wraps attachment base64 and round-trips the payload', async () => { const payload = crypto.randomBytes(3000); const { email_content } = builder.build({ ...BASE, html_body: '

with attachment

', attachments: [{ filename: 'data.bin', content_type: 'application/octet-stream', data: payload.toString('base64') }] }); assert_line_limit(email_content); const parsed = await simpleParser(email_content); assert.equal(parsed.attachments.length, 1); assert.equal(parsed.attachments[0].filename, 'data.bin'); assert.ok(parsed.attachments[0].content.equals(payload)); }); test('rejects missing or non-base64 attachment data', () => { assert.throws( () => builder.build({ ...BASE, attachments: [{ filename: 'x.bin' }] }), /Invalid attachment data/ ); assert.throws( () => builder.build({ ...BASE, attachments: [{ filename: 'x.bin', data: 'not base64!!' }] }), /Invalid attachment data/ ); }); test('encodes non-ASCII attachment filenames per RFC 2231', async () => { const { email_content } = builder.build({ ...BASE, attachments: [{ filename: 'résumé 2026.pdf', content_type: 'application/pdf', data: Buffer.from('%PDF-fake').toString('base64') }] }); assert.ok(email_content.includes("filename*=UTF-8''")); const header_block = email_content.split('\r\n\r\n')[0]; assert.match(header_block, /^[\x00-\x7F]*$/); const parsed = await simpleParser(email_content); assert.equal(parsed.attachments[0].filename, 'résumé 2026.pdf'); }); test('quoted-printable bodies stay within soft line limits and round-trip', async () => { const text = '日本語のテキスト mixed with ASCII words. '.repeat(120); const { email_content } = builder.build({ ...BASE, text_body: text }); const body = email_content.split('\r\n\r\n').slice(1).join('\r\n\r\n'); for (const line of body.split('\r\n')) { assert.ok(line.length <= 76, `QP line exceeds 76 chars: ${line.length}`); } const parsed = await simpleParser(email_content); assert.equal(parsed.text.replace(/\n$/, ''), text); });