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
+58
View File
@@ -398,6 +398,23 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
}
data["CurrentValues"] = optValues
}
// App-email per-app toggle — only for apps that declare an smtp_mapping. Shown with
// honest context whether or not the global toggle is on.
if supported, enabled := s.stackMgr.AppEmailStatus(name); supported {
data["AppEmailSupported"] = true
data["AppEmailAppOn"] = enabled
data["AppEmailGlobalOn"] = s.settings.AppEmailEnabled()
local := meta.SMTPMapping.FromLocal
if local == "" {
local = meta.Slug
}
domain := "felhom.eu"
if len(s.cfg.MailRelay.FromDomains) > 0 && s.cfg.MailRelay.FromDomains[0] != "" {
domain = s.cfg.MailRelay.FromDomains[0]
}
data["AppEmailFromAddress"] = local + "@" + domain
}
}
// Memory info for deploy page (only for non-deployed apps)
@@ -881,6 +898,13 @@ func (s *Server) settingsData() map[string]interface{} {
data["NotificationPrefs"] = s.settings.GetNotificationPrefs()
// App-email (SMTP relay) — global toggle. Only meaningful when a hub is configured (the relay
// path runs through the hub); the template hides the control otherwise.
appEmail := s.settings.GetAppEmail()
data["AppEmailEnabled"] = appEmail.Enabled
data["AppEmailFromName"] = appEmail.FromName
data["AppEmailAvailable"] = s.cfg.Hub.URL != "" && s.cfg.MailRelay.HardEnabled()
// Storage paths with display data
storagePaths := s.settings.GetStoragePaths()
connectedCount := 0
@@ -1101,6 +1125,40 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
s.executeTemplate(w, r, "settings", data)
}
// settingsAppEmailHandler saves the global app-email toggle and starts/stops the on-box
// SMTP shim to match (no controller restart needed).
func (s *Server) settingsAppEmailHandler(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
enabled := r.FormValue("app_email_enabled") == "on" || r.FormValue("app_email_enabled") == "true"
fromName := strings.TrimSpace(r.FormValue("app_email_from_name"))
data := s.settingsData()
if err := s.settings.SetAppEmail(enabled, fromName); err != nil {
s.logger.Printf("[ERROR] [web] Failed to save app-email toggle: %v", err)
data["AppEmailError"] = "Hiba az alkalmazás-email beállítás mentésekor"
s.executeTemplate(w, r, "settings", data)
return
}
// Reconcile the shim's running state with the new toggle.
if s.mailShim != nil {
if err := s.mailShim.Apply(enabled); err != nil {
s.logger.Printf("[ERROR] [web] app-email shim could not be %s: %v", map[bool]string{true: "started", false: "stopped"}[enabled], err)
data = s.settingsData()
data["AppEmailError"] = "A beállítás elmentve, de az email-szolgáltatás indítása nem sikerült."
s.executeTemplate(w, r, "settings", data)
return
}
}
s.logger.Printf("[INFO] [web] App-email globally %s (from_name=%q)", map[bool]string{true: "enabled", false: "disabled"}[enabled], fromName)
data = s.settingsData()
if enabled {
data["AppEmailSuccess"] = "Alkalmazás-email bekapcsolva. Kapcsold be az egyes alkalmazásoknál is, ahol email-küldést szeretnél."
} else {
data["AppEmailSuccess"] = "Alkalmazás-email kikapcsolva."
}
s.executeTemplate(w, r, "settings", data)
}
func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) {
data := s.settingsData()