v0.89.0: app-email plaintext-only listener (:2526) + split-From mapping

Gap 1: third shim listener :2526, plaintext, does NOT advertise STARTTLS (TLSConfig
nil) — for opportunistic-STARTTLS clients with no cert-skip (cal.com, nextcloud).
Gap 2: SMTPMapping tls_mode (picks port 2525/2526/2465) + from_domain_var (split
local-part + domain for nextcloud's MAIL_FROM_ADDRESS/MAIL_DOMAIN). Default keeps
existing apps on 2525. Hub untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 13:13:40 +02:00
parent 6692a2f631
commit a405505e81
8 changed files with 252 additions and 47 deletions
+60 -10
View File
@@ -212,11 +212,12 @@ func TestSanitizeReason(t *testing.T) {
func TestLifecycle_StartStopIdempotent(t *testing.T) {
fwd := &fakeForwarder{status: 200}
lc := NewLifecycle(Options{
PlainAddr: "127.0.0.1:0",
TLSAddr: "127.0.0.1:0",
Policy: NewPolicy([]string{"felhom.eu"}),
Forwarder: fwd,
Logger: quietLogger(),
PlainAddr: "127.0.0.1:0",
TLSAddr: "127.0.0.1:0",
PlainNoTLSAddr: "127.0.0.1:0",
Policy: NewPolicy([]string{"felhom.eu"}),
Forwarder: fwd,
Logger: quietLogger(),
})
if lc.Running() {
t.Fatal("should not be running before Apply")
@@ -257,14 +258,63 @@ func TestLifecycle_StartStopIdempotent(t *testing.T) {
}
}
// Gap-1: the :2526 listener must NOT advertise STARTTLS (TLSConfig nil), while :2525 must.
// Proven both at the config level and over a real EHLO.
func TestServer_PlainNoTLSListener_NoSTARTTLS(t *testing.T) {
fwd := &fakeForwarder{status: 200}
s, err := New(Options{
PlainAddr: "127.0.0.1:0",
TLSAddr: "127.0.0.1:0",
PlainNoTLSAddr: "127.0.0.1:0",
Policy: NewPolicy([]string{"felhom.eu"}),
Forwarder: fwd,
Logger: quietLogger(),
})
if err != nil {
t.Fatalf("New: %v", err)
}
// Config-level: STARTTLS is advertised iff TLSConfig != nil.
if s.plainNoTLSSrv.TLSConfig != nil {
t.Fatal(":2526 server must have TLSConfig==nil (so STARTTLS is not advertised)")
}
if s.plainSrv.TLSConfig == nil {
t.Fatal(":2525 server must keep TLSConfig (STARTTLS advertised)")
}
if err := s.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
defer s.Close()
// Real EHLO: :2526 must NOT offer STARTTLS; :2525 must.
advertises := func(addr string) bool {
c, err := netsmtp.Dial(addr)
if err != nil {
t.Fatalf("dial %s: %v", addr, err)
}
defer c.Close()
if err := c.Hello("test.local"); err != nil {
t.Fatalf("EHLO %s: %v", addr, err)
}
ok, _ := c.Extension("STARTTLS")
return ok
}
if advertises(s.PlainNoTLSAddr()) {
t.Error(":2526 must NOT advertise STARTTLS over EHLO")
}
if !advertises(s.PlainAddr()) {
t.Error(":2525 must advertise STARTTLS over EHLO")
}
}
func TestServer_EndToEnd_STARTTLS(t *testing.T) {
fwd := &fakeForwarder{status: 200}
s, err := New(Options{
PlainAddr: "127.0.0.1:0",
TLSAddr: "127.0.0.1:0",
Policy: NewPolicy([]string{"felhom.eu"}),
Forwarder: fwd,
Logger: quietLogger(),
PlainAddr: "127.0.0.1:0",
TLSAddr: "127.0.0.1:0",
PlainNoTLSAddr: "127.0.0.1:0",
Policy: NewPolicy([]string{"felhom.eu"}),
Forwarder: fwd,
Logger: quietLogger(),
})
if err != nil {
t.Fatalf("New: %v", err)