Files
tillandClaude Opus 5 e2481139a1
CI / check (push) Has been cancelled
Klarbild 1.0.0 — first public release
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
2026-09-15 14:52:41 +00:00

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>'), '&lt;b&gt;x&lt;/b&gt;');
assert.equal(esc('a & b'), 'a &amp; b');
assert.equal(esc('said "hello"'), 'said &quot;hello&quot;');
// Order: & first, otherwise &lt; would become &amp;lt;
assert.equal(esc('<&>'), '&lt;&amp;&gt;');
});
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, /&lt;script&gt;/);
});
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');
});