refactor: reject mail with a reason identifier, not a sentence

dispatch and apply_commands returned English prose that became the reply
subject, so the pure layers owned author-facing copy. They now return
reason::Reason and the mail-building edge resolves it through the catalogue.

core::rules needed no change: RuleError was already a typed enum, and its
Display stays for author-side tools. The daemon maps the variants itself
rather than borrowing that prose.

Error-path tests now assert on variants, so rewording main.ftl cannot break
them, and PruneModeUnknown carries the value the author actually sent.

Reason::every_variant_resolves_to_copy walks the enum, so a variant added
without a catalogue entry fails in tests rather than mailing someone
"???reason-…???".
This commit is contained in:
randogoth 2026-10-14 18:57:56 +03:00
parent 3279fd3a2d
commit bf6e2eaa15
5 changed files with 244 additions and 42 deletions

View file

@ -35,3 +35,39 @@ welcome-body =
Post by sending mail to this address with the twt as the body. Manage it
with Twt-Nick, Twt-Delete, Twt-Repost and Twt-Prune frontmatter fields
(PLAN.md).
## Rejection reasons — one per `reason::Reason` variant, rendered into the
## reply subject and body above. Each is a sentence fragment completing
## "your mail could not be applied because …", so they start lowercase and
## carry the fix where there is one.
reason-body-not-utf8 = body is not UTF-8
reason-post-not-canonical = post mail is not in canonical form
reason-repost-not-canonical = repost mail is not in canonical form
reason-fields-beyond-reply-to =
post mail carries fields beyond Reply-To (Subject, headers or an Accept
token); resend the bare text
reason-nick-invalid = nick must be 1-30 bytes of a-z, 0-9 or hyphen
reason-delete-needs-timestamp = Twt-Delete needs an RFC 3339 UTC timestamp
reason-prune-mode-unknown = unknown prune mode `{ $got }`, want fifo or off
## Post rules, from core::rules::RuleError
reason-too-many-codepoints = more than 160 Unicode code points
reason-too-many-bytes = more than 640 bytes
reason-forbidden-char = contains TAB, LF, CR or U+2028
reason-bad-timestamp = not an RFC 3339 UTC timestamp at second precision
reason-duplicate-timestamp = timestamp already exists in this feed
## Retention and quota, from the apply step
reason-over-quota-post = daily post quota exceeded; try tomorrow
reason-over-quota-repost = daily repost quota exceeded; try tomorrow
reason-collision = timestamp { $timestamp } already exists; wait a second and resend
reason-feed-full =
feed is at its size cap; delete records with Twt-Delete or choose
Twt-Prune: fifo
reason-reply-to-changed =
your Reply-To does not match the feed's bound author { $author }; send from
that address or mail the operator
reason-no-record-at = no record at { $timestamp }