// Package webui is the admin web interface, mirroring email_server/server_web_ui/.
// It's mounted at /pymta-manager, matching the Flask blueprint's url_prefix exactly.
package webui

import (
	"html/template"
	"io/fs"
	"net/http"
	"net/netip"
	"time"

	"gopkg.in/ini.v1"
	"mailgoserver/internal/acmecert"
	"mailgoserver/internal/db"
	"mailgoserver/internal/dkim"
	"mailgoserver/internal/mailstore"
	"mailgoserver/internal/relay"
	"mailgoserver/internal/toolbox"
)

const Prefix = "/pymta-manager"

// App holds every dependency the web routes need, mirroring the module-level
// singletons (Session, DKIMManager, settings) that server_web_ui/*.py imports.
type App struct {
	DB         *db.DB
	DKIM       *dkim.Manager
	Mailstore  *mailstore.Store
	ACME       *acmecert.Manager // DNS-01
	ACMEHTTP   *acmecert.Manager // HTTP-01
	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.
// 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, acmeHTTPMgr *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, ACMEHTTP: acmeHTTPMgr, 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
	}
	return a, nil
}

type healthStatus struct {
	Status    string
	Timestamp string
	Services  map[string]string
}

// checkHealth mirrors app.py's SMTPServerApp.check_health.
func (a *App) checkHealth() healthStatus {
	dbStatus := "ok"
	if err := a.DB.Ping(); err != nil {
		dbStatus = "error"
	}
	smtpStatus := "stopped"
	if a.SMTPUp != nil && a.SMTPUp() {
		smtpStatus = "running"
	}
	overall := "healthy"
	if smtpStatus == "stopped" || dbStatus == "error" {
		overall = "degraded"
	}
	return healthStatus{
		Status:    overall,
		Timestamp: time.Now().Format(time.RFC3339),
		Services:  map[string]string{"smtp_server": smtpStatus, "web_frontend": "running", "database": dbStatus},
	}
}

