Initial commit on Forgejo
Some checks are pending
CI and deploy / Test (push) Waiting to run
CI and deploy / Build, push, and deploy (push) Blocked by required conditions

Fresh repository history for elektrine/elektrine-haraka hosted at
https://git.elektrine.com/elektrine/elektrine-haraka.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:42 -04:00
commit 67c1d41a0c
82 changed files with 8713 additions and 0 deletions

173
test/email-builder.test.js Normal file
View file

@ -0,0 +1,173 @@
'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" <maxfield@elektrine.com>',
to: 'Ada Lovelace <ada@example.com>',
cc: 'Grüße Müller <mueller@example.com>',
subject: 'Grüße aus Zürich — 日本語テスト',
text_body: 'Plain body with unicode ✓ and a long stretch ' + 'x'.repeat(300),
html_body: '<p>HTML body ✓ — ünïcödé</p>'
});
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(), '<p>HTML body ✓ — ünïcödé</p>');
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 <sender@elektrine.com>',
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 <recipient${i}@example.com>`);
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: '<p>with attachment</p>',
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);
});