v0.89.0: app-email plaintext-only listener (:2526) + split-From mapping
Gap 1: third shim listener :2526, plaintext, does NOT advertise STARTTLS (TLSConfig nil) — for opportunistic-STARTTLS clients with no cert-skip (cal.com, nextcloud). Gap 2: SMTPMapping tls_mode (picks port 2525/2526/2465) + from_domain_var (split local-part + domain for nextcloud's MAIL_FROM_ADDRESS/MAIL_DOMAIN). Default keeps existing apps on 2525. Hub untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,22 +39,25 @@ const forwardTimeout = 30 * time.Second
|
||||
|
||||
// Options configures the shim.
|
||||
type Options struct {
|
||||
PlainAddr string // plaintext + STARTTLS listener (default ":2525")
|
||||
TLSAddr string // implicit-TLS listener (default ":2465")
|
||||
ServiceName string // CN/SAN of the self-signed cert + SMTP greeting (e.g. "felhom-controller")
|
||||
Policy *Policy // From-domain allowlist (required)
|
||||
Forwarder Forwarder // hub forwarder (required)
|
||||
Logger *log.Logger
|
||||
PlainAddr string // plaintext + STARTTLS listener (default ":2525")
|
||||
TLSAddr string // implicit-TLS listener (default ":2465")
|
||||
PlainNoTLSAddr string // plaintext-only listener, STARTTLS NOT advertised (default ":2526")
|
||||
ServiceName string // CN/SAN of the self-signed cert + SMTP greeting (e.g. "felhom-controller")
|
||||
Policy *Policy // From-domain allowlist (required)
|
||||
Forwarder Forwarder // hub forwarder (required)
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
// Server runs the two SMTP listeners.
|
||||
// Server runs the three SMTP listeners.
|
||||
type Server struct {
|
||||
opts Options
|
||||
tlsConf *tls.Config
|
||||
plainSrv *smtp.Server
|
||||
tlsSrv *smtp.Server
|
||||
plainLn net.Listener
|
||||
tlsLn net.Listener
|
||||
opts Options
|
||||
tlsConf *tls.Config
|
||||
plainSrv *smtp.Server
|
||||
tlsSrv *smtp.Server
|
||||
plainNoTLSSrv *smtp.Server
|
||||
plainLn net.Listener
|
||||
tlsLn net.Listener
|
||||
plainNoTLSLn net.Listener
|
||||
}
|
||||
|
||||
// New builds the shim and its self-signed cert. It does not bind sockets — call Start.
|
||||
@@ -71,6 +74,9 @@ func New(opts Options) (*Server, error) {
|
||||
if opts.TLSAddr == "" {
|
||||
opts.TLSAddr = ":2465"
|
||||
}
|
||||
if opts.PlainNoTLSAddr == "" {
|
||||
opts.PlainNoTLSAddr = ":2526"
|
||||
}
|
||||
if opts.ServiceName == "" {
|
||||
opts.ServiceName = "felhom-controller"
|
||||
}
|
||||
@@ -107,6 +113,19 @@ func New(opts Options) (*Server, error) {
|
||||
s.tlsSrv.ReadTimeout = 60 * time.Second
|
||||
s.tlsSrv.WriteTimeout = 60 * time.Second
|
||||
|
||||
// :2526 — plaintext, STARTTLS NOT advertised (TLSConfig stays nil ⇒ go-smtp omits the
|
||||
// STARTTLS capability from EHLO). For clients that opportunistically upgrade to STARTTLS
|
||||
// whenever it's offered AND then validate the cert with no skip-verify knob (cal.com /
|
||||
// Nodemailer, nextcloud / Symfony Mailer): with no offer, they never attempt TLS. Accepted
|
||||
// posture: plaintext on the single-tenant app Docker bridge only (never host/internet).
|
||||
s.plainNoTLSSrv = smtp.NewServer(be)
|
||||
s.plainNoTLSSrv.Addr = opts.PlainNoTLSAddr
|
||||
s.plainNoTLSSrv.Domain = opts.ServiceName
|
||||
s.plainNoTLSSrv.AllowInsecureAuth = true
|
||||
s.plainNoTLSSrv.MaxMessageBytes = maxMessageBytes
|
||||
s.plainNoTLSSrv.ReadTimeout = 60 * time.Second
|
||||
s.plainNoTLSSrv.WriteTimeout = 60 * time.Second
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -123,10 +142,17 @@ func (s *Server) Start() error {
|
||||
pl.Close()
|
||||
return fmt.Errorf("mailrelay: listen TLS %s: %w", s.opts.TLSAddr, err)
|
||||
}
|
||||
s.plainLn, s.tlsLn = pl, tl
|
||||
pn, err := net.Listen("tcp", s.opts.PlainNoTLSAddr)
|
||||
if err != nil {
|
||||
pl.Close()
|
||||
tl.Close()
|
||||
return fmt.Errorf("mailrelay: listen %s: %w", s.opts.PlainNoTLSAddr, err)
|
||||
}
|
||||
s.plainLn, s.tlsLn, s.plainNoTLSLn = pl, tl, pn
|
||||
|
||||
s.opts.Logger.Printf("[INFO] [mailrelay] plaintext+STARTTLS listener on %s", pl.Addr())
|
||||
s.opts.Logger.Printf("[INFO] [mailrelay] implicit-TLS listener on %s", tl.Addr())
|
||||
s.opts.Logger.Printf("[INFO] [mailrelay] plaintext-only (no STARTTLS) listener on %s", pn.Addr())
|
||||
|
||||
go func() {
|
||||
if err := s.plainSrv.Serve(pl); err != nil && !isClosedErr(err) {
|
||||
@@ -138,6 +164,11 @@ func (s *Server) Start() error {
|
||||
s.opts.Logger.Printf("[ERROR] [mailrelay] implicit-TLS listener stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if err := s.plainNoTLSSrv.Serve(pn); err != nil && !isClosedErr(err) {
|
||||
s.opts.Logger.Printf("[ERROR] [mailrelay] plaintext-only listener stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -157,7 +188,15 @@ func (s *Server) TLSAddr() string {
|
||||
return s.opts.TLSAddr
|
||||
}
|
||||
|
||||
// Close stops both listeners.
|
||||
// PlainNoTLSAddr returns the bound plaintext-only (no-STARTTLS) address.
|
||||
func (s *Server) PlainNoTLSAddr() string {
|
||||
if s.plainNoTLSLn != nil {
|
||||
return s.plainNoTLSLn.Addr().String()
|
||||
}
|
||||
return s.opts.PlainNoTLSAddr
|
||||
}
|
||||
|
||||
// Close stops all listeners.
|
||||
func (s *Server) Close() error {
|
||||
var err error
|
||||
if s.plainSrv != nil {
|
||||
@@ -170,6 +209,11 @@ func (s *Server) Close() error {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
if s.plainNoTLSSrv != nil {
|
||||
if e := s.plainNoTLSSrv.Close(); e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user