feat(mail): manifest-declared mail.env value maps for enum-style SMTP config (#302) - #314
feat(mail): manifest-declared mail.env value maps for enum-style SMTP config (#302)#314bogdanpydev wants to merge 1 commit into
Conversation
… config (#302) Generalize BYO-mail injection so an app whose mail config is a boot-validated enum auto-wires where compose interpolation can't remap tokens. The manifest declares, per app-owned mail env var, the token for each mail state (from: bound|encryption); the brain resolves the final token in Go and stamps it under the app's own name, emitted in both bound and unbound states so the enum is present-and-valid unbound instead of boot-rejecting on an empty value. Closes #302
Confidence Score: 4/5Safe to merge; the one finding is a missing The core mechanism is correct and well-tested across all mail states and rebind cycles. The only gap is that
Important Files Changed
Reviews (1): Last reviewed commit: "feat(mail): manifest-declared mail.env v..." | Re-trigger Greptile |
| for _, name := range names { | ||
| em := mail.Env[name] | ||
| var domainValue string | ||
| switch em.From { | ||
| case manifest.MailFromEncryption: | ||
| domainValue = store.MailEncryptionNone // unbound ⇒ no encryption | ||
| if bound != nil { | ||
| domainValue = bound.Encryption | ||
| } | ||
| case manifest.MailFromBound: | ||
| domainValue = manifest.MailUnbound | ||
| if bound != nil { | ||
| domainValue = manifest.MailBound | ||
| } | ||
| } | ||
| lines = append(lines, name+"="+em.Map[domainValue]) | ||
| } |
There was a problem hiding this comment.
Silent empty-token fallthrough in
mailAppEnvLines. If em.From is neither MailFromEncryption nor MailFromBound, the switch falls through without setting domainValue, leaving it as "". em.Map[""] then returns the zero value (""), and the function appends VAR= — exactly the empty-value that causes a boot-reject enum app to fail. Validation via validateMail prevents this at parse time for manifests loaded with Parse(), but if a Mail struct is ever constructed directly or a new domain is added without updating this switch, the failure mode is silent rather than loud.
| for _, name := range names { | |
| em := mail.Env[name] | |
| var domainValue string | |
| switch em.From { | |
| case manifest.MailFromEncryption: | |
| domainValue = store.MailEncryptionNone // unbound ⇒ no encryption | |
| if bound != nil { | |
| domainValue = bound.Encryption | |
| } | |
| case manifest.MailFromBound: | |
| domainValue = manifest.MailUnbound | |
| if bound != nil { | |
| domainValue = manifest.MailBound | |
| } | |
| } | |
| lines = append(lines, name+"="+em.Map[domainValue]) | |
| } | |
| for _, name := range names { | |
| em := mail.Env[name] | |
| var domainValue string | |
| switch em.From { | |
| case manifest.MailFromEncryption: | |
| domainValue = store.MailEncryptionNone // unbound ⇒ no encryption | |
| if bound != nil { | |
| domainValue = bound.Encryption | |
| } | |
| case manifest.MailFromBound: | |
| domainValue = manifest.MailUnbound | |
| if bound != nil { | |
| domainValue = manifest.MailBound | |
| } | |
| default: | |
| // validateMail rejects unknown From values at parse time; this | |
| // branch guards against direct struct construction or a future | |
| // domain added without updating this switch. | |
| panic("mailAppEnvLines: unhandled mail domain " + em.From) | |
| } | |
| lines = append(lines, name+"="+em.Map[domainValue]) | |
| } |
Closes #302.
Generalizes BYO-mail injection so an app whose mail config is a boot-validated enum can be auto-wired, where compose
environment:interpolation can only substitute / default (:-) / test-presence (:+) and cannot remap one enum's tokens onto another's. Two shipped apps are degraded purely by this — twenty'sEMAIL_DRIVER(logger|smtp, boot-rejects the empty string${MALMO_MAIL_HOST:+smtp}expands to on an unbound box) and vaultwarden'sSMTP_SECURITY(off|starttls|force_tls, a value remap off malmo'snone|starttls|tls).Mechanism
The manifest declares, per app-owned mail env var, the token for each mail state; the brain resolves the final token in Go and stamps it into the
.envunder the app's own name — noMALMO_indirection, the same direct-stamp conventionconfig:uses.internal/manifest—MailgainsEnv map[string]MailEnvMap;validateMailenforces the env name like a configapp_env(uppercase, neverMALMO_/loader var) and requires eachmapcover itsfromdomain exactly.internal/lifecycle— newmailAppEnvLinesresolves each declared var;writeEnvandrewriteEnvMailstamp them in both bound and unbound states (the load-bearing change — unbound resolves thenone/unboundtokens so the enum boots valid instead of rejecting on an empty value), stripping the declared vars by name on rebind so no duplicate lines are left behind.APP_MANIFEST.md#D3,SERVICE_PROVISIONING.md#BYO outgoing mail, gap ledgermail-enable-enum — twenty→ implemented,capabilities.ymlidmail-enable-enum(version 1→2), and a progress entry.Scope
Brain mechanism only. The
catalog/tree was cut over to the store repo, socatalog/twenty(EMAIL_DRIVER) and the store repo'sapps/vaultwarden(SMTP_SECURITY) manifest wiring + the live send checks are the coordinated follow-up against this mechanism (documented in the progress entry's Known gaps).Verification
make checkgreen.mailAppEnvLines100%,validateMail100%.validateMailrejects unknownfrom, incomplete/extra/misspelled domain keys, empty tokens,MALMO_-prefixed / lowercase / reserved names; install unbound stampslogger/offwith noMALMO_MAIL_*; install bound stampssmtp/force_tlsalongsideMALMO_MAIL_*; rebind bound→unbound→bound re-stamps the right tokens each way with exactly one line per var; andmailAppEnvLinesacross all three encryption modes (guarding the manifest ↔store.MailEncryption*alignment).