// Mux builds the *http.ServeMux for the whole admin UI, mirroring routes.py's
// blueprint registration plus every route file's own routes. Everything except
// login/MFA and static assets requires a valid, fully-verified session — see
// requireAuth in auth.go.
func (a *App) Mux() *http.ServeMux {
	outer := http.NewServeMux()

	staticFS, err := fs.Sub(assets, "static")
	if err != nil {
		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)
	outer.HandleFunc("GET "+Prefix+"/login/mfa", a.mfaForm)
	outer.HandleFunc("POST "+Prefix+"/login/mfa", a.mfaSubmit)
	outer.HandleFunc("GET "+Prefix+"/login/passkey/begin", a.passkeyLoginBegin)
	outer.HandleFunc("POST "+Prefix+"/login/passkey/finish", a.passkeyLoginFinish)
	outer.HandleFunc("POST "+Prefix+"/logout", a.logout)

	// Self-service webmail portal — entirely separate prefix, session cookie, and
	// context keys from the admin dashboard above (see webmail_auth.go).
	outer.HandleFunc("GET "+MailboxPrefix+"/login", a.webmailLoginForm)
	outer.HandleFunc("POST "+MailboxPrefix+"/login", a.webmailLoginSubmit)
	outer.HandleFunc("GET "+MailboxPrefix+"/login/mfa", a.webmailMFAForm)
	outer.HandleFunc("POST "+MailboxPrefix+"/login/mfa", a.webmailMFASubmit)
	outer.HandleFunc("GET "+MailboxPrefix+"/login/passkey/begin", a.webmailPasskeyLoginBegin)
	outer.HandleFunc("POST "+MailboxPrefix+"/login/passkey/finish", a.webmailPasskeyLoginFinish)
	outer.HandleFunc("POST "+MailboxPrefix+"/logout", a.webmailLogout)

	webmailMux := http.NewServeMux()
	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/preferences", a.webmailSetGroupMessages)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/remote-images-mode", a.webmailSetRemoteImagesMode)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/trusted-senders/add", a.webmailAddTrustedImageSender)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/trusted-senders/{id}/remove", a.webmailRemoveTrustedImageSender)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/rebuild-cache", a.webmailRebuildMessageCache)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/totp/setup", a.webmailTOTPSetupBegin)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/totp/confirm", a.webmailTOTPSetupConfirm)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/totp/disable", a.webmailTOTPDisable)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/passkey/begin", a.webmailPasskeyRegisterBegin)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/account/passkey/finish", a.webmailPasskeyRegisterFinish)
	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("GET "+MailboxPrefix+"/mail/{folder}/{uid}/pane", a.webmailMessagePane)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/delete", a.webmailMessageDelete)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/move", a.webmailMessageMove)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/star", a.webmailToggleStar)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/restore", a.webmailRestoreMessage)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/mark-junk", a.webmailMarkAsJunk)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/{uid}/always-allow-images", a.webmailAlwaysAllowImages)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/bulk", a.webmailBulkAction)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/mark-all-read", a.webmailMarkAllRead)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/empty", a.webmailEmptyTrash)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/{folder}/clean-spam", a.webmailCleanSpam)
	webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}/{uid}/attachment/{idx}", a.webmailAttachmentDownload)
	webmailMux.HandleFunc("GET "+MailboxPrefix+"/mail/{folder}/{uid}/attachments.zip", a.webmailDownloadAllAttachments)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/add", a.webmailAddFolder)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/{name}/remove", a.webmailDeleteFolder)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/{name}/rename", a.webmailRenameFolder)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/{name}/delete-permanently", a.webmailDeleteFolderPermanently)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/{name}/restore", a.webmailRestoreFolder)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/mail/folders/order", a.webmailSetFolderOrder)

	webmailMux.HandleFunc("GET "+MailboxPrefix+"/rules", a.webmailRulesList)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/rules/save", a.webmailSaveRule)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/rules/{rule_id}/toggle", a.webmailToggleRule)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/rules/{rule_id}/remove", a.webmailRemoveRule)
	webmailMux.HandleFunc("GET "+MailboxPrefix+"/signatures", a.webmailSignaturesPage)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/signatures/save", a.webmailSignatureSave)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/signatures/{id}/delete", a.webmailSignatureDelete)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/signatures/{id}/default", a.webmailSignatureSetDefault)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/signatures/alias-default/remove", a.webmailSignatureRemoveAliasDefault)
	webmailMux.HandleFunc("GET "+MailboxPrefix+"/contacts", a.webmailContactsPage)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/contacts/save", a.webmailContactSave)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/contacts/{id}/delete", a.webmailContactDelete)
	webmailMux.HandleFunc("GET "+MailboxPrefix+"/blocklist", a.webmailBlocklistPage)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/blocklist/add", a.webmailBlocklistAdd)
	webmailMux.HandleFunc("POST "+MailboxPrefix+"/blocklist/{id}/remove", a.webmailBlocklistRemove)
	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()

	mux.HandleFunc("GET "+Prefix+"/", a.dashboard)
	mux.HandleFunc("GET "+Prefix+"/account", a.accountPage)
	mux.HandleFunc("GET "+Prefix+"/mfa-setup", a.mfaSetupRequiredPage)
	mux.HandleFunc("POST "+Prefix+"/account/password", a.changePassword)
	mux.HandleFunc("POST "+Prefix+"/account/totp/setup", a.totpSetupBegin)
	mux.HandleFunc("POST "+Prefix+"/account/totp/confirm", a.totpSetupConfirm)
	mux.HandleFunc("POST "+Prefix+"/account/totp/disable", a.totpDisable)
	mux.HandleFunc("POST "+Prefix+"/account/passkey/begin", a.passkeyRegisterBegin)
	mux.HandleFunc("POST "+Prefix+"/account/passkey/finish", a.passkeyRegisterFinish)
	mux.HandleFunc("POST "+Prefix+"/account/passkey/{id}/remove", a.passkeyRemove)
	mux.HandleFunc("GET "+Prefix+"/first-login", a.firstLoginForm)
	mux.HandleFunc("POST "+Prefix+"/first-login", a.firstLoginSubmit)

	mux.HandleFunc("GET "+Prefix+"/admins", a.adminsList)
	mux.HandleFunc("GET "+Prefix+"/admins/add", a.addAdminForm)
	mux.HandleFunc("POST "+Prefix+"/admins/add", a.addAdmin)
	mux.HandleFunc("GET "+Prefix+"/admins/{id}/edit", a.editAdminDomainsForm)
	mux.HandleFunc("POST "+Prefix+"/admins/{id}/edit", a.editAdminDomains)
	mux.HandleFunc("POST "+Prefix+"/admins/{id}/reset_mfa", a.resetAdminMFA)
	mux.HandleFunc("POST "+Prefix+"/admins/{id}/remove", a.removeAdmin)

	mux.HandleFunc("GET "+Prefix+"/domains", a.domainsList)
	mux.HandleFunc("GET "+Prefix+"/domains/add", a.addDomainForm)
	mux.HandleFunc("POST "+Prefix+"/domains/add", a.addDomain)
	mux.HandleFunc("POST "+Prefix+"/domains/{id}/delete", a.toggleDomainOff)
	mux.HandleFunc("GET "+Prefix+"/domains/{id}/edit", a.editDomainForm)
	mux.HandleFunc("POST "+Prefix+"/domains/{id}/edit", a.editDomain)
	mux.HandleFunc("POST "+Prefix+"/domains/{id}/toggle", a.toggleDomain)
	mux.HandleFunc("POST "+Prefix+"/domains/{id}/remove", a.removeDomain)
	mux.HandleFunc("POST "+Prefix+"/domains/{id}/verify_check", a.verifyDomainCheck)

	mux.HandleFunc("GET "+Prefix+"/senders", a.sendersList)
	mux.HandleFunc("GET "+Prefix+"/senders/add", a.addSenderForm)
	mux.HandleFunc("POST "+Prefix+"/senders/add", a.addSender)
	mux.HandleFunc("POST "+Prefix+"/senders/{id}/delete", a.disableSender)
	mux.HandleFunc("POST "+Prefix+"/senders/{id}/enable", a.enableSender)
	mux.HandleFunc("POST "+Prefix+"/senders/{id}/remove", a.removeSender)
	mux.HandleFunc("GET "+Prefix+"/senders/{id}/edit", a.editSenderForm)
	mux.HandleFunc("POST "+Prefix+"/senders/{id}/edit", a.editSender)

	mux.HandleFunc("GET "+Prefix+"/mailboxes", a.mailboxesList)
	mux.HandleFunc("GET "+Prefix+"/mailboxes/add", a.addMailboxForm)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/add", a.addMailbox)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/delete", a.disableMailbox)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/enable", a.enableMailbox)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/reset_mfa", a.resetMailboxMFA)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/remove", a.removeMailbox)
	mux.HandleFunc("GET "+Prefix+"/mailboxes/{id}/edit", a.editMailboxForm)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/edit", a.editMailbox)
	mux.HandleFunc("GET "+Prefix+"/mailboxes/{id}/apppasswords", a.appPasswordsList)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/apppasswords/add", a.addAppPassword)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/apppasswords/{pw_id}/revoke", a.revokeAppPassword)
	mux.HandleFunc("GET "+Prefix+"/mailboxes/{id}/aliases", a.aliasesList)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/aliases/add", a.addAlias)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/aliases/{alias_id}/remove", a.removeAlias)
	mux.HandleFunc("GET "+Prefix+"/mailboxes/{id}/lists", a.listsPage)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/lists/add", a.addAllowBlockEntry)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/lists/{entry_id}/remove", a.removeAllowBlockEntry)
	mux.HandleFunc("GET "+Prefix+"/mailboxes/{id}/rules", a.rulesList)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/rules/add", a.addRule)
	mux.HandleFunc("POST "+Prefix+"/mailboxes/{id}/rules/{rule_id}/remove", a.removeRule)

	mux.HandleFunc("GET "+Prefix+"/ips", a.ipsList)
	mux.HandleFunc("GET "+Prefix+"/ips/add", a.addIPForm)
	mux.HandleFunc("POST "+Prefix+"/ips/add", a.addIP)
	mux.HandleFunc("POST "+Prefix+"/ips/{id}/delete", a.disableIP)
	mux.HandleFunc("POST "+Prefix+"/ips/{id}/enable", a.enableIP)
	mux.HandleFunc("POST "+Prefix+"/ips/{id}/remove", a.removeIP)
	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)
	mux.HandleFunc("GET "+Prefix+"/dkim/{id}/edit", a.editDKIMForm)
	mux.HandleFunc("POST "+Prefix+"/dkim/{id}/edit", a.editDKIM)
	mux.HandleFunc("POST "+Prefix+"/dkim/{id}/toggle", a.toggleDKIM)
	mux.HandleFunc("POST "+Prefix+"/dkim/{id}/remove", a.removeDKIM)
	mux.HandleFunc("POST "+Prefix+"/dkim/check_dns", a.checkDKIMDNS)
	mux.HandleFunc("POST "+Prefix+"/dkim/check_spf", a.checkSPFDNS)

	// Server-wide config, not scoped to any domain — a domain-scoped admin has no
	// business reading or changing these, so every route here is global-admin-only.
	mux.HandleFunc("GET "+Prefix+"/letsencrypt", a.requireGlobalAdmin(a.letsEncryptPage))
	mux.HandleFunc("POST "+Prefix+"/letsencrypt/save", a.requireGlobalAdmin(a.letsEncryptSave))
	mux.HandleFunc("POST "+Prefix+"/letsencrypt/obtain", a.requireGlobalAdmin(a.letsEncryptObtainNow))
	mux.HandleFunc("POST "+Prefix+"/letsencrypt/http/save", a.requireGlobalAdmin(a.letsEncryptHTTPSave))
	mux.HandleFunc("POST "+Prefix+"/letsencrypt/http/obtain", a.requireGlobalAdmin(a.letsEncryptHTTPObtainNow))
	mux.HandleFunc("POST "+Prefix+"/api/letsencrypt/upload_gcloud_key", a.requireGlobalAdmin(a.uploadGCloudServiceAccount))
	mux.HandleFunc("GET "+Prefix+"/api/letsencrypt/detect_ip", a.requireGlobalAdmin(a.detectWANIP))

	mux.HandleFunc("GET "+Prefix+"/logs", a.logs)

	mux.HandleFunc("GET "+Prefix+"/settings", a.requireGlobalAdmin(a.settingsPage))
	mux.HandleFunc("POST "+Prefix+"/settings_update", a.requireGlobalAdmin(a.settingsUpdate))
	mux.HandleFunc("POST "+Prefix+"/api/settings/test_database", a.requireGlobalAdmin(a.testDatabaseConnection))
	mux.HandleFunc("POST "+Prefix+"/api/settings/upload_cert", a.requireGlobalAdmin(a.uploadCert))
	mux.HandleFunc("POST "+Prefix+"/api/settings/upload_key", a.requireGlobalAdmin(a.uploadKey))
	mux.HandleFunc("GET "+Prefix+"/api/settings/get_public_ip", a.requireGlobalAdmin(a.getServerIP))
	mux.HandleFunc("POST "+Prefix+"/test_attachments_path", a.requireGlobalAdmin(a.testAttachmentsPath))

	mux.HandleFunc("GET "+Prefix+"/msg/content/{id}", a.viewMessageContent)
	mux.HandleFunc("GET "+Prefix+"/msg/attachment/{id}/download", a.downloadAttachment)
	mux.HandleFunc("POST "+Prefix+"/msg/attachment/{id}/delete", a.deleteAttachment)

	mux.HandleFunc("GET "+Prefix, func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, Prefix+"/", http.StatusFound)
	})

	outer.Handle(Prefix+"/", a.requireAuth(mux))
	return outer
}
