ihasmail 2.0: rebuild as Stalwart-first JMAP webmail

Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a
React 19/Vite SPA. Mail (conversation view, search operators, labels,
sanitised HTML, privacy image proxy, invites, undo send, templates),
calendar (month/week/day/agenda, invites, free/busy, categories,
context menus), contacts (JSContact, groups, vCard), files, Sieve filter
builder (incl. filter-from-message with retroactive apply), vacation,
identities with default + Reply-To, PWA/mobile layout, push via SSE,
in-memory mock Stalwart for dev, Docker + CI.
This commit is contained in:
2026-08-23 01:07:13 -07:00
parent fe17e1d507
commit 645b8b510f
162 changed files with 20398 additions and 1072 deletions
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { sanitizeEmailHtml, sanitizeEditorHtml } from "../html";
describe("sanitizeEmailHtml", () => {
it("removes scripts and event handlers", () => {
const r = sanitizeEmailHtml('<div onclick="x()">hi<script>alert(1)</script><iframe src="https://evil"></iframe></div>');
expect(r.html).not.toContain("script");
expect(r.html).not.toContain("onclick");
expect(r.html).not.toContain("iframe");
});
it("blocks remote images until allowed and maps cid", () => {
const src = '<img src="https://t.example/p.gif"><img src="cid:logo@x"><div style="background:url(https://t.example/b.png)">x</div>';
const blocked = sanitizeEmailHtml(src, { cidMap: { "logo@x": "/api/blob/a/b/logo.png" } });
expect(blocked.remoteCount).toBe(2);
expect(blocked.html).toContain('data-ihm-blocked="1"');
expect(blocked.html).toContain("/api/blob/a/b/logo.png");
expect(blocked.html).not.toMatch(/src="https:\/\/t\.example/);
expect(blocked.html).not.toContain("url(https://t.example");
const allowed = sanitizeEmailHtml(src, { allowRemote: true, proxyRemote: true });
expect(allowed.html).toContain("/api/image?url=https%3A%2F%2Ft.example%2Fp.gif");
});
it("forces links to open in new tabs", () => {
const r = sanitizeEmailHtml('<a href="https://x.io">x</a>');
expect(r.html).toContain('target="_blank"');
expect(r.html).toContain("noopener");
});
it("strips javascript: urls", () => {
const r = sanitizeEmailHtml('<a href="javascript:alert(1)">x</a>');
expect(r.html).not.toContain("javascript:");
});
it("editor sanitizer keeps basic formatting", () => {
expect(sanitizeEditorHtml("<b>x</b><script>1</script>")).toBe("<b>x</b>");
});
});