John, 2026-09-20: "SMTP and JMAP should be shown with the selector locked,
we want to prevent those two protocols from being shutdown for now."
The selector lists every mail protocol the server speaks, so the operator
sees the whole surface at once; SMTP and JMAP sit in it named and visibly
not switchable. JMAP was never closeable -- closing it locks everyone out
of their mail and the operator out of INBUXA Admin, with no way back but
the host -- and is now visibly so. SMTP is locked whole. LP-3 already
spared inbound on 25; this extends that to submission on 465 and 587,
which LP-1 would otherwise have closed by default.
So closeSubmission has no effect while the lock stands, and is forced to
false. A client that asks for true is not refused: the value is recorded,
overruled, and the overrule reported, because the field is specified and
the lock is meant to be temporary. is_locked() is consulted before
anything else in closes(), so no phrasing of a request reaches past it.
This costs the feature nothing. Submission's ports stay open and sign-in
over them is still refused once LP-6 lands, which is the case acceptance
test 2 already described: a mail app reaching 465 is told it cannot sign
in rather than finding nothing listening. The operator also keeps a port
they may well be forwarding, which is the LP-20 problem in miniature.
The locked set is a server constant the front ends read, not a list they
carry, so unlocking later is a server change and no admin release. The
LP-3 tests stay as they are, to keep it covered if the lock is lifted.
Recorded as LP-21, with acceptance tests 17 and 18.
LP-1 and LP-5, the registry half. close() removes every listener object
the policy closes, saving each one whole first; reopen() puts them back.
The switch removes the listener objects, not just their sockets. A stopped
socket returns on the next restart, which would reopen every port the
operator had just closed, and the operator would have no way to know. A
removed object stays removed, and a server that boots with the switch on
never spawns those listeners at all -- so there is no boot-time special
case to write or to forget.
LP-3 is decided here, on the object rather than the running socket, and
sees every address a listener binds: a submission listener that also binds
25 is inbound and stays. lmtp and http are never candidates.
A listener that cannot be put back does not stop the others; it comes back
with its reason and stays saved for another try (LP-5). A delete the
registry declines is reported as not removed, so the policy never claims a
port is closed while it is still accepting.
Stopping the running socket is still a separate step in common, which owns
the listener registry. Nothing calls any of this yet.
The server-wide legacy-protocols policy: the switch, whether submission
closes with it, the listeners taken away to honour it, and who last
changed it. Stored like inbuxa:AiLimits, as JSON in the fork's subspace,
so an unset field reads as its default and an old record still loads.
closes() is where LP-3 lives. imap, pop3 and manageSieve are named
outright; smtp is not, because an SMTP listener is inbound or submission
depending on its port and nothing else can tell them apart. A listener
bound to 25 is inbound whatever it is called, including one that also
binds 465, so it stays. http and lmtp are never candidates at all.
savedListeners keeps each listener's registry object whole rather than a
few fields of it. LP-5 promises the listeners come back exactly as they
were, and a listener carries proxy networks, TLS timeouts and socket
options that no one should have to re-derive -- a field this code has
never heard of has to survive the round trip too, and a test holds that.
The module is under security/ rather than beside the rebuilt features,
because this one is not a rebuild: upstream has nothing like it.
Still only a fact. Nothing reads this policy yet, so no port closes and
no sign-in is refused; the acting code needs the listener registry and
the config store, which live above this crate.