updated layout for webmail and added http dns letsencrypt

This commit is contained in:
2026-08-15 12:35:44 +01:00
parent 310700407e
commit f283c90f11
49 changed files with 3359 additions and 431 deletions
+81 -29
View File
@@ -109,25 +109,74 @@ func main() {
os.Exit(1)
}
mstore := mailstore.New(database, masterKey, mailstoreBase)
relayer.Mailstore = mstore
// Shared by both the SMTP and IMAP implicit-TLS listeners, so a single Reload()
// call (self-signed regeneration today; a Let's Encrypt renewal later) updates
// both without restarting the process.
certFile := absPath(root, cfg.Section("TLS").Key("TLS_CERT_FILE").String())
keyFile := absPath(root, cfg.Section("TLS").Key("TLS_KEY_FILE").String())
if err := tlsutil.GenerateSelfSignedCert(certFile, keyFile); err != nil {
logger.Error("generate TLS certificate: %v", err)
os.Exit(1)
// Three independent certificate "slots" — custom (self-signed by default, or your
// own uploaded cert/key), letsencrypt_dns, letsencrypt_http — each with its own
// file pair and CertReloader. [TLS]'s smtp_tls_cert/imap_tls_cert/web_https_cert
// independently pick which slot each listener uses, e.g. an HTTP-01 cert for mail
// while a DNS-01 cert serves the dashboard. Every slot is seeded with a self-signed
// cert if missing, so a reloader can always be constructed even before any ACME
// manager has obtained anything yet.
customCertFile := absPath(root, cfg.Section("TLS").Key("TLS_CERT_FILE").String())
customKeyFile := absPath(root, cfg.Section("TLS").Key("TLS_KEY_FILE").String())
dnsCertFile := absPath(root, "ssl_certs/letsencrypt_dns.crt")
dnsKeyFile := absPath(root, "ssl_certs/letsencrypt_dns.key")
httpCertFile := absPath(root, "ssl_certs/letsencrypt_http.crt")
httpKeyFile := absPath(root, "ssl_certs/letsencrypt_http.key")
for _, pair := range [][2]string{
{customCertFile, customKeyFile}, {dnsCertFile, dnsKeyFile}, {httpCertFile, httpKeyFile},
} {
if err := tlsutil.GenerateSelfSignedCert(pair[0], pair[1]); err != nil {
logger.Error("generate TLS certificate: %v", err)
os.Exit(1)
}
}
certReloader, err := tlsutil.NewCertReloader(certFile, keyFile)
customReloader, err := tlsutil.NewCertReloader(customCertFile, customKeyFile)
if err != nil {
logger.Error("load TLS certificate: %v", err)
os.Exit(1)
}
tlsConfig := tlsutil.NewReloadableTLSConfig(certReloader)
dnsReloader, err := tlsutil.NewCertReloader(dnsCertFile, dnsKeyFile)
if err != nil {
logger.Error("load TLS certificate: %v", err)
os.Exit(1)
}
httpReloader, err := tlsutil.NewCertReloader(httpCertFile, httpKeyFile)
if err != nil {
logger.Error("load TLS certificate: %v", err)
os.Exit(1)
}
certSlots := map[string]*tlsutil.CertReloader{
"custom": customReloader, "letsencrypt_dns": dnsReloader, "letsencrypt_http": httpReloader,
}
resolveCertSlot := func(key string) *tlsutil.CertReloader {
if r, ok := certSlots[cfg.Section("TLS").Key(key).MustString("custom")]; ok {
return r
}
return customReloader
}
smtpTLSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("smtp_tls_cert"))
imapTLSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("imap_tls_cert"))
webHTTPSConfig := tlsutil.NewReloadableTLSConfig(resolveCertSlot("web_https_cert"))
acmeDataDir := absPath(root, "server_data/acme")
acmeMgr := acmecert.New(cfg, certFile, keyFile, acmeDataDir, certReloader, toolbox.GetLogger("acme"))
acmeMgr := acmecert.New(cfg, "LetsEncrypt", "dns-01", dnsCertFile, dnsKeyFile, acmeDataDir, dnsReloader, toolbox.GetLogger("acme"))
acmeHTTPMgr := acmecert.New(cfg, "LetsEncryptHTTP", "http-01", httpCertFile, httpKeyFile, acmeDataDir, httpReloader, toolbox.GetLogger("acme-http"))
// The HTTP-01 challenge responder is long-lived (unlike lego's own ephemeral
// per-obtain listener) so an operator behind NAT/a reverse proxy can verify their
// port-forwarding actually reaches this host before/without triggering a real,
// rate-limited ACME attempt — curling it should return 200 once this is up.
// Started once at boot if enabled; toggling [LetsEncryptHTTP] enabled needs a
// restart to start or stop this listener, same as the *_cert routing settings.
if cfg.Section("LetsEncryptHTTP").Key("enabled").MustBool(false) {
httpChallengeServer := acmecert.NewHTTP01Server()
httpPort := cfg.Section("Server").Key("HTTP_LETSENCRYPT_PORT").MustString("80")
httpChallengeServer.Start(fmt.Sprintf(":%s", httpPort), toolbox.GetLogger("acme-http"))
acmeHTTPMgr.HTTP01Server = httpChallengeServer
logger.Info("HTTP-01 challenge responder listening on :%s", httpPort)
}
backend := &smtpserver.Backend{
DB: database, DKIM: dkimMgr, Relay: relayer, Cfg: cfg, Mailstore: mstore,
@@ -147,7 +196,7 @@ func main() {
// setting) — a bare ":port" address binds dual-stack on most systems,
// which would accept IPv6 connections the Python version never did.
plainServer := smtpserver.NewPlainServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpPort), banner)
tlsServer := smtpserver.NewTLSServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpTLSPort), banner, tlsConfig)
tlsServer := smtpserver.NewTLSServer(backend, fmt.Sprintf("0.0.0.0:%d", smtpTLSPort), banner, smtpTLSConfig)
smtpRunning.Store(true)
logger.Info("Plain SMTP listening on :%d, direct-TLS SMTP listening on :%d", smtpPort, smtpTLSPort)
@@ -164,7 +213,7 @@ func main() {
logger.Error("plain SMTP server: %v", err)
}
}()
tlsListener, err := tls.Listen("tcp", tlsServer.Addr, tlsConfig)
tlsListener, err := tls.Listen("tcp", tlsServer.Addr, smtpTLSConfig)
if err != nil {
logger.Error("TLS SMTP listen: %v", err)
return
@@ -179,7 +228,7 @@ func main() {
imapTLSPort := cfg.Section("IMAP").Key("IMAP_TLS_PORT").MustInt(993)
plainServer := imapserver.NewPlainServer(imapBackend)
tlsServer := imapserver.NewTLSServer(imapBackend, tlsConfig)
tlsServer := imapserver.NewTLSServer(imapBackend, imapTLSConfig)
logger.Info("Plain IMAP listening on :%d, direct-TLS IMAP listening on :%d", imapPort, imapTLSPort)
@@ -195,7 +244,7 @@ func main() {
logger.Error("plain IMAP server: %v", err)
}
}()
imapTLSListener, err := tls.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", imapTLSPort), tlsConfig)
imapTLSListener, err := tls.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", imapTLSPort), imapTLSConfig)
if err != nil {
logger.Error("TLS IMAP listen: %v", err)
return
@@ -207,25 +256,28 @@ func main() {
// runCertRenewal is the first periodic/background job in this codebase — everything
// else is purely request-driven. Checks soon after boot (so enabling Let's Encrypt
// and restarting converges quickly) and every 12h thereafter. The first check each
// process run always attempts ObtainOrRenew regardless of NeedsRenewal's expiry
// check, since a fresh self-signed cert has ~1 year left and would otherwise never
// get replaced by the very first real certificate.
// and restarting converges quickly) and every 12h thereafter. NeedsRenewal is a pure
// disk-state check (it reads the cert file itself, including recognizing the
// self-signed placeholder by its issuer) — deliberately not gated on any in-memory
// "has this process attempted yet" flag, so restarting an already-working setup
// never triggers a redundant re-obtain of an already-valid real certificate.
runCertRenewal := func() {
time.Sleep(1 * time.Minute)
checkAndRenew := func() {
if !cfg.Section("LetsEncrypt").Key("enabled").MustBool(false) {
checkAndRenewOne := func(mgr *acmecert.Manager) {
if !mgr.Enabled() {
return
}
if !acmeMgr.Status().LastAttempt.IsZero() {
if needs, err := acmeMgr.NeedsRenewal(); err != nil || !needs {
return
}
if needs, err := mgr.NeedsRenewal(); err != nil || !needs {
return
}
if err := acmeMgr.ObtainOrRenew(context.Background()); err != nil {
logger.Error("ACME obtain/renew: %v", err)
if err := mgr.ObtainOrRenew(context.Background()); err != nil {
logger.Error("ACME obtain/renew (%s): %v", mgr.Section, err)
}
}
checkAndRenew := func() {
checkAndRenewOne(acmeMgr)
checkAndRenewOne(acmeHTTPMgr)
}
checkAndRenew()
ticker := time.NewTicker(12 * time.Hour)
defer ticker.Stop()
@@ -248,7 +300,7 @@ func main() {
os.Exit(1)
}
app, err := webui.New(database, dkimMgr, mstore, acmeMgr, relayer, cfg, configPath, toolbox.GetLogger("web"), smtpRunning.Load, appSecret)
app, err := webui.New(database, dkimMgr, mstore, acmeMgr, acmeHTTPMgr, relayer, cfg, configPath, toolbox.GetLogger("web"), smtpRunning.Load, appSecret)
if err != nil {
logger.Error("init web UI: %v", err)
os.Exit(1)
@@ -280,7 +332,7 @@ func main() {
httpsPort := cfg.Section("Server").Key("WEB_HTTPS_PORT").MustInt(5001)
httpsAddr := fmt.Sprintf("%s:%d", *host, httpsPort)
httpsServer := &http.Server{Addr: httpsAddr, Handler: handler, TLSConfig: tlsConfig}
httpsServer := &http.Server{Addr: httpsAddr, Handler: handler, TLSConfig: webHTTPSConfig}
go func() {
logger.Info("Web interface (HTTPS) starting at https://%s", httpsAddr)
// Empty cert/key paths: TLSConfig.GetCertificate (backed by certReloader) supplies