feat: custom frontmatter headers in compose and replies
Adds Key: value header rows under the subject, validated with carrier's rules: keys of 1-64 letters, digits and dashes, unique case-insensitively, none of the fields the client sets itself, and the whole block inside the reader's budget. A reply to a message with the sender's own fields asks whether to carry copies over. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d19568c1c0
commit
0ee4a7a65f
5 changed files with 178 additions and 9 deletions
|
|
@ -155,6 +155,42 @@ for (const v of vectors.ed_to_x25519) {
|
|||
eq("fm-too-many-keys", proto.parseFrontmatter("---\n" + "X: y\n".repeat(65) + "---\nbody").fields.subject ?? "none", "none");
|
||||
}
|
||||
|
||||
// --- compose headers ----------------------------------------------------------
|
||||
|
||||
{
|
||||
const refused = (text) => {
|
||||
try { proto.headerRow(text); return null; } catch (err) { return err instanceof proto.FieldError ? err.id : err; }
|
||||
};
|
||||
eq("hdr-pair", proto.headerRow("Urgency: high").join("|"), "Urgency|high");
|
||||
eq("hdr-trimmed", proto.headerRow(" spaced-key : trimmed value ").join("|"), "spaced-key|trimmed value");
|
||||
eq("hdr-empty", proto.headerRow(" "), null);
|
||||
eq("hdr-no-colon", refused("no colon"), "compose-header-colon-missing");
|
||||
eq("hdr-empty-key", refused(": value"), "compose-header-invalid-key");
|
||||
eq("hdr-bad-key", refused("bad key: v"), "compose-header-invalid-key");
|
||||
eq("hdr-long-key", refused(`${"k".repeat(65)}: v`), "compose-header-invalid-key");
|
||||
eq("hdr-inner-newline", refused("multi\nline: v"), "compose-header-invalid-key");
|
||||
eq("hdr-multiline-value", refused("k: a\nb"), "compose-header-value-multiline");
|
||||
// the client's own fields would be shadowed by a copy
|
||||
for (const reserved of ["Subject: x", "subject: x", "Reply-To: x", "In-Reply-To: x", "accept: x"])
|
||||
eq(`hdr-reserved[${reserved}]`, refused(reserved), "compose-header-reserved-key");
|
||||
|
||||
const rejects = (pairs) => {
|
||||
try { proto.validateHeaders(pairs); return null; } catch (err) { return err.id; }
|
||||
};
|
||||
eq("hdr-valid", rejects([["Urgency", "high"]]), null);
|
||||
eq("hdr-duplicate", rejects([["Urgency", "high"], ["urgency", "low"]]), "compose-header-duplicate-key");
|
||||
eq("hdr-too-large", rejects([0, 1, 2, 3, 4].map(i => [`X-Key${i}`, "v".repeat(1024)])), "compose-headers-too-large");
|
||||
eq("hdr-too-many", rejects(Array.from({ length: 61 }, (_, i) => [`K${i}`, "v"])), "compose-headers-too-many");
|
||||
|
||||
// only the sender's own fields carry into a reply
|
||||
const rows = proto.replyRows({ priority: "high", "in-reply-to": "ab".repeat(32), subject: "shadow", accept: "token" });
|
||||
eq("hdr-reply-rows", rows.join("|"), "priority: high");
|
||||
|
||||
// user rows round-trip through the block the reader parses
|
||||
const built = proto.buildFrontmatter([["Subject", "hi"], ["X-Mood", "fine"]], "body");
|
||||
eq("hdr-roundtrip", proto.parseFrontmatter(built).fields["x-mood"], "fine");
|
||||
}
|
||||
|
||||
// --- addresses (§3) -----------------------------------------------------------
|
||||
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue