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 } }