v0.88.0: app-email SMTP relay (in-process shim + per-app injection)

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>
This commit is contained in:
2026-06-29 08:45:04 +02:00
parent 7cddb885e0
commit 0e20eb19c1
22 changed files with 1619 additions and 64 deletions
+26
View File
@@ -29,6 +29,32 @@ type Metadata struct {
// container (e.g. Crafty's default-creds.txt). The controller reads + parses that file live and
// surfaces it on the app page, so the customer never has to dig through logs. Optional.
InitialCreds *InitialCredentials `yaml:"initial_credentials,omitempty" json:"initial_credentials,omitempty"`
// SMTPMapping declares how this app's compose env receives the managed app-email relay settings.
// Present only for apps that support outbound email; absent = the app has no email UI/injection.
SMTPMapping *SMTPMapping `yaml:"smtp_mapping,omitempty" json:"smtp_mapping,omitempty"`
}
// SMTPMapping renames the generic relay settings (host / port / security / from / from-name)
// to an app's specific env-var keys, plus any fixed Extra vars (e.g. accept-invalid-cert
// flags). When app-email is on (global + per-app) the controller injects these at
// deploy/redeploy: host = the in-controller shim, port = 2525, security = SecurityValue
// (the app's term for STARTTLS), from = <FromLocal>@<allowlisted-domain>. The values are
// NEVER persisted to app.yaml — they are derived from settings on every compose, so a
// toggle change applies on the next redeploy without rewriting secrets. (Spike §7.)
type SMTPMapping struct {
HostVar string `yaml:"host_var" json:"host_var"` // env key for the shim host (required)
PortVar string `yaml:"port_var" json:"port_var"` // env key for the port (required)
SecurityVar string `yaml:"security_var" json:"security_var"` // env key for the TLS mode (optional)
SecurityValue string `yaml:"security_value" json:"security_value"` // app term for STARTTLS (e.g. "starttls", "TLS")
FromVar string `yaml:"from_var" json:"from_var"` // env key for the From address (required)
FromNameVar string `yaml:"from_name_var" json:"from_name_var"` // env key for the From display name (optional)
FromLocal string `yaml:"from_local" json:"from_local"` // From local-part (defaults to the app slug)
Extra map[string]string `yaml:"extra" json:"extra"` // fixed extra env (accept-invalid-cert flags, etc.)
}
// HasSMTPMapping reports whether this app declares a usable email mapping.
func (m *Metadata) HasSMTPMapping() bool {
return m.SMTPMapping != nil && m.SMTPMapping.HostVar != "" && m.SMTPMapping.FromVar != ""
}
// InitialCredentials tells the controller how to extract an app's auto-generated first-login