CI / check (push) Has been cancelled
A self-hosted print studio: images at exact physical sizes, sheet layout with crop marks, tiling, 1:1 PDF. Everything else — image generation, delivery, share links, mail, Telegram — is an optional module that is off by default. This is the neutral public twin of a private tool. Compared to that original: - optional modules behind KLARBILD_MODULES; the default install is print-only - English interface with German as a full second language (1,038 strings); the English sentence is the translation key - no built-in accounts — first boot creates one admin from ADMIN_USERNAME / ADMIN_PASSWORD, or generates a password and logs it once - delivery and backup targets are generic SFTP/FTPS endpoints, off by default - English routes, English identifiers, English comments throughout - two compose files: print-only by default, everything in the full one - AGPL-3.0, CI (build, tests, type check, secret scan), CONTRIBUTING, SECURITY 124 tests green, type check clean, verified end to end against a live instance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nh4A79bbqT9dcd2inzYUdg
67 lines
3.1 KiB
TypeScript
67 lines
3.1 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { isEmail, mailLayout, esc } from '../src/lib/mail.ts';
|
|
|
|
test('Valid addresses are recognised', () => {
|
|
for (const good of ['first.last@example.com', 'a.b+c@sub.example.co.uk', 'x@y.de', 'Gran_1949@example.de']) {
|
|
assert.ok(isEmail(good), `wrongly rejected: ${good}`);
|
|
}
|
|
});
|
|
|
|
test('Invalid addresses are rejected — including the ones with a line break appended', () => {
|
|
const bad = [
|
|
'', ' ', 'not-an-address', 'a@b', '@b.de', 'a@.de', 'a b@c.de', 'a@b .de',
|
|
'a@b.de\nBcc: victim@example.com', // header injection
|
|
'a@b.de\r\nSubject: x',
|
|
'a@b.de\n', // JavaScript's "$" also matches before a line end
|
|
'a'.repeat(250) + '@b.de', // too long
|
|
];
|
|
for (const s of bad) assert.ok(!isEmail(s), `slipped through: ${JSON.stringify(s)}`);
|
|
});
|
|
|
|
test('esc defuses everything that could take the HTML apart', () => {
|
|
assert.equal(esc('<b>x</b>'), '<b>x</b>');
|
|
assert.equal(esc('a & b'), 'a & b');
|
|
assert.equal(esc('said "hello"'), 'said "hello"');
|
|
// Order: & first, otherwise < would become &lt;
|
|
assert.equal(esc('<&>'), '<&>');
|
|
});
|
|
|
|
test('The heading of the mail is always escaped', () => {
|
|
const html = mailLayout('</h1><script>alert(1)</script>', ['Text']);
|
|
assert.ok(!html.includes('<script>'), 'script tag ended up in the HTML');
|
|
assert.match(html, /<script>/);
|
|
});
|
|
|
|
test('The address of the button is escaped', () => {
|
|
const html = mailLayout('Title', ['Text'], { text: 'View', url: 'https://x.de/"><script>alert(1)</script>' });
|
|
assert.ok(!html.includes('<script>'), 'script tag smuggled in through the address');
|
|
});
|
|
|
|
test('Paragraphs are deliberately allowed to contain HTML — which is why callers have to escape', () => {
|
|
// This is on purpose (for <b>), and exactly for that reason it is the caller's
|
|
// duty to send user input through esc() first. The bug the review found was a
|
|
// place that had forgotten to.
|
|
const html = mailLayout('Title', ['A <b>bold</b> word']);
|
|
assert.match(html, /<b>bold<\/b>/);
|
|
// And with esc() it is defused:
|
|
assert.ok(!mailLayout('T', [esc('<b>x</b>')]).includes('<b>x</b>'));
|
|
});
|
|
|
|
test('The mail loads nothing from outside servers', () => {
|
|
const html = mailLayout('Title', ['Text'], { text: 'Go', url: 'https://klarbild.example/s/abc' });
|
|
// No images, no fonts, no stylesheets from outside — that would be a tracking
|
|
// pixel, and many mailboxes would block it anyway.
|
|
assert.ok(!/<img/i.test(html), 'image in the mail HTML');
|
|
assert.ok(!/<link/i.test(html), 'external resource in the mail HTML');
|
|
assert.ok(!/@import|url\(/i.test(html), 'the CSS loads something');
|
|
// Exactly one destination: the link that was handed in.
|
|
const targets = [...html.matchAll(/href="([^"]*)"/g)].map((m) => m[1]);
|
|
assert.deepEqual([...new Set(targets)], ['https://klarbild.example/s/abc']);
|
|
});
|
|
|
|
test('Without a button no link comes into being either', () => {
|
|
const html = mailLayout('Title', ['Text only']);
|
|
assert.ok(!html.includes('href='), 'unexpected link');
|
|
});
|