2026-10-14 18:48:31 +03:00
## Loaded once by `src/i18n.rs`; every string the daemon mails to an author
## lives here, grouped by the mail it belongs to. Operator-facing CLI output is
## deliberately not here — see the module header.
##
## Keep the `-32` style placeables out: these bodies are read in plain-text
## mail clients and on phones, so they stay short lines and no markup.
## Error replies — a mail the daemon could not apply
reply-subject = twtxt: { $reason }
reply-body =
Your mail could not be applied.
Reason: { $reason }
Quoted mail:
{ $quoted }
## Retention — the one-time warning as a feed approaches its size cap
cap-warning-subject = twtxt: feed at 90% of its size cap
cap-warning-body =
Your feed has reached 90% of the { $cap }-byte cap.
In hard-limit mode (the default) a full feed rejects new records; delete
some with Twt-Delete, or choose FIFO pruning with Twt-Prune: fifo, which
drops the oldest records to make room.
## Admission — the mail that hands over the accept token
welcome-subject = twtxt: your feed is ready
welcome-body =
Your smolboid feed is live: { $url }
Post by sending mail to this address with the twt as the body. Manage it
feat: answer unknown senders, gated on proving they hold their address
process now splits by whether the sender owns a feed. The owner path runs
first, so a rotated owner is never mistaken for a registration, and behaves
exactly as before for ordinary mail.
The stranger path is the new one, and every branch of it is gated. No signed
Reply-To means no address to verify or answer, so it stays a silent drop. The
blocked check runs before anything touches the network, so Twt-Block cannot be
sidestepped by re-registering. Silent mode stays silent, which is how every
version before this behaved — except for an admin, who would otherwise have no
way to turn registration back on.
Then the gate the whole change rests on: before any outbound mail, the claimed
address is resolved on its own server and the resolved key must equal the key
that signed the request. Without it a signed Reply-To naming someone else's
address would be enough to take their feed, and replying to the claimed
smol:// URI would pin the sender's key as verified under that address — a
permanent denial of service against whoever really owns it. Every reply is
queued against the short form for the same reason; nothing hands a
self-certifying URI to send.
Registration attempts are counted before the dial rather than after, since one
that fails its key check costs the same connection. A per-cycle dial budget
bounds how long the state lock is held, and a mail over budget keeps its row
instead of being dropped.
Admin commands are recognised only from an admin address, and only Admit,
Block and Registration count as admin commands — an admin's Twt-Help means
Twt-Help. A non-admin sending one gets the ordinary answer and is never told
admin commands exist.
Rig still passes, so the owner path is unchanged in practice as well as in
intent.
2026-10-14 20:21:11 +03:00
with the Twt-Nick, Twt-Delete, Twt-Repost and Twt-Prune frontmatter
fields, and send Twt-Help to get this list again.
2026-10-14 18:57:56 +03:00
## 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.
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
##
## Each MUST be a single line, however long. These are interpolated into the
## reply's Subject, and a frontmatter value carrying a line break makes the
## whole block malformed — smolmail then fails closed toward display (SPEC.md
## sec 5.5) and the reply arrives with no subject at all. Fluent multiline
## values keep their newlines, so there is no safe way to wrap them here.
## `reason::tests::every_reason_is_a_single_line` enforces this.
2026-10-14 18:57:56 +03:00
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
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
reason-fields-beyond-reply-to = post mail carries fields beyond Reply-To (Subject or headers); resend the bare text
2026-10-14 18:57:56 +03:00
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
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
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
2026-10-14 18:57:56 +03:00
reason-no-record-at = no record at { $timestamp }
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.
2026-10-14 19:37:17 +03:00
## Mailbox requests — register, help and the admin commands
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
reason-one-command-per-mail = one command per mail: send Twt-Register, Twt-Help, Twt-Admit, Twt-Block or Twt-Registration on its own, and feed commands in a separate mail
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.
2026-10-14 19:37:17 +03:00
reason-bad-address = `{ $got }` is not a smolmail address
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
reason-registration-mode-unknown = unknown registration mode `{ $got }`, want open, closed or silent
feat: answer unknown senders, gated on proving they hold their address
process now splits by whether the sender owns a feed. The owner path runs
first, so a rotated owner is never mistaken for a registration, and behaves
exactly as before for ordinary mail.
The stranger path is the new one, and every branch of it is gated. No signed
Reply-To means no address to verify or answer, so it stays a silent drop. The
blocked check runs before anything touches the network, so Twt-Block cannot be
sidestepped by re-registering. Silent mode stays silent, which is how every
version before this behaved — except for an admin, who would otherwise have no
way to turn registration back on.
Then the gate the whole change rests on: before any outbound mail, the claimed
address is resolved on its own server and the resolved key must equal the key
that signed the request. Without it a signed Reply-To naming someone else's
address would be enough to take their feed, and replying to the claimed
smol:// URI would pin the sender's key as verified under that address — a
permanent denial of service against whoever really owns it. Every reply is
queued against the short form for the same reason; nothing hands a
self-certifying URI to send.
Registration attempts are counted before the dial rather than after, since one
that fails its key check costs the same connection. A per-cycle dial budget
bounds how long the state lock is held, and a mail over budget keeps its row
instead of being dropped.
Admin commands are recognised only from an admin address, and only Admit,
Block and Registration count as admin commands — an admin's Twt-Help means
Twt-Help. A non-admin sending one gets the ordinary answer and is never told
admin commands exist.
Rig still passes, so the owner path is unchanged in practice as well as in
intent.
2026-10-14 20:21:11 +03:00
## First contact — an unknown sender who may register
info-subject = twtxt: how to get a feed
info-body =
You can publish a twtxt feed here by sending mail.
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
Ask for one, optionally naming the nick you want:
feat: answer unknown senders, gated on proving they hold their address
process now splits by whether the sender owns a feed. The owner path runs
first, so a rotated owner is never mistaken for a registration, and behaves
exactly as before for ordinary mail.
The stranger path is the new one, and every branch of it is gated. No signed
Reply-To means no address to verify or answer, so it stays a silent drop. The
blocked check runs before anything touches the network, so Twt-Block cannot be
sidestepped by re-registering. Silent mode stays silent, which is how every
version before this behaved — except for an admin, who would otherwise have no
way to turn registration back on.
Then the gate the whole change rests on: before any outbound mail, the claimed
address is resolved on its own server and the resolved key must equal the key
that signed the request. Without it a signed Reply-To naming someone else's
address would be enough to take their feed, and replying to the claimed
smol:// URI would pin the sender's key as verified under that address — a
permanent denial of service against whoever really owns it. Every reply is
queued against the short form for the same reason; nothing hands a
self-certifying URI to send.
Registration attempts are counted before the dial rather than after, since one
that fails its key check costs the same connection. A per-cycle dial budget
bounds how long the state lock is held, and a mail over budget keeps its row
instead of being dropped.
Admin commands are recognised only from an admin address, and only Admit,
Block and Registration count as admin commands — an admin's Twt-Help means
Twt-Help. A non-admin sending one gets the ordinary answer and is never told
admin commands exist.
Rig still passes, so the owner path is unchanged in practice as well as in
intent.
2026-10-14 20:21:11 +03:00
fumi send { $mailbox } --header "Twt-Register: yournick"
We will reply with your feed's address and the commands for managing it.
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
Nothing is published until you ask, and this mail is the only one you get
until you do.
Our replies arrive in your requests tier, so look for them there:
fumi fetch && fumi list --requests
Do not accept this address in your client. If you do, it will attach an
accept token to everything you send, and a published record cannot carry
one and still be verifiable — so your posts would be refused.
feat: answer unknown senders, gated on proving they hold their address
process now splits by whether the sender owns a feed. The owner path runs
first, so a rotated owner is never mistaken for a registration, and behaves
exactly as before for ordinary mail.
The stranger path is the new one, and every branch of it is gated. No signed
Reply-To means no address to verify or answer, so it stays a silent drop. The
blocked check runs before anything touches the network, so Twt-Block cannot be
sidestepped by re-registering. Silent mode stays silent, which is how every
version before this behaved — except for an admin, who would otherwise have no
way to turn registration back on.
Then the gate the whole change rests on: before any outbound mail, the claimed
address is resolved on its own server and the resolved key must equal the key
that signed the request. Without it a signed Reply-To naming someone else's
address would be enough to take their feed, and replying to the claimed
smol:// URI would pin the sender's key as verified under that address — a
permanent denial of service against whoever really owns it. Every reply is
queued against the short form for the same reason; nothing hands a
self-certifying URI to send.
Registration attempts are counted before the dial rather than after, since one
that fails its key check costs the same connection. A per-cycle dial budget
bounds how long the state lock is held, and a mail over budget keeps its row
instead of being dropped.
Admin commands are recognised only from an admin address, and only Admit,
Block and Registration count as admin commands — an admin's Twt-Help means
Twt-Help. A non-admin sending one gets the ordinary answer and is never told
admin commands exist.
Rig still passes, so the owner path is unchanged in practice as well as in
intent.
2026-10-14 20:21:11 +03:00
## First contact while registration is closed
info-closed-subject = twtxt: registration is closed
info-closed-body =
Thanks for writing. This instance is not taking new feeds at the moment,
so there is nothing for you to do here yet.
Nothing was published and nothing was kept.
## Twt-Help — the command reference
help-subject = twtxt: your commands
help-body =
Your feed: { $url }
Post by sending mail to this address with the twt as the body. The body is
published byte for byte, at most 160 characters.
Management fields, one per mail:
Twt-Nick: yournick set the displayed nick
Twt-Delete: <timestamp> remove one post or repost
Twt-Repost: <mention> <#id> publish a repost
Twt-Prune: fifo | off what a full feed does at its size cap
Twt-Help: send this list again
help-admin-section =
Admin, from your address:
Twt-Admit: user@host give someone a feed
Twt-Block: user@host withdraw someone's token
Twt-Registration: open | closed | silent
## Admin notifications
admin-notify-subject = twtxt: new feed { $slug }
admin-notify-body =
A feed was created.
Address: { $address }
Feed: { $url }
Fingerprint: { $fingerprint }
Verify that fingerprint out of band if you do not already know this
author. Twt-Block withdraws their token.
admin-ack-subject = twtxt: { $what }
admin-ack-body =
{ $what }
Sent because your address is listed as an admin of this instance.
test: cover self-service registration, and fix what it found
Rig legs for the whole author path with no CLI admit anywhere: an unknown
sender drawing the info mail, Twt-Register creating the feed, the token
arriving so she can publish, Twt-Help, the admin notification, Twt-Block
stopping a feed and Twt-Registration closing the door. Pentest probes for the
attacks: no Reply-To, a claim on someone else's address, a claim on an address
nobody holds, an admin command from a non-admin, and a blocked author coming
back.
Two real bugs, both found by asserting on what the author receives rather than
on what the daemon meant to send.
Five catalogue entries were wrapped across lines, and a reason is interpolated
into the reply's Subject. A frontmatter value carrying a line break makes the
whole block malformed, so smolmail fails closed toward display (SPEC.md sec
5.5) and the reply arrives with no subject at all — every rejection, silently.
The same bug class the parent commit fixed sender-side; step 2 reintroduced it
from the catalogue, because the Rust prose it replaced used backslash
continuations that join without a newline. Reasons are single-line now, with a
test over the enum that fails if one ever wraps again.
And the flow had a step that could not work. Telling an author to accept this
mailbox makes their client attach an Accept token to every later mail, and a
published record is verified by rebuilding the exact bytes that were signed —
which cannot include a token without publishing it. So every post from an
author who followed that advice was refused forever. The info mail now says
not to accept the address and where to find replies instead, and that refusal
has its own reason, since "resend the bare text" is useless advice when the
client adds the field by itself.
Three of my own probes were wrong too, kept here as comments so they are not
rewritten the same way: grep -q under `set -o pipefail` SIGPIPEs its producer
and can report failure for a subject that is plainly present; the refusal
reply lands in the main tier precisely because accepting the mailbox is what
moves replies there; and blocking an address that was never accepted is a
store no-op, so the probe has to register first.
2026-10-14 20:51:21 +03:00
reason-accept-field-on-post = your client attached an accept token because this address is in your contacts; a published record is verified by rebuilding the exact bytes you signed and cannot include a token, so withdraw the acceptance and resend