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
@@ -143,6 +143,61 @@ func TestSMTPEnv_MealieMapping(t *testing.T) {
}
}
// §10: port chosen by tls_mode. plaintext→2526, starttls/empty→2525, implicit-tls→2465.
func TestSMTPEnv_PortByTLSMode(t *testing.T) {
m := newMailManager(t, true, "")
cases := map[string]string{"": "2525", "starttls": "2525", "plaintext": "2526", "implicit-tls": "2465"}
for mode, wantPort := range cases {
meta := vaultwardenMeta()
meta.SMTPMapping.TLSMode = mode
got := envMap(m.smtpEnv(meta, true))
if got["SMTP_PORT"] != wantPort {
t.Errorf("tls_mode=%q → SMTP_PORT=%q, want %q", mode, got["SMTP_PORT"], wantPort)
}
}
}
// §10 companion: shimPortForTLSMode must actually branch on the mode (not always 2525).
func TestShimPortForTLSMode(t *testing.T) {
if shimPortForTLSMode("plaintext") == shimPortForTLSMode("starttls") {
t.Fatal("companion: plaintext and starttls must map to DIFFERENT ports (gap-1 fix)")
}
if shimPortForTLSMode("plaintext") != "2526" {
t.Fatalf("plaintext must be 2526, got %q", shimPortForTLSMode("plaintext"))
}
}
// §10: split-From. from_domain_var set → two keys (local + domain); unset → single <local>@<domain>.
func TestSMTPEnv_SplitFrom(t *testing.T) {
m := newMailManager(t, true, "")
meta := &Metadata{
Slug: "nextcloud", DisplayName: "Nextcloud",
SMTPMapping: &SMTPMapping{
HostVar: "SMTP_HOST", PortVar: "SMTP_PORT",
SecurityVar: "SMTP_SECURE", SecurityValue: "",
FromVar: "MAIL_FROM_ADDRESS", FromDomainVar: "MAIL_DOMAIN", FromLocal: "nextcloud",
TLSMode: "plaintext",
},
}
got := envMap(m.smtpEnv(meta, true))
if got["MAIL_FROM_ADDRESS"] != "nextcloud" {
t.Errorf("split From local = %q, want bare 'nextcloud'", got["MAIL_FROM_ADDRESS"])
}
if got["MAIL_DOMAIN"] != "felhom.eu" {
t.Errorf("split From domain = %q, want 'felhom.eu'", got["MAIL_DOMAIN"])
}
if got["SMTP_PORT"] != "2526" {
t.Errorf("nextcloud SMTP_PORT = %q, want 2526 (plaintext)", got["SMTP_PORT"])
}
// Companion: a mapping WITHOUT from_domain_var must produce the single full address (not split).
single := vaultwardenMeta()
gotS := envMap(m.smtpEnv(single, true))
if gotS["SMTP_FROM"] != "vaultwarden@felhom.eu" {
t.Errorf("single-From mapping = %q, want 'vaultwarden@felhom.eu'", gotS["SMTP_FROM"])
}
}
func TestMetadata_SMTPMappingParse(t *testing.T) {
dir := t.TempDir()
yml := `display_name: Vaultwarden