- 每个连接记录一条日志:协议、端口、来源 IP、用户名、成功/失败、 失败原因(密码错误/IP封禁/中继被拒/发件人伪造/未认证发信等)、 操作摘要、消息数与会话时长 - 管理后台新增「协议日志」页:按协议/状态/IP/用户名/时间筛选, 今日与历史成功/失败统计卡片,分页查看,可手动清理 - 后台每 6 小时自动清理超出 protocol_log_keep_days(默认30天) 的日志;新增 [web] protocol_log_keep_days 配置项 - 修复 POP3 认证既有 bug:handleUSER 丢弃邮箱域名导致 PASS 永远失败 - 新增 store 单测、SMTP/POP3 端到端测试与模板渲染测试
140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
package store
|
|
|
|
import (
|
|
"time"
|
|
|
|
"mail_go/internal/db"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ProtocolLogFilter holds optional filters for listing protocol logs.
|
|
type ProtocolLogFilter struct {
|
|
Protocol string // smtp | imap | pop3,空表示全部
|
|
Success *bool // nil 表示全部
|
|
IP string // 客户端 IP 模糊匹配
|
|
Username string // 用户名模糊匹配
|
|
From time.Time // 起(含)
|
|
To time.Time // 止(含)
|
|
}
|
|
|
|
// ProtocolLogStore defines the interface for protocol call log operations.
|
|
type ProtocolLogStore interface {
|
|
Create(log *db.ProtocolLog) error
|
|
// UpdateDuration 回填会话时长(登录后连接关闭时调用)。
|
|
UpdateDuration(id uint, durationMs int64) error
|
|
List(page, size int, filter ProtocolLogFilter) ([]db.ProtocolLog, int64, error)
|
|
// CountStats 汇总各协议的失败/成功记录数(用于页面统计卡片)。
|
|
CountStats(from time.Time) (map[string]map[string]int64, error)
|
|
// CleanupBefore 删除 created_at 早于 before 的记录。
|
|
CleanupBefore(before time.Time) (int64, error)
|
|
}
|
|
|
|
// protocolLogStoreGorm implements ProtocolLogStore using GORM.
|
|
type protocolLogStoreGorm struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// newProtocolLogStore creates a new GORM-backed ProtocolLogStore.
|
|
func newProtocolLogStore(database *gorm.DB) ProtocolLogStore {
|
|
return &protocolLogStoreGorm{db: database}
|
|
}
|
|
|
|
// Create inserts a new protocol log record.
|
|
func (s *protocolLogStoreGorm) Create(log *db.ProtocolLog) error {
|
|
return s.db.Create(log).Error
|
|
}
|
|
|
|
// UpdateDuration 回填会话时长,仅更新 duration_ms 字段。
|
|
func (s *protocolLogStoreGorm) UpdateDuration(id uint, durationMs int64) error {
|
|
return s.db.Model(&db.ProtocolLog{}).Where("id = ?", id).Update("duration_ms", durationMs).Error
|
|
}
|
|
|
|
// List retrieves a paginated list of protocol logs, newest first.
|
|
func (s *protocolLogStoreGorm) List(page, size int, filter ProtocolLogFilter) ([]db.ProtocolLog, int64, error) {
|
|
var logs []db.ProtocolLog
|
|
var total int64
|
|
|
|
query := s.db.Model(&db.ProtocolLog{})
|
|
query = s.applyFilter(query, filter)
|
|
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
offset := (page - 1) * size
|
|
if err := query.Order("id DESC").Offset(offset).Limit(size).Find(&logs).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return logs, total, nil
|
|
}
|
|
|
|
func (s *protocolLogStoreGorm) applyFilter(query *gorm.DB, filter ProtocolLogFilter) *gorm.DB {
|
|
if filter.Protocol != "" {
|
|
query = query.Where("protocol = ?", filter.Protocol)
|
|
}
|
|
if filter.Success != nil {
|
|
query = query.Where("success = ?", *filter.Success)
|
|
}
|
|
if filter.IP != "" {
|
|
query = query.Where("client_ip LIKE ?", "%"+filter.IP+"%")
|
|
}
|
|
if filter.Username != "" {
|
|
query = query.Where("username LIKE ?", "%"+filter.Username+"%")
|
|
}
|
|
if !filter.From.IsZero() {
|
|
query = query.Where("created_at >= ?", filter.From)
|
|
}
|
|
if !filter.To.IsZero() {
|
|
query = query.Where("created_at <= ?", filter.To)
|
|
}
|
|
return query
|
|
}
|
|
|
|
// CountStats 返回自 from 以来的记录数,按 protocol 再按 success 分组:
|
|
// map[protocol]map[successKey]count。successKey 为 "success"/"fail"。
|
|
func (s *protocolLogStoreGorm) CountStats(from time.Time) (map[string]map[string]int64, error) {
|
|
stats := make(map[string]map[string]int64)
|
|
for _, proto := range []string{db.ProtocolSMTP, db.ProtocolIMAP, db.ProtocolPOP3} {
|
|
stats[proto] = map[string]int64{"success": 0, "fail": 0}
|
|
}
|
|
|
|
type row struct {
|
|
Protocol string
|
|
Success bool
|
|
Count int64
|
|
}
|
|
var rows []row
|
|
|
|
query := s.db.Model(&db.ProtocolLog{}).
|
|
Select("protocol, success, COUNT(*) AS count").
|
|
Group("protocol, success")
|
|
if !from.IsZero() {
|
|
query = query.Where("created_at >= ?", from)
|
|
}
|
|
if err := query.Scan(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, r := range rows {
|
|
proto := stats[r.Protocol]
|
|
if proto == nil {
|
|
proto = map[string]int64{"success": 0, "fail": 0}
|
|
stats[r.Protocol] = proto
|
|
}
|
|
key := "fail"
|
|
if r.Success {
|
|
key = "success"
|
|
}
|
|
proto[key] = r.Count
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// CleanupBefore deletes records older than the given time and returns the
|
|
// number of deleted rows.
|
|
func (s *protocolLogStoreGorm) CleanupBefore(before time.Time) (int64, error) {
|
|
res := s.db.Where("created_at < ?", before).Delete(&db.ProtocolLog{})
|
|
return res.RowsAffected, res.Error
|
|
}
|