mail/cmd/inbound/inbound.go

144 lines
3 KiB
Go

package main
import (
"context"
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/emersion/go-milter"
"github.com/emersion/go-smtp"
"go.uber.org/zap"
"golang.org/x/time/rate"
localtls "git.cycore.io/cycore/mail/pkg/tls"
"git.cycore.io/cycore/mail/server"
)
var (
listenPort int
healthPort int
enableTLS bool
)
var allowedDomains []string
func init() {
flag.IntVar(&listenPort, "smtp", 2525, "port on which to listen for incoming emails")
flag.IntVar(&listenPort, "smtps", 2526, "port on which to listen for incoming secure emails")
flag.IntVar(&healthPort, "health", 8080, "port on which to listen for health checks")
}
func main() {
var err error
var log *zap.Logger
flag.Parse()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
if _, ok := os.LookupEnv("DEBUG"); ok {
log, err = zap.NewDevelopment()
} else {
log, err = zap.NewProduction()
}
if err != nil {
panic("failed to instantiate logger: " + err.Error())
}
if v := os.Getenv("ALLOWED_DOMAINS"); v != "" {
if err := json.Unmarshal([]byte(v), &allowedDomains); err != nil {
log.Fatal("failed to read ALLOWED_DOMAINS from environment", zap.Error(err))
}
}
if len(allowedDomains) < 1 {
log.Fatal("no allowed domains")
}
var cm *localtls.CertManager
if tlsCertFile, ok := os.LookupEnv("TLS_CERT_FILE"); ok {
enableTLS = true
cm, err = localtls.NewCertManager(tlsCertFile, os.Getenv("TLS_KEY_FILE"))
if err != nil {
log.Fatal("failed to create certificate manager", zap.Error(err))
}
go func() {
log.Info("starting certificate manager")
if err := cm.Watch(ctx, log); err != nil {
log.Error("certificate manager exited", zap.Error(err))
cancel()
}
}()
}
be := &server.Backend{
AllowedDomains: allowedDomains,
Log: log,
LMTPAddress: os.Getenv("LMTP_ADDRESS"),
RSpam: milter.NewDefaultClient("tcp", "rspamd:11334"),
OverallLimiter: rate.NewLimiter(rate.Limit(10), 20),
}
log.Info("starting mail service")
s := smtp.NewServer(be)
defer s.Close() //nolint:errcheck
s.Addr = ":2525"
s.Domain = "cycore.io"
s.Debug = os.Stderr
s.WriteTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = 300 * 1024 * 1024 // 300 MiB
s.MaxRecipients = 10
s.AuthDisabled = true
log.Info("starting health service")
go runHealthService(cm)
if enableTLS {
s.TLSConfig = &tls.Config{
GetCertificate: cm.Get,
}
go func() {
defer cancel()
if err := s.ListenAndServeTLS(); err != nil {
log.Error("TLS listener failed", zap.Error(err))
}
}()
}
log.Fatal("server exited", zap.Error(s.ListenAndServe()))
}
func runHealthService(cm *localtls.CertManager) {
http.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) {
if cm != nil && !cm.Ready() {
http.Error(w, "no TLS cert", http.StatusInternalServerError)
return
}
w.Write([]byte("OK")) //nolint:errcheck
})
http.ListenAndServe(fmt.Sprintf(":%d", healthPort), nil) //nolint:errcheck
}