feat: 新增 SMTP/IMAP/POP3 协议调用日志(含攻击分析筛选)
- 每个连接记录一条日志:协议、端口、来源 IP、用户名、成功/失败、 失败原因(密码错误/IP封禁/中继被拒/发件人伪造/未认证发信等)、 操作摘要、消息数与会话时长 - 管理后台新增「协议日志」页:按协议/状态/IP/用户名/时间筛选, 今日与历史成功/失败统计卡片,分页查看,可手动清理 - 后台每 6 小时自动清理超出 protocol_log_keep_days(默认30天) 的日志;新增 [web] protocol_log_keep_days 配置项 - 修复 POP3 认证既有 bug:handleUSER 丢弃邮箱域名导致 PASS 永远失败 - 新增 store 单测、SMTP/POP3 端到端测试与模板渲染测试
This commit is contained in:
30 files changed
+1233
-59
No files matched your search
@@ -30,12 +30,14 @@ type AdminHandler struct {
|
||||
tlsDir string
|
||||
caddyDataDir string
|
||||
outbound *outbound.Manager
|
||||
// protocolLogKeepDays SMTP/IMAP/POP3 协议日志保留天数(配置文件 [web])
|
||||
protocolLogKeepDays int
|
||||
}
|
||||
|
||||
// NewAdminHandler creates a new AdminHandler with the given stores, attachment
|
||||
// storage, TLS directory, Caddy data directory and outbound delivery manager.
|
||||
func NewAdminHandler(stores *store.Stores, attStorage *storage.AttachmentStorage, tlsDir string, caddyDataDir string, ob *outbound.Manager) *AdminHandler {
|
||||
return &AdminHandler{stores: stores, storage: attStorage, tlsDir: tlsDir, caddyDataDir: caddyDataDir, outbound: ob}
|
||||
func NewAdminHandler(stores *store.Stores, attStorage *storage.AttachmentStorage, tlsDir string, caddyDataDir string, ob *outbound.Manager, protocolLogKeepDays int) *AdminHandler {
|
||||
return &AdminHandler{stores: stores, storage: attStorage, tlsDir: tlsDir, caddyDataDir: caddyDataDir, outbound: ob, protocolLogKeepDays: protocolLogKeepDays}
|
||||
}
|
||||
|
||||
// Dashboard renders the admin dashboard with summary statistics.
|
||||
@@ -773,6 +775,111 @@ func (h *AdminHandler) CleanupBans(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/admin/bans")
|
||||
}
|
||||
|
||||
// ListProtocolLogs 渲染协议调用日志页(SMTP/IMAP/POP3 调用记录,支持筛选)。
|
||||
func (h *AdminHandler) ListProtocolLogs(c *gin.Context) {
|
||||
// 页面访问时顺带清理过期日志,避免日志表无限增长
|
||||
h.stores.ProtocolLogs.CleanupBefore(time.Now().AddDate(0, 0, -h.protocolLogKeepDays))
|
||||
|
||||
page := getPageParam(c, "page", 1)
|
||||
pageSize := 50
|
||||
|
||||
var success *bool
|
||||
switch c.Query("success") {
|
||||
case "success":
|
||||
v := true
|
||||
success = &v
|
||||
case "fail":
|
||||
v := false
|
||||
success = &v
|
||||
}
|
||||
|
||||
from := parseDateQuery(c.Query("from"))
|
||||
to := parseDateQuery(c.Query("to"))
|
||||
// 日期选择到天,含当天
|
||||
if !to.IsZero() {
|
||||
to = to.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
filter := store.ProtocolLogFilter{
|
||||
Protocol: c.Query("protocol"),
|
||||
Success: success,
|
||||
IP: strings.TrimSpace(c.Query("ip")),
|
||||
Username: strings.TrimSpace(c.Query("username")),
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
|
||||
logs, total, err := h.stores.ProtocolLogs.List(page, pageSize, filter)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "加载协议日志失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 统计卡片:今日 + 全部成功/失败数(按协议),int64 → int 供模板 add 使用
|
||||
dayStart := time.Now().Truncate(24 * time.Hour)
|
||||
todayStats, _ := h.stores.ProtocolLogs.CountStats(dayStart)
|
||||
allStats, _ := h.stores.ProtocolLogs.CountStats(time.Time{})
|
||||
normStats := func(m map[string]map[string]int64) map[string]map[string]int {
|
||||
out := make(map[string]map[string]int, len(m))
|
||||
for proto, counts := range m {
|
||||
out[proto] = map[string]int{"success": int(counts["success"]), "fail": int(counts["fail"])}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
currentUser, _ := c.Get("currentUser")
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
if totalPages < 1 {
|
||||
totalPages = 0
|
||||
}
|
||||
|
||||
// 分页/筛选链接保留当前筛选条件(URL 编码防止特殊字符破坏链接)
|
||||
query := map[string]string{
|
||||
"protocol": url.QueryEscape(filter.Protocol),
|
||||
"success": url.QueryEscape(c.Query("success")),
|
||||
"ip": url.QueryEscape(filter.IP),
|
||||
"username": url.QueryEscape(filter.Username),
|
||||
"from": url.QueryEscape(c.Query("from")),
|
||||
"to": url.QueryEscape(c.Query("to")),
|
||||
}
|
||||
|
||||
c.HTML(200, "admin_protocol_logs", gin.H{
|
||||
"currentUser": currentUser,
|
||||
"logs": logs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
"totalPages": totalPages,
|
||||
"filter": query,
|
||||
"todayStats": normStats(todayStats),
|
||||
"allStats": normStats(allStats),
|
||||
"keepDays": h.protocolLogKeepDays,
|
||||
"activeFolder": "protocol-logs",
|
||||
})
|
||||
}
|
||||
|
||||
// CleanupProtocolLogs 手动清理超出保留天数的协议日志。
|
||||
func (h *AdminHandler) CleanupProtocolLogs(c *gin.Context) {
|
||||
_, _ = h.stores.ProtocolLogs.CleanupBefore(time.Now().AddDate(0, 0, -h.protocolLogKeepDays))
|
||||
c.Redirect(http.StatusFound, "/admin/protocol-logs")
|
||||
}
|
||||
|
||||
// parseDateQuery 解析 YYYY-MM-DD 日期,失败返回零值。
|
||||
func parseDateQuery(s string) time.Time {
|
||||
if s == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02", s, time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ListMails renders the admin mail list page showing all messages across all users.
|
||||
func (h *AdminHandler) ListMails(c *gin.Context) {
|
||||
page := getPageParam(c, "page", 1)
|
||||
|
||||
@@ -68,6 +68,27 @@ func TestRenderAllPages(t *testing.T) {
|
||||
}},
|
||||
{"settings", ginH{"currentUser": user, "activeFolder": "settings", "error": "", "success": "", "inboxUnread": int64(2), "draftsTotal": int64(1), "sentTotal": int64(3)}},
|
||||
{"admin_dashboard", ginH{"currentUser": user, "activeFolder": "admin", "domainCount": 2, "userCount": 5, "totalMails": 100, "banCount": 1, "inboxCount": 50, "sentCount": 30, "draftsCount": 10, "trashCount": 5, "inboxSize": int64(1024), "sentSize": int64(512), "totalSize": int64(2048), "todayReceived": 3, "todaySent": 2, "weekReceived": 20, "weekSent": 15}},
|
||||
{"admin_protocol_logs", ginH{
|
||||
"currentUser": user, "activeFolder": "protocol-logs",
|
||||
"logs": []db.ProtocolLog{
|
||||
{ID: 1, Protocol: db.ProtocolSMTP, Port: 25, ClientIP: "203.0.113.7", Username: "", Success: true, FailReason: "", Detail: "MAIL FROM:<spam@evil.example> RCPT×1 本地投递1", MsgCount: 1, DurationMs: 1234, CreatedAt: now},
|
||||
{ID: 2, Protocol: db.ProtocolIMAP, Port: 993, ClientIP: "203.0.113.9", Username: "admin", Success: false, FailReason: "用户名或密码错误", Detail: "LOGIN 失败", DurationMs: 88, CreatedAt: now.Add(-time.Minute)},
|
||||
{ID: 3, Protocol: db.ProtocolPOP3, Port: 110, ClientIP: "10.0.0.2", Username: "alice", Success: true, FailReason: "", Detail: "USER PASS STAT RETR×3 QUIT", MsgCount: 3, DurationMs: 500, CreatedAt: now.Add(-2 * time.Minute)},
|
||||
},
|
||||
"total": 3, "page": 1, "pageSize": 50, "totalPages": 1,
|
||||
"filter": map[string]string{"protocol": "smtp", "success": "fail", "ip": "203.0.113", "username": "", "from": "2026-08-01", "to": "2026-08-19"},
|
||||
"todayStats": map[string]map[string]int{
|
||||
db.ProtocolSMTP: {"success": 10, "fail": 2},
|
||||
db.ProtocolIMAP: {"success": 5, "fail": 7},
|
||||
db.ProtocolPOP3: {"success": 3, "fail": 4},
|
||||
},
|
||||
"allStats": map[string]map[string]int{
|
||||
db.ProtocolSMTP: {"success": 100, "fail": 20},
|
||||
db.ProtocolIMAP: {"success": 50, "fail": 70},
|
||||
db.ProtocolPOP3: {"success": 30, "fail": 40},
|
||||
},
|
||||
"keepDays": 30,
|
||||
}},
|
||||
}
|
||||
|
||||
outDir := os.Getenv("MAILGO_PREVIEW_DIR")
|
||||
|
||||
@@ -59,7 +59,7 @@ func templateFuncs() template.FuncMap {
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"mul": func(a, b int) int { return a * b },
|
||||
"div": func(a, b int) int { return a / b },
|
||||
"div": func(a, b int64) int64 { return a / b },
|
||||
"mod": func(a, b int) int { return a % b },
|
||||
"ceilDiv": func(a, b int) int { return int(math.Ceil(float64(a) / float64(b))) },
|
||||
"seq": func(n int) []int {
|
||||
@@ -228,7 +228,7 @@ func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storag
|
||||
func (ws *WebServer) registerRoutes() {
|
||||
authHandler := handlers.NewAuthHandler(ws.stores, ws.authCfg, ws.banCfg)
|
||||
mailHandler := handlers.NewMailHandler(ws.stores, ws.storage, ws.outbound)
|
||||
adminHandler := handlers.NewAdminHandler(ws.stores, ws.storage, filepath.Join(ws.storageCfg.BaseDir, "tls", "domains"), ws.caddyDataDir, ws.outbound)
|
||||
adminHandler := handlers.NewAdminHandler(ws.stores, ws.storage, filepath.Join(ws.storageCfg.BaseDir, "tls", "domains"), ws.caddyDataDir, ws.outbound, ws.cfg.ProtocolLogKeepDays)
|
||||
|
||||
// Apply BanMiddleware globally before public routes
|
||||
ws.engine.Use(middleware.BanMiddleware(ws.stores))
|
||||
@@ -297,6 +297,8 @@ func (ws *WebServer) registerRoutes() {
|
||||
admin.GET("/bans", adminHandler.ListBans)
|
||||
admin.POST("/bans/:id/unban", adminHandler.UnbanIP)
|
||||
admin.POST("/bans/cleanup", adminHandler.CleanupBans)
|
||||
admin.GET("/protocol-logs", adminHandler.ListProtocolLogs)
|
||||
admin.POST("/protocol-logs/cleanup", adminHandler.CleanupProtocolLogs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2 style="margin-bottom:24px;">管理后台</h2>
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" class="active">所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2 style="margin-bottom:24px;">外发队列</h2>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
{{define "admin_protocol_logs"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>协议日志 - MailGo</title>
|
||||
{{template "styles" .}}
|
||||
</head>
|
||||
<body>
|
||||
{{template "navbar" .}}
|
||||
<div class="container">
|
||||
<div class="clearfix">
|
||||
<div class="sidebar">
|
||||
<a href="/inbox">返回邮箱</a>
|
||||
<a href="/admin" {{if eq .activeFolder "admin"}}class="active"{{end}}>控制面板</a>
|
||||
<a href="/admin/domains" {{if eq .activeFolder "domains"}}class="active"{{end}}>域名管理</a>
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
||||
<h2>协议日志(SMTP / IMAP / POP3)</h2>
|
||||
<form method="POST" action="/admin/protocol-logs/cleanup" style="display:inline;"
|
||||
onsubmit="return confirm('确认清理 {{.keepDays}} 天前的协议日志?');">
|
||||
<button type="submit" class="btn btn-primary">清理旧日志</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:24px;">
|
||||
<div class="stat-card">
|
||||
<h3>{{index .todayStats "smtp" "fail"}}</h3>
|
||||
<p>今日 SMTP 失败</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>{{index .todayStats "imap" "fail"}}</h3>
|
||||
<p>今日 IMAP 失败</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>{{index .todayStats "pop3" "fail"}}</h3>
|
||||
<p>今日 POP3 失败</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>{{add (add (index .allStats "smtp" "fail") (index .allStats "imap" "fail")) (index .allStats "pop3" "fail")}}</h3>
|
||||
<p>历史失败(全部)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="GET" action="/admin/protocol-logs" style="margin-bottom:16px;">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
|
||||
<select name="protocol" style="padding:6px 10px;">
|
||||
<option value="">全部协议</option>
|
||||
<option value="smtp" {{if eq .filter.protocol "smtp"}}selected{{end}}>SMTP</option>
|
||||
<option value="imap" {{if eq .filter.protocol "imap"}}selected{{end}}>IMAP</option>
|
||||
<option value="pop3" {{if eq .filter.protocol "pop3"}}selected{{end}}>POP3</option>
|
||||
</select>
|
||||
<select name="success" style="padding:6px 10px;">
|
||||
<option value="">全部状态</option>
|
||||
<option value="success" {{if eq .filter.success "success"}}selected{{end}}>成功</option>
|
||||
<option value="fail" {{if eq .filter.success "fail"}}selected{{end}}>失败</option>
|
||||
</select>
|
||||
<input type="text" name="ip" placeholder="来源 IP(模糊)" value="{{.filter.ip}}" style="padding:6px 10px;width:160px;">
|
||||
<input type="text" name="username" placeholder="用户名(模糊)" value="{{.filter.username}}" style="padding:6px 10px;width:160px;">
|
||||
<input type="date" name="from" value="{{.filter.from}}" style="padding:6px 10px;">
|
||||
<span>至</span>
|
||||
<input type="date" name="to" value="{{.filter.to}}" style="padding:6px 10px;">
|
||||
<button type="submit" class="btn btn-sm btn-primary">筛选</button>
|
||||
<a href="/admin/protocol-logs" class="btn btn-sm">重置</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>协议</th>
|
||||
<th>端口</th>
|
||||
<th>来源 IP</th>
|
||||
<th>用户名</th>
|
||||
<th>状态</th>
|
||||
<th>失败原因</th>
|
||||
<th>操作摘要</th>
|
||||
<th>消息数</th>
|
||||
<th>时长</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .logs}}
|
||||
<tr>
|
||||
<td style="white-space:nowrap;">{{.CreatedAt.Format "2006-01-02 15:04:05"}}</td>
|
||||
<td>
|
||||
{{if eq .Protocol "smtp"}}<span class="badge" style="background:#3498db;color:#fff;">SMTP</span>
|
||||
{{else if eq .Protocol "imap"}}<span class="badge" style="background:#9b59b6;color:#fff;">IMAP</span>
|
||||
{{else}}<span class="badge" style="background:#16a085;color:#fff;">POP3</span>{{end}}
|
||||
</td>
|
||||
<td>{{.Port}}</td>
|
||||
<td>{{.ClientIP}}</td>
|
||||
<td>{{if .Username}}{{.Username}}{{else}}—{{end}}</td>
|
||||
<td>
|
||||
{{if .Success}}<span class="badge" style="background:#27ae60;color:#fff;">成功</span>
|
||||
{{else}}<span class="badge badge-unread">失败</span>{{end}}
|
||||
</td>
|
||||
<td style="max-width:200px;">{{if .FailReason}}{{.FailReason}}{{else}}—{{end}}</td>
|
||||
<td style="max-width:320px;word-break:break-all;font-size:13px;color:#555;">{{.Detail}}</td>
|
||||
<td>{{if .MsgCount}}{{.MsgCount}}{{else}}—{{end}}</td>
|
||||
<td>{{if .DurationMs}}{{div .DurationMs 1000}}s{{else}}—{{end}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="10" style="text-align:center;color:#7f8c8d;">暂无记录</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{if gt .totalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if gt .page 1}}<a href="/admin/protocol-logs?page={{sub .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">上一页</a>{{end}}
|
||||
<span class="current">第 {{.page}} / {{.totalPages}} 页(共 {{.total}} 条)</span>
|
||||
{{if lt .page .totalPages}}<a href="/admin/protocol-logs?page={{add .page 1}}&protocol={{.filter.protocol}}&success={{.filter.success}}&ip={{.filter.ip}}&username={{.filter.username}}&from={{.filter.from}}&to={{.filter.to}}">下一页</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
<a href="/admin/users" {{if eq .activeFolder "users"}}class="active"{{end}}>用户管理</a>
|
||||
<a href="/admin/mails" {{if eq .activeFolder "mails"}}class="active"{{end}}>所有邮件</a>
|
||||
<a href="/admin/outbound" {{if eq .activeFolder "outbound"}}class="active"{{end}}>外发队列</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
<a href="/admin/protocol-logs" {{if eq .activeFolder "protocol-logs"}}class="active"{{end}}>协议日志</a>
|
||||
<a href="/admin/bans" {{if eq .activeFolder "bans"}}class="active"{{end}}>IP黑名单</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
|
||||
Reference in New Issue
Block a user