package webui import ( "net/http" "strings" "time" "mailgoserver/internal/db" ) func emailDomain(addr string) string { if i := strings.LastIndex(addr, "@"); i >= 0 { return strings.ToLower(addr[i+1:]) } return "" } // dashboard mirrors dashboard.py's dashboard(), scoped to the current admin's // assigned domains unless they're a global admin. func (a *App) dashboard(w http.ResponseWriter, r *http.Request) { scope := scopeFromContext(r) allowedNames, isGlobal, err := a.accessibleDomainNames(r) if err != nil { a.Logger.Error("dashboard: %v", err) } // Domain/sender/mailbox/DKIM counts are injected uniformly into every page by // render() (see computeNavCounts) — only "near quota" is dashboard-specific, // so it's the only mailbox stat still computed here. var mailboxesNearQuota int mailboxes, _ := a.DB.ListMailboxes() for _, m := range mailboxes { if !m.IsActive || (!isGlobal && !scope.Allowed(m.DomainID)) { continue } if m.QuotaBytes > 0 && float64(m.UsedBytes)/float64(m.QuotaBytes)*100 >= 90 { mailboxesNearQuota++ } } allEmails, err := a.DB.ListEmailLogsPage(0, 50) if err != nil { setFlash(w, "error", "Error loading recent activity") } var recentEmails []db.EmailLog for _, e := range allEmails { if isGlobal || allowedNames[emailDomain(e.MailFrom)] { recentEmails = append(recentEmails, e) } if len(recentEmails) == 10 { break } } allAuths, _ := a.DB.ListRecentAuthLogs(50) var recentAuths []db.AuthLog for _, au := range allAuths { if isGlobal || allowedNames[authLogDomain(au.Identifier)] { recentAuths = append(recentAuths, au) } if len(recentAuths) == 10 { break } } data := M{ "active": "dashboard", "mailboxes_near_quota": mailboxesNearQuota, "recent_emails": recentEmails, "recent_auths": recentAuths, } // Attack-count tiles: blacklist entries aren't attributable to a single domain // (see blacklist.go's doc comment), so this is global-admin-only, matching the // Blacklist page and sidebar section's own gating. if isGlobal { now := time.Now() since24h := now.Add(-24 * time.Hour) since7d := now.Add(-7 * 24 * time.Hour) data["failed_auth_24h"], _ = a.DB.CountFailedAuthSince(since24h) data["failed_auth_7d"], _ = a.DB.CountFailedAuthSince(since7d) data["blacklist_events_24h"], _ = a.DB.CountBlacklistEventsSince(since24h) data["blacklist_events_7d"], _ = a.DB.CountBlacklistEventsSince(since7d) var activeBlacklistCount int if entries, err := a.DB.ListBlacklist(); err == nil { for _, e := range entries { if e.ExpiresAt.After(now) { activeBlacklistCount++ } } } data["active_blacklist_count"] = activeBlacklistCount } a.render(w, r, "dashboard.html", data) } // authLogDomain best-effort extracts a domain name from an AuthLog identifier, whose // format varies by auth_type: a bare email ("sender"), "ip -> domain" (ip), or // "sender@x -> target@y" (sender_validation). There's no domain_id column on this // table (it predates admin scoping), so this is a text heuristic, not a foreign key. func authLogDomain(identifier string) string { if idx := strings.LastIndex(identifier, "->"); idx >= 0 { return emailOrBareDomain(strings.TrimSpace(identifier[idx+2:])) } return emailOrBareDomain(identifier) } func emailOrBareDomain(s string) string { if strings.Contains(s, "@") { return emailDomain(s) } return strings.ToLower(s) }