feat: parse mailbox requests and factor out the address claim

dispatch::request recognises Twt-Register, Twt-Help, Twt-Admit, Twt-Block and
Twt-Registration, validating values but deciding nothing: whether the sender
may do what they asked is the caller's business, as with the rest of this
module.

One command per mail. Two requests have no sensible order, and a request mixed
with a feed command would make the reply ambiguous about what took effect.
Twt-Register takes an optional nick, validated against the nick rule rather
than the path rule, since the value lands in feed metadata.

dispatch::claimed is follow_rotation's Reply-To extraction, lifted out and
documented for what it actually proves: the sender signed this URI. It does
not prove they hold the mailbox it names. follow_rotation is safe on that
alone only because it additionally demands a feed already bound to the address
and a signed chain from the key on file; a registration path has neither and
must resolve the address itself.

Twt-Help ignores any value rather than refusing one — failing a request for
help on a typo would be perverse.

Also narrows i18n's id list back to the ids looked up by name: generating it
from the catalogue made that test assert the catalogue against itself. The
reason-* ids are covered by walking the enum instead, which cannot drift.
This commit is contained in:
randogoth 2026-10-14 19:37:17 +03:00
parent ff289671a5
commit 03e54fea49
5 changed files with 254 additions and 18 deletions

View file

@ -246,22 +246,12 @@ impl Daemon {
incoming: &Incoming,
state: &mut MutexGuard<'_, State>,
) -> Result<Option<usize>, String> {
let Ok(text) = std::str::from_utf8(&incoming.body) else {
let Some(addr) = dispatch::claimed(incoming) else {
return Ok(None);
};
let (fields, _) = fumi_core::message::parse_frontmatter(text);
let Some(reply_to) = fields.get("reply-to") else {
return Ok(None);
};
let Ok(addr) = Address::parse(reply_to) else {
return Ok(None);
};
let Some(user_key) = addr.identity else {
return Ok(None);
};
if user_key != incoming.sender {
return Ok(None);
}
// The claim alone proves only that the sender signed this URI. What
// makes it safe here is the next two lines: the address must already
// own a feed, and the chain below must lead from the key on file.
let Some(index) = state.feeds.iter().position(|f| f.address == addr.short()) else {
return Ok(None);
};