3 Commits
Author SHA1 Message Date
jcoffey-dev 7dfe4c8e70 No legacy listener can be added while the switch is off (LP-4)
While legacy mail protocols are off, x:NetworkListener/set refuses to
create a listener the switch would close, and refuses an update that would
turn an existing one into such a listener -- otherwise changing a
listener's protocol would walk straight past the check. The refusal is
invalidProperties on protocol (or on bind, for a submission listener once
SMTP is unlocked, since its port is what makes it one), and its description
names inbuxa:ProtocolPolicy and says to turn legacy protocols back on first.

The rule is the switch's own, listeners::closes, so what can't be added is
exactly what the switch would close: locked protocols (SMTP, LMTP, HTTP)
and the inbound port are never refused. Putting saved listeners back
(LP-5) writes through the registry, not /set, so it is unaffected.

The e2e changes with it. LP-6's check that an IMAP listener "created by
mistake" refuses sign-in can't be set up any more -- LP-4 is what stops
that listener existing -- so that step now proves test 4 instead: creating
an IMAP listener is refused, naming the policy; an SMTP listener can still
be created; and updating it to IMAP is refused. LP-6 stays proven live over
submission, and its IMAP wording by unit tests. All checks pass.
2026-09-21 10:06:28 -07:00
jcoffey-dev 04252000da Legacy sign-in is refused while the switch is off (LP-6)
The second lock. While legacy mail protocols are off, a sign-in over IMAP,
POP3, ManageSieve or SMTP AUTH is refused for every account, so a listener
that exists by mistake -- or submission, which the SMTP lock keeps open --
still lets nobody in.

The check sits at the top of each protocol's sign-in, before the
credentials are looked at. So the answer is the same for a right password,
a wrong one and an account that doesn't exist; it isn't auth.failed, so it
counts nothing against the account and never feeds the auto-ban; and the
session stays open, since the mail app is being told, not thrown off.

Mail apps read the spec's words (LP-12, at server scope):

  IMAP         NO [ALERT] This server allows only INBUXA webmail and JMAP
               apps. This mail app can't sign in.
  POP3         -ERR [AUTH] ...the same...
  ManageSieve  NO "This server allows only INBUXA webmail and JMAP apps."
  SMTP         535 5.7.0 This server allows only INBUXA webmail and JMAP
               apps. This mail app can't send.

SMTP AUTH is refused on every SMTP listener, port 25 included: only mail
apps authenticate, so inbound delivery is untouched. LMTP is left alone.

The policy is read from the store on each sign-in rather than cached, so
every node of a cluster answers the same the moment the switch turns.

Each refusal raises a new event, auth.legacy-protocol-refused (id 642, info
level, also in the packaged schema), with the protocol as source, the
policy's scope and the domain -- never the account. The session adds the
listener and remote IP.

tests/e2e/legacy_protocols.py now also proves, on a running server: a
normal IMAP and submission sign-in works with the switch on, before and
after; while off, submission refuses the right password and six wrong ones
with the same words and without hanging up; and an IMAP listener created by
mistake while off refuses the right password, a wrong one and an account
that doesn't exist. All 33 checks pass. SMTP sign-ins in the script wait
out a second first: every connection arrives from Docker's gateway, and the
stock inbound throttle takes five a second from one IP.
2026-09-21 09:49:40 -07:00
jcoffey-dev b73aa13fa3 Prove the switch on a running server, not just in unit tests
tests/e2e/legacy_protocols.py boots the debug binary in a container, turns
the switch off and on, and checks the ports themselves. Everything below it
was unit-tested and none of it could have told us this worked.

What it establishes: IMAP and POP3 stop answering while inbound SMTP,
submission and JMAP keep going (LP-1, LP-2, LP-3); the listeners are saved
whole (LP-1); a restart does not reopen them, which is the point of taking
the objects away rather than only the sockets; both come back on their own
without a restart (LP-5); savedListeners empties; and asking to close
submission is overruled to false and reported, with 465 still answering
(LP-21, acceptance test 18). wouldClose named imaps, pop3s and sieve, and
those were exactly the three that closed (LP-16).

One caveat about the method, because it nearly produced a false pass in
reverse. A published Docker port accepts connections whether or not
anything is listening in the container, so connecting proves nothing. The
first run of this script reported IMAP still open after the switch, and
that was the script being wrong, not the server. Each port now has to
speak: a TLS handshake on 993, 995 and 465, a greeting on 25.

It lives under tests/ because target/ is ignored and this is worth keeping.
It derives its own root, needs Docker and a debug build, and clears its
state directory first -- a half-bootstrapped one from an earlier run is no
longer in bootstrap mode and the recovery admin stops working.
2026-09-20 15:55:49 -07:00