feat: adopt smolmail protocol 1.1, adaptive nav shell, and server mail retention

This commit is contained in:
randogoth 2026-09-27 11:07:11 +03:00
parent eaaa3f2ede
commit c693e6fcb9
27 changed files with 1190 additions and 592 deletions

View file

@ -163,24 +163,28 @@ void main() {
});
test("frontmatter parses and fails closed (§5.5)", () {
const spec =
"---\nSubject: Re: the thing\nIn-Reply-To: 4f2a1c9e8b7d6a5f3e2d1c0b9a8f7e6d\nX-Mood: cautiously optimistic\n---\nBody text starts here.";
const inReplyTo =
"4f2a1c9e8b7d6a5f3e2d1c0b9a8f7e6d5c4b3a291807f6e5d4c3b2a1908f7e6d5";
final spec =
"---\nSubject: Re: the thing\nIn-Reply-To: $inReplyTo\nX-Mood: cautiously optimistic\n---\nBody text starts here.";
final parsed = parseFrontmatter(spec);
expect(parsed.fields["Subject"], "Re: the thing");
expect(parsed.fields["In-Reply-To"], "4f2a1c9e8b7d6a5f3e2d1c0b9a8f7e6d");
expect(parsed.fields["subject"], "Re: the thing");
expect(parsed.fields["in-reply-to"], inReplyTo);
expect(parsed.body, "Body text starts here.");
// a malformed line invalidates the whole block, which fails closed toward display
expect(parseFrontmatter("---\nno colon here\n---\nrest").body,
"---\nno colon here\n---\nrest");
expect(parseFrontmatter("---\nSubject: x\nno end").body,
"---\nSubject: x\nno end");
expect(parseFrontmatter("---\nA: 1\nA: 2\n---\ntext").fields["A"], "1");
expect(parseFrontmatter("---\nA: 1\nA: 2\n---\ntext").fields["a"], "1");
// keys compare case-insensitively; the first occurrence wins (§5.5)
expect(parseFrontmatter("---\nSubject: x\nsubject: y\n---\ntext").fields["subject"], "x");
expect(buildFrontmatter(const {}, "---\nactual body"),
"---\n---\n---\nactual body");
expect(buildFrontmatter({"Subject": "hi"}, "there"), "---\nSubject: hi\n---\nthere");
expect(buildFrontmatter(const {}, "plain"), "plain");
expect(
parseFrontmatter("---\n${"X: y\n" * 65}---\nbody").fields["Subject"],
parseFrontmatter("---\n${"X: y\n" * 65}---\nbody").fields["subject"],
isNull);
});
@ -195,6 +199,9 @@ void main() {
expect(parsed.identity, key);
expect(parsed.user, "bob");
expect(() => parseAddress("-bob@h"), throwsA(isA<SmolError>()));
// §3: never two separators in a row
expect(() => parseAddress("a..b@h"), throwsA(isA<SmolError>()));
expect(parseAddress("a.b_c@h").user, "a.b_c");
// §3: fingerprints are the first 20 base32 characters in groups of four
final b32 = b32encode(key);
expect(
@ -210,27 +217,31 @@ void main() {
});
test("rotation chains validate, break and oversize per §7", () {
const username = "alice";
final old = identityFromSeed(randomBytes(32));
final mid = randomBytes(32);
final fresh = randomBytes(32);
final when = nowSeconds();
final chain = [
makeCert(old, mid, when),
makeCert(identityFromSeed(mid), fresh, when),
makeCert(username, old, mid, when),
makeCert(username, identityFromSeed(mid), fresh, when),
];
expect(walkChain(old.publicKey, ed25519PublicKey(fresh), chain), isTrue);
expect(walkChain(old.publicKey, old.publicKey, []), isTrue);
expect(walkChain(username, old.publicKey, ed25519PublicKey(fresh), chain), isTrue);
expect(walkChain(username, old.publicKey, old.publicKey, []), isTrue);
expect(
walkChain(old.publicKey, ed25519PublicKey(fresh), chain.sublist(1)),
walkChain(username, old.publicKey, ed25519PublicKey(fresh), chain.sublist(1)),
isFalse);
final forged = List<Uint8List>.from(chain);
forged[1] = makeCert(identityFromSeed(mid), randomBytes(32), when);
forged[1] = makeCert(username, identityFromSeed(mid), randomBytes(32), when);
expect(
walkChain(old.publicKey, ed25519PublicKey(fresh), forged), isFalse);
walkChain(username, old.publicKey, ed25519PublicKey(fresh), forged), isFalse);
expect(
walkChain(old.publicKey, ed25519PublicKey(fresh),
walkChain(username, old.publicKey, ed25519PublicKey(fresh),
List.filled(17, chain[0])),
isFalse);
// a chain signed for a different username must not validate (§7)
expect(
walkChain("bob", old.publicKey, ed25519PublicKey(fresh), chain), isFalse);
expect(chain[0].length, certLen);
});