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:
@@ -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()
|
||||
|
||||
|
||||
@@ -81,12 +81,27 @@ type Server struct {
|
||||
// manual trigger goes through the loop (stop stacks → backup → resume), never a bare agent call.
|
||||
backupTrigger BackupTrigger
|
||||
|
||||
// App-email SMTP shim lifecycle (optional — nil when no hub is configured or the kill-switch is
|
||||
// off). The global app-email settings toggle calls Apply() so the shim starts/stops at runtime.
|
||||
mailShim MailShimController
|
||||
|
||||
// Debug mode support
|
||||
logBuffer *LogBuffer
|
||||
debugCallbacks *DebugCallbacks
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// MailShimController is the lifecycle handle the settings toggle uses to start/stop the
|
||||
// app-email SMTP shim at runtime. Satisfied by *mailrelay.Lifecycle (kept as an interface
|
||||
// to avoid a web→mailrelay import cycle risk and to allow a fake in tests).
|
||||
type MailShimController interface {
|
||||
Apply(enabled bool) error
|
||||
Running() bool
|
||||
}
|
||||
|
||||
// SetMailShim wires the app-email shim lifecycle (optional).
|
||||
func (s *Server) SetMailShim(c MailShimController) { s.mailShim = c }
|
||||
|
||||
func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, sched *scheduler.Scheduler, sett *settings.Settings, alertMgr *AlertManager, notif *notify.Notifier, updater *selfupdate.Updater, logger *log.Logger, version string) *Server {
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
@@ -253,6 +268,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.settingsNotificationsHandler(w, r)
|
||||
case path == "/settings/notifications/test" && r.Method == http.MethodPost:
|
||||
s.settingsNotificationsTestHandler(w, r)
|
||||
case path == "/settings/app-email" && r.Method == http.MethodPost:
|
||||
s.settingsAppEmailHandler(w, r)
|
||||
case path == "/settings/storage/add" && r.Method == http.MethodPost:
|
||||
s.settingsStorageAddHandler(w, r)
|
||||
case path == "/settings/storage/remove" && r.Method == http.MethodPost:
|
||||
@@ -287,6 +304,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/backup") && r.Method == http.MethodPost:
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(path, "/stacks/"), "/backup")
|
||||
s.tier2ConfigSaveHandler(w, r, name)
|
||||
case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/app-email") && r.Method == http.MethodPost:
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(path, "/stacks/"), "/app-email")
|
||||
s.appEmailToggleHandler(w, r, name)
|
||||
case path == "/import":
|
||||
s.importPageHandler(w, r)
|
||||
case path == "/static/style.css":
|
||||
|
||||
@@ -393,6 +393,33 @@
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{if .AppEmailSupported}}
|
||||
<div class="app-optional-config">
|
||||
<h3>Email-küldés</h3>
|
||||
<p class="config-group-desc">
|
||||
Ez az alkalmazás tud emailt küldeni (pl. jelszó-visszaállítás, meghívók) a Felhom-on keresztül,
|
||||
külön szolgáltató beállítása nélkül. A feladó címe: <strong>{{.AppEmailFromAddress}}</strong>.
|
||||
</p>
|
||||
{{if not .AppEmailGlobalOn}}
|
||||
<p class="alert alert-warning" style="margin-bottom:1rem">
|
||||
Az alkalmazás-email jelenleg ki van kapcsolva globálisan. Kapcsold be a
|
||||
<a href="/settings">Beállítások</a> oldalon, hogy itt is működjön.
|
||||
</p>
|
||||
{{end}}
|
||||
<form method="POST" action="/stacks/{{.Stack.Name}}/app-email">
|
||||
{{$.CSRFField}}
|
||||
<label style="display:flex;align-items:center;gap:.5rem;margin-bottom:1rem">
|
||||
<input type="checkbox" name="app_email_enabled" value="on" {{if .AppEmailAppOn}}checked{{end}}>
|
||||
Email-küldés engedélyezése ennél az alkalmazásnál
|
||||
</label>
|
||||
<div class="config-actions">
|
||||
<button class="btn btn-primary" type="submit">Mentés</button>
|
||||
<span class="config-group-desc" style="margin-left:.5rem">A mentés újraindítja az alkalmazást.</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if and (not .AlreadyDeployed) .MemoryInfo}}
|
||||
{{with .MemoryInfo}}
|
||||
{{if .Available}}
|
||||
|
||||
@@ -1095,6 +1095,41 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Section: App email (SMTP relay) -->
|
||||
{{if .AppEmailAvailable}}
|
||||
<div class="settings-card">
|
||||
<h3>Alkalmazás-email</h3>
|
||||
<p class="settings-card-desc">
|
||||
Az alkalmazások a Felhom-on keresztül küldhetnek emailt (pl. jelszó-visszaállítás, meghívók),
|
||||
külön email-szolgáltató beállítása nélkül. A feladó címe minden alkalmazásnál a saját
|
||||
<em><alkalmazás>@felhom.eu</em> címe lesz.
|
||||
</p>
|
||||
{{if .AppEmailSuccess}}<div class="alert alert-info">{{.AppEmailSuccess}}</div>{{end}}
|
||||
{{if .AppEmailError}}<div class="alert alert-error">{{.AppEmailError}}</div>{{end}}
|
||||
<form method="POST" action="/settings/app-email">
|
||||
{{.CSRFField}}
|
||||
<div class="form-group">
|
||||
<label style="display:flex;align-items:center;gap:.5rem">
|
||||
<input type="checkbox" name="app_email_enabled" value="on" {{if .AppEmailEnabled}}checked{{end}}>
|
||||
Alkalmazás-email engedélyezése
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="app_email_from_name">Feladó neve (opcionális)</label>
|
||||
<input type="text" id="app_email_from_name" name="app_email_from_name"
|
||||
value="{{.AppEmailFromName}}" placeholder="pl. a háztartás neve" class="form-control">
|
||||
<span class="form-hint">Ez jelenik meg a kimenő emailek feladójaként az alkalmazás neve mellett.</span>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Mentés</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="settings-card-desc" style="margin-top:.75rem">
|
||||
Bekapcsolás után az egyes alkalmazásoknál is engedélyezni kell az email-küldést (az alkalmazás oldalán).
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Section: Recovery Info -->
|
||||
{{if .RetrievalPassword}}
|
||||
<div class="settings-card">
|
||||
|
||||
@@ -93,6 +93,31 @@ func (s *Server) tier2ConfigSaveHandler(w http.ResponseWriter, r *http.Request,
|
||||
s.redirectTier2(w, r, name, "A 2. mentés beállítása elmentve.", "")
|
||||
}
|
||||
|
||||
// appEmailToggleHandler flips the per-app email toggle (only for apps with an smtp_mapping)
|
||||
// and recreates the stack so the SMTP env injection takes effect. Redirects back to the
|
||||
// app's config page with a flash.
|
||||
func (s *Server) appEmailToggleHandler(w http.ResponseWriter, r *http.Request, name string) {
|
||||
if _, ok := s.stackMgr.GetStack(name); !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
enabled := r.FormValue("app_email_enabled") == "on" || r.FormValue("app_email_enabled") == "true"
|
||||
|
||||
dest := "/stacks/" + url.PathEscape(name) + "/deploy"
|
||||
if err := s.stackMgr.SetAppEmailEnabled(name, enabled); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] app-email toggle for %s: %v", name, err)
|
||||
http.Redirect(w, r, dest+"?flash_error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] App-email for %s set to %v", name, enabled)
|
||||
msg := "Email-küldés kikapcsolva ennél az alkalmazásnál."
|
||||
if enabled {
|
||||
msg = "Email-küldés bekapcsolva ennél az alkalmazásnál."
|
||||
}
|
||||
http.Redirect(w, r, dest+"?flash="+url.QueryEscape(msg), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// redirectTier2 sends the customer back to the panel with a flash message.
|
||||
func (s *Server) redirectTier2(w http.ResponseWriter, r *http.Request, name, flash, flashErr string) {
|
||||
dest := "/stacks/" + url.PathEscape(name) + "/backup"
|
||||
|
||||
Reference in New Issue
Block a user