0e20eb19c1
In-process go-smtp shim (Shape 1): apps → shim → hub → Resend, Resend key stays hub-side. From-header allowlist (reject 5xx pre-hub), single-shot raw-MIME forward, status→SMTP mapping. Global + per-app toggles gate compose-time env injection from .felhom.yml smtp_mapping. Hungarian UI on settings + app config pages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
186 lines
5.4 KiB
Go
186 lines
5.4 KiB
Go
package stacks
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
func newMailManager(t *testing.T, globalOn bool, fromName string) *Manager {
|
|
t.Helper()
|
|
lg := log.New(os.Stderr, "", 0)
|
|
cfg := &config.Config{}
|
|
cfg.MailRelay.ShimHost = "felhom-controller"
|
|
cfg.MailRelay.FromDomains = []string{"felhom.eu"}
|
|
m := &Manager{cfg: cfg, logger: lg, stacks: map[string]*Stack{}}
|
|
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), lg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if globalOn {
|
|
if err := sett.SetAppEmail(true, fromName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
m.settings = sett
|
|
return m
|
|
}
|
|
|
|
func vaultwardenMeta() *Metadata {
|
|
return &Metadata{
|
|
Slug: "vaultwarden",
|
|
DisplayName: "Vaultwarden",
|
|
SMTPMapping: &SMTPMapping{
|
|
HostVar: "SMTP_HOST",
|
|
PortVar: "SMTP_PORT",
|
|
SecurityVar: "SMTP_SECURITY",
|
|
SecurityValue: "starttls",
|
|
FromVar: "SMTP_FROM",
|
|
FromNameVar: "SMTP_FROM_NAME",
|
|
Extra: map[string]string{
|
|
"SMTP_ACCEPT_INVALID_CERTS": "true",
|
|
"SMTP_ACCEPT_INVALID_HOSTNAMES": "true",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func envMap(kvs []string) map[string]string {
|
|
m := make(map[string]string, len(kvs))
|
|
for _, kv := range kvs {
|
|
if i := strings.IndexByte(kv, '='); i >= 0 {
|
|
m[kv[:i]] = kv[i+1:]
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// §7 A / §8: global ON + app ON + mapping → full mapped env injected.
|
|
func TestSMTPEnv_BothTogglesOn_Injects(t *testing.T) {
|
|
m := newMailManager(t, true, "")
|
|
got := envMap(m.smtpEnv(vaultwardenMeta(), true))
|
|
|
|
want := map[string]string{
|
|
"SMTP_HOST": "felhom-controller",
|
|
"SMTP_PORT": "2525",
|
|
"SMTP_SECURITY": "starttls",
|
|
"SMTP_FROM": "vaultwarden@felhom.eu",
|
|
"SMTP_FROM_NAME": "Vaultwarden",
|
|
"SMTP_ACCEPT_INVALID_CERTS": "true",
|
|
"SMTP_ACCEPT_INVALID_HOSTNAMES": "true",
|
|
}
|
|
for k, v := range want {
|
|
if got[k] != v {
|
|
t.Errorf("env %s = %q, want %q", k, got[k], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// §7 E / §8: per-app OFF → nothing injected even when global is ON.
|
|
func TestSMTPEnv_PerAppOff_NoInjection(t *testing.T) {
|
|
m := newMailManager(t, true, "")
|
|
if got := m.smtpEnv(vaultwardenMeta(), false); got != nil {
|
|
t.Fatalf("per-app off must inject nothing, got %v", got)
|
|
}
|
|
}
|
|
|
|
// §7 E / §8: global OFF → nothing injected even when the app toggle is ON.
|
|
func TestSMTPEnv_GlobalOff_NoInjection(t *testing.T) {
|
|
m := newMailManager(t, false, "")
|
|
if got := m.smtpEnv(vaultwardenMeta(), true); got != nil {
|
|
t.Fatalf("global off must inject nothing, got %v", got)
|
|
}
|
|
}
|
|
|
|
// §8: app with no smtp_mapping → nothing injected.
|
|
func TestSMTPEnv_NoMapping_NoInjection(t *testing.T) {
|
|
m := newMailManager(t, true, "")
|
|
meta := &Metadata{Slug: "plex", DisplayName: "Plex"}
|
|
if got := m.smtpEnv(meta, true); got != nil {
|
|
t.Fatalf("app without smtp_mapping must inject nothing, got %v", got)
|
|
}
|
|
}
|
|
|
|
// Household display-name override wins over the app display name.
|
|
func TestSMTPEnv_HouseholdFromNameWins(t *testing.T) {
|
|
m := newMailManager(t, true, "Kovács család")
|
|
got := envMap(m.smtpEnv(vaultwardenMeta(), true))
|
|
if got["SMTP_FROM_NAME"] != "Kovács család" {
|
|
t.Fatalf("SMTP_FROM_NAME = %q, want household name", got["SMTP_FROM_NAME"])
|
|
}
|
|
}
|
|
|
|
// Mealie-style mapping: different env keys + from_local + STARTTLS expressed as "TLS".
|
|
func TestSMTPEnv_MealieMapping(t *testing.T) {
|
|
m := newMailManager(t, true, "")
|
|
meta := &Metadata{
|
|
Slug: "mealie",
|
|
DisplayName: "Mealie",
|
|
SMTPMapping: &SMTPMapping{
|
|
HostVar: "SMTP_HOST",
|
|
PortVar: "SMTP_PORT",
|
|
SecurityVar: "SMTP_AUTH_STRATEGY",
|
|
SecurityValue: "TLS",
|
|
FromVar: "SMTP_FROM_EMAIL",
|
|
FromNameVar: "SMTP_FROM_NAME",
|
|
FromLocal: "mealie",
|
|
},
|
|
}
|
|
got := envMap(m.smtpEnv(meta, true))
|
|
if got["SMTP_AUTH_STRATEGY"] != "TLS" {
|
|
t.Errorf("SMTP_AUTH_STRATEGY = %q, want TLS", got["SMTP_AUTH_STRATEGY"])
|
|
}
|
|
if got["SMTP_FROM_EMAIL"] != "mealie@felhom.eu" {
|
|
t.Errorf("SMTP_FROM_EMAIL = %q, want mealie@felhom.eu", got["SMTP_FROM_EMAIL"])
|
|
}
|
|
if got["SMTP_PORT"] != "2525" {
|
|
t.Errorf("SMTP_PORT = %q, want 2525", got["SMTP_PORT"])
|
|
}
|
|
}
|
|
|
|
func TestMetadata_SMTPMappingParse(t *testing.T) {
|
|
dir := t.TempDir()
|
|
yml := `display_name: Vaultwarden
|
|
slug: vaultwarden
|
|
smtp_mapping:
|
|
host_var: SMTP_HOST
|
|
port_var: SMTP_PORT
|
|
security_var: SMTP_SECURITY
|
|
security_value: starttls
|
|
from_var: SMTP_FROM
|
|
from_name_var: SMTP_FROM_NAME
|
|
extra:
|
|
SMTP_ACCEPT_INVALID_CERTS: "true"
|
|
`
|
|
if err := os.WriteFile(filepath.Join(dir, ".felhom.yml"), []byte(yml), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
meta := LoadMetadata(dir)
|
|
if !meta.HasSMTPMapping() {
|
|
t.Fatal("expected smtp_mapping to parse")
|
|
}
|
|
if meta.SMTPMapping.SecurityValue != "starttls" {
|
|
t.Errorf("security_value = %q", meta.SMTPMapping.SecurityValue)
|
|
}
|
|
if meta.SMTPMapping.Extra["SMTP_ACCEPT_INVALID_CERTS"] != "true" {
|
|
t.Errorf("extra not parsed: %v", meta.SMTPMapping.Extra)
|
|
}
|
|
}
|
|
|
|
// Tolerant decode: an app with no smtp_mapping parses fine and reports no mapping.
|
|
func TestMetadata_NoSMTPMapping(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, ".felhom.yml"), []byte("display_name: Plex\nslug: plex\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
meta := LoadMetadata(dir)
|
|
if meta.HasSMTPMapping() {
|
|
t.Fatal("plex must report no smtp_mapping")
|
|
}
|
|
}
|