303 lines
17 KiB
Go
303 lines
17 KiB
Go
// 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
|
|
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 *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
|
|
}
|
|
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/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("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()
|
|
|
|
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+"/api/letsencrypt/upload_gcloud_key", a.requireGlobalAdmin(a.uploadGCloudServiceAccount))
|
|
|
|
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
|
|
}
|