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:
@@ -0,0 +1,79 @@
|
||||
package mailrelay
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Lifecycle owns the shim's start/stop so the global app-email toggle can take effect
|
||||
// at runtime without a controller restart. The shim listens ONLY when app-email is
|
||||
// globally ON (Part 1: "start listeners only when hub.enabled and global app-email ON").
|
||||
// Apply is idempotent: enabling when already up (or disabling when already down) is a
|
||||
// no-op.
|
||||
type Lifecycle struct {
|
||||
mu sync.Mutex
|
||||
opts Options
|
||||
srv *Server
|
||||
logger *log.Logger
|
||||
stopped bool // Close was called; further Apply(true) is refused (shutdown)
|
||||
}
|
||||
|
||||
// NewLifecycle prepares (but does not start) a shim from the given options.
|
||||
func NewLifecycle(opts Options) *Lifecycle {
|
||||
lg := opts.Logger
|
||||
if lg == nil {
|
||||
lg = log.Default()
|
||||
}
|
||||
return &Lifecycle{opts: opts, logger: lg}
|
||||
}
|
||||
|
||||
// Apply reconciles the shim's running state with the desired toggle value. It returns
|
||||
// an error only if a requested START failed (a stop never errors meaningfully).
|
||||
func (l *Lifecycle) Apply(enabled bool) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.stopped {
|
||||
return nil
|
||||
}
|
||||
if enabled {
|
||||
if l.srv != nil {
|
||||
return nil // already running
|
||||
}
|
||||
srv, err := New(l.opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := srv.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
l.srv = srv
|
||||
l.logger.Printf("[INFO] [mailrelay] app-email shim ON (host=%s plain=%s tls=%s)",
|
||||
l.opts.ServiceName, srv.PlainAddr(), srv.TLSAddr())
|
||||
return nil
|
||||
}
|
||||
// disabled → stop if running
|
||||
if l.srv != nil {
|
||||
_ = l.srv.Close()
|
||||
l.srv = nil
|
||||
l.logger.Printf("[INFO] [mailrelay] app-email shim OFF")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Running reports whether the shim is currently listening.
|
||||
func (l *Lifecycle) Running() bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.srv != nil
|
||||
}
|
||||
|
||||
// Close stops the shim permanently (shutdown). Idempotent.
|
||||
func (l *Lifecycle) Close() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.stopped = true
|
||||
if l.srv != nil {
|
||||
_ = l.srv.Close()
|
||||
l.srv = nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user