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
+60 -13
View File
@@ -43,16 +43,17 @@ func (a *App) funcMap() template.FuncMap {
// isPast reports whether a nullable expiry timestamp has already passed —
// used to badge an app password as "Expired" even while is_active is
// still 1 (expiry and revocation are independent states).
"isPast": func(t *time.Time) bool { return t != nil && t.Before(time.Now()) },
"title": strings.Title,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"safe": func(s string) template.HTML { return template.HTML(s) },
"filesize": humanFileSize,
"dotToDash": func(s string) string { return strings.ReplaceAll(s, ".", "-") },
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"eq2": func(a, b any) bool { return fmt.Sprint(a) == fmt.Sprint(b) },
"isPast": func(t *time.Time) bool { return t != nil && t.Before(time.Now()) },
"ruleSummary": summarizeConditions,
"title": strings.Title,
"upper": strings.ToUpper,
"lower": strings.ToLower,
"safe": func(s string) template.HTML { return template.HTML(s) },
"filesize": humanFileSize,
"dotToDash": func(s string) string { return strings.ReplaceAll(s, ".", "-") },
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"eq2": func(a, b any) bool { return fmt.Sprint(a) == fmt.Sprint(b) },
// dget looks up an optional map key, returning "" if absent — mirrors Jinja's
// `x if x is defined else ''` pattern used for context vars only some pages set
// (e.g. sidebar badge counts, which only dashboard passes).
@@ -62,7 +63,8 @@ func (a *App) funcMap() template.FuncMap {
}
return ""
},
"list": func(items ...string) []string { return items },
"list": func(items ...string) []string { return items },
"isStandardFolder": isStandardFolder,
// emailOverallStatus mirrors the delivered/failed selectattr computation
// dashboard.html and logs.html both do in the Python templates.
"emailOverallStatus": func(recipients []db.EmailRecipientLog) string {
@@ -130,6 +132,7 @@ var pages = []string{
"mailboxes.html", "add_mailbox.html", "edit_mailbox.html", "mailbox_apppasswords.html", "mailbox_aliases.html",
"mailbox_lists.html", "mailbox_rules.html",
"ips.html", "add_ip.html", "edit_ip.html",
"blacklist.html",
"dkim.html", "edit_dkim.html",
"settings.html", "letsencrypt.html", "logs.html", "view_message_content.html", "error.html",
"account.html", "first_login.html",
@@ -144,6 +147,39 @@ var pages = []string{
var standalonePages = []string{
"login.html", "login_mfa.html", "mfa_setup_required.html", "totp_setup.html",
"webmail_login.html", "webmail_login_mfa.html", "webmail_account.html", "webmail_totp_setup.html", "webmail_mfa_setup_required.html",
"webmail_folder.html", "webmail_message.html", "webmail_compose.html", "webmail_rules.html", "webmail_certs.html",
}
// pagesWithComposeWidget are the standalone pages that show a Compose/Reply/Forward
// entry point and so need webmail_compose_widget.html's floating-popup markup+JS
// parsed alongside them (see webmail_compose_widget.html's {{define "compose_widget"}}).
// webmail_compose.html itself is excluded — it's what opens inside the popup, not
// something that opens a popup of its own.
var pagesWithComposeWidget = []string{
"webmail_folder.html", "webmail_message.html", "webmail_rules.html", "webmail_certs.html", "webmail_account.html",
}
func hasComposeWidget(page string) bool {
for _, p := range pagesWithComposeWidget {
if p == page {
return true
}
}
return false
}
// pagesWithShortcuts are the two pages keyboard shortcuts make sense on — the
// message list (j/k/Enter/o) and a single open message (r/a/f/#). See
// webmail_shortcuts.html's {{define "webmail_shortcuts"}}.
var pagesWithShortcuts = []string{"webmail_folder.html", "webmail_message.html"}
func hasShortcuts(page string) bool {
for _, p := range pagesWithShortcuts {
if p == page {
return true
}
}
return false
}
// loadTemplates parses from the embedded assets FS (see embed.go), not the
@@ -153,7 +189,7 @@ func (a *App) loadTemplates() error {
a.templates = map[string]*template.Template{}
for _, page := range pages {
t := template.New("base.html").Funcs(a.funcMap())
t, err := t.ParseFS(assets, "templates/base.html", "templates/sidebar_email.html", "templates/"+page)
t, err := t.ParseFS(assets, "templates/base.html", "templates/sidebar_email.html", "templates/csrf_script.html", "templates/"+page)
if err != nil {
return fmt.Errorf("parse %s: %w", page, err)
}
@@ -161,7 +197,14 @@ func (a *App) loadTemplates() error {
}
for _, page := range standalonePages {
t := template.New(page).Funcs(a.funcMap())
t, err := t.ParseFS(assets, "templates/"+page)
files := []string{"templates/" + page, "templates/csrf_script.html"}
if hasComposeWidget(page) {
files = append(files, "templates/webmail_compose_widget.html")
}
if hasShortcuts(page) {
files = append(files, "templates/webmail_shortcuts.html")
}
t, err := t.ParseFS(assets, files...)
if err != nil {
return fmt.Errorf("parse %s: %w", page, err)
}
@@ -190,6 +233,9 @@ func (a *App) render(w http.ResponseWriter, r *http.Request, page string, data M
if data == nil {
data = M{}
}
// Set unconditionally for every page — pages with no session cookie yet (login)
// just get "", which csrf_script.html's injected script treats as a no-op.
data["csrf_token"] = a.csrfTokenFor(r)
if isStandalonePage(page) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := t.ExecuteTemplate(w, page, data); err != nil {
@@ -214,6 +260,7 @@ func (a *App) render(w http.ResponseWriter, r *http.Request, page string, data M
data["mailbox_count"] = counts.MailboxCount
data["ip_count"] = counts.IPCount
data["dkim_count"] = counts.DKIMCount
data["blacklist_count"] = counts.BlacklistCount
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := t.ExecuteTemplate(w, "base.html", data); err != nil {
a.Logger.Error("template render error (%s): %v", page, err)