MFA fix, added IP blacklist, update webmail client

This commit is contained in:
2026-08-14 13:04:55 +01:00
parent 6063f95504
commit 892f366a16
122 changed files with 13362 additions and 251 deletions
+77 -4
View File
@@ -6,6 +6,7 @@ import (
"html/template"
"io/fs"
"net/http"
"net/netip"
"time"
"gopkg.in/ini.v1"
@@ -13,6 +14,7 @@ import (
"mailgoserver/internal/db"
"mailgoserver/internal/dkim"
"mailgoserver/internal/mailstore"
"mailgoserver/internal/relay"
"mailgoserver/internal/toolbox"
)
@@ -25,18 +27,44 @@ type App struct {
DKIM *dkim.Manager
Mailstore *mailstore.Store
ACME *acmecert.Manager
Relay *relay.Relay // used by the webmail client's compose/send (see webmail_compose.go)
Cfg *ini.File
ConfigPath string
Logger *toolbox.Logger
SMTPUp func() bool // reports whether the SMTP listeners are currently running
// pgpKeys caches unlocked PGP identities for the rest of a login session — purely
// in-memory server state, not an external dependency, so it's built internally
// rather than threaded in as a New(...) parameter. S/MIME has no equivalent cache
// since its private keys are stored plain, not passphrase-protected.
pgpKeys *pgpKeyCache
// trustedProxies gates requestIP's use of forwarded headers — see trusted_proxy.go.
trustedProxies []netip.Prefix
// loginLimiter throttles login POSTs per source IP — see ratelimit.go. Separate
// from the per-account lockout (login.go/webmail_login.go), which uses
// esrv_auth_logs via CountRecentFailedAttempts instead of in-memory state.
loginLimiter *ipRateLimiter
// appSecret signs CSRF tokens — see csrf.go and LoadOrCreateAppSecret (secret.go).
appSecret []byte
templates map[string]*template.Template
}
// New builds the web UI. Templates and static assets come from the embedded
// filesystem (embed.go), not disk, so no directory paths are needed for them.
func New(database *db.DB, dkimMgr *dkim.Manager, mstore *mailstore.Store, acmeMgr *acmecert.Manager, cfg *ini.File, configPath string, logger *toolbox.Logger, smtpUp func() bool) (*App, error) {
a := &App{DB: database, DKIM: dkimMgr, Mailstore: mstore, ACME: acmeMgr, Cfg: cfg, ConfigPath: configPath, Logger: logger, SMTPUp: smtpUp}
// appSecret is loaded by the caller via LoadOrCreateAppSecret, mirroring how
// mailstore's master key is loaded in main.go and threaded in rather than resolved
// internally (both are file paths relative to the app's root working directory,
// which this package doesn't otherwise know).
func New(database *db.DB, dkimMgr *dkim.Manager, mstore *mailstore.Store, acmeMgr *acmecert.Manager, relayer *relay.Relay, cfg *ini.File, configPath string, logger *toolbox.Logger, smtpUp func() bool, appSecret []byte) (*App, error) {
trustedProxies := parseTrustedProxies(cfg.Section("Server").Key("trusted_proxies").MustString(""), logger)
a := &App{
DB: database, DKIM: dkimMgr, Mailstore: mstore, ACME: acmeMgr, Relay: relayer, Cfg: cfg, ConfigPath: configPath, Logger: logger, SMTPUp: smtpUp,
pgpKeys: newPGPKeyCache(), trustedProxies: trustedProxies, loginLimiter: newIPRateLimiter(20, time.Minute), appSecret: appSecret,
}
if err := a.loadTemplates(); err != nil {
return nil, err
}
@@ -82,6 +110,11 @@ func (a *App) Mux() *http.ServeMux {
panic(err) // embed.go's directive is malformed if this ever fails
}
outer.Handle("GET "+Prefix+"/static/", http.StripPrefix(Prefix+"/static/", http.FileServerFS(staticFS)))
// Webmail templates reference the same vendored assets (Bootstrap/Quill/etc. —
// see static/vendor/) but live under a different URL prefix, so they need their
// own route to the identical embedded files rather than reaching across into the
// admin prefix.
outer.Handle("GET "+MailboxPrefix+"/static/", http.StripPrefix(MailboxPrefix+"/static/", http.FileServerFS(staticFS)))
outer.HandleFunc("GET "+Prefix+"/login", a.loginForm)
outer.HandleFunc("POST "+Prefix+"/login", a.loginSubmit)
@@ -102,7 +135,8 @@ func (a *App) Mux() *http.ServeMux {
outer.HandleFunc("POST "+MailboxPrefix+"/logout", a.webmailLogout)
webmailMux := http.NewServeMux()
webmailMux.HandleFunc("GET "+MailboxPrefix+"/", a.webmailDashboard)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/", a.webmailMailRoot)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/account", a.webmailDashboard)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mfa-setup", a.webmailMFASetupRequiredPage)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/password", a.webmailChangePassword)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/totp/setup", a.webmailTOTPSetupBegin)
@@ -113,6 +147,39 @@ func (a *App) Mux() *http.ServeMux {
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/passkey/{id}/remove", a.webmailPasskeyRemove)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/apppasswords/add", a.webmailAddAppPassword)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/apppasswords/{pw_id}/revoke", a.webmailRevokeAppPassword)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail", a.webmailMailRoot)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/compose", a.webmailComposeForm)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/compose", a.webmailComposeSend)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/save-draft", a.webmailComposeSaveDraft)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/search", a.webmailSearch)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/recipients", a.webmailRecipientSuggest)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}", a.webmailFolderView)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}/{uid}", a.webmailMessageView)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/delete", a.webmailMessageDelete)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/move", a.webmailMessageMove)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}/{uid}/attachment/{idx}", a.webmailAttachmentDownload)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/add", a.webmailAddFolder)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/{name}/remove", a.webmailDeleteFolder)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/rules", a.webmailRulesList)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/rules/add", a.webmailAddRule)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/rules/{rule_id}/remove", a.webmailRemoveRule)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/certs", a.webmailCertsPage)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/identity/generate", a.webmailSMIMEGenerate)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/identity/import", a.webmailSMIMEImport)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/identity/{identity_id}/remove", a.webmailSMIMERemoveIdentity)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/smime/identity/{identity_id}/download", a.webmailSMIMEDownloadCert)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/contacts/add", a.webmailSMIMEAddContact)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/smime/contacts/{contact_id}/remove", a.webmailSMIMERemoveContact)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/identity/generate", a.webmailPGPGenerate)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/identity/import", a.webmailPGPImport)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/identity/{identity_id}/remove", a.webmailPGPRemoveIdentity)
webmailMux.HandleFunc("GET "+MailboxPrefix+"/pgp/identity/{identity_id}/download", a.webmailPGPDownloadKey)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/unlock", a.webmailPGPUnlock)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/contacts/add", a.webmailPGPAddContact)
webmailMux.HandleFunc("POST "+MailboxPrefix+"/pgp/contacts/{contact_id}/remove", a.webmailPGPRemoveContact)
outer.Handle(MailboxPrefix+"/", a.requireMailboxAuth(webmailMux))
mux := http.NewServeMux()
@@ -188,6 +255,13 @@ func (a *App) Mux() *http.ServeMux {
mux.HandleFunc("GET "+Prefix+"/ips/{id}/edit", a.editIPForm)
mux.HandleFunc("POST "+Prefix+"/ips/{id}/edit", a.editIP)
mux.HandleFunc("GET "+Prefix+"/blacklist", a.requireGlobalAdmin(a.blacklistPage))
mux.HandleFunc("POST "+Prefix+"/blacklist/add", a.requireGlobalAdmin(a.addBlacklistEntry))
mux.HandleFunc("POST "+Prefix+"/blacklist/{id}/remove", a.requireGlobalAdmin(a.removeBlacklistEntry))
mux.HandleFunc("POST "+Prefix+"/blacklist/{id}/whitelist", a.requireGlobalAdmin(a.whitelistBlacklistedIP))
mux.HandleFunc("POST "+Prefix+"/abuse-whitelist/add", a.requireGlobalAdmin(a.addAbuseWhitelistEntry))
mux.HandleFunc("POST "+Prefix+"/abuse-whitelist/{id}/remove", a.requireGlobalAdmin(a.removeAbuseWhitelistEntry))
mux.HandleFunc("GET "+Prefix+"/dkim", a.dkimList)
mux.HandleFunc("POST "+Prefix+"/dkim/create", a.createDKIM)
mux.HandleFunc("POST "+Prefix+"/dkim/{id}/regenerate", a.regenerateDKIM)
@@ -217,7 +291,6 @@ func (a *App) Mux() *http.ServeMux {
mux.HandleFunc("GET "+Prefix+"/msg/content/{id}", a.viewMessageContent)
mux.HandleFunc("GET "+Prefix+"/msg/attachment/{id}/download", a.downloadAttachment)
mux.HandleFunc("GET "+Prefix+"/msg/attachment/{id}/delete", a.deleteAttachment)
mux.HandleFunc("POST "+Prefix+"/msg/attachment/{id}/delete", a.deleteAttachment)
mux.HandleFunc("GET "+Prefix, func(w http.ResponseWriter, r *http.Request) {