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:
2026-08-19 18:49:29 +08:00
parent 8ea4a623a9
commit 353bfa88f2
30 files changed
+1233 -59

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ func InitDB(cfg config.DatabaseConfig, storageCfg config.StorageConfig) (*gorm.D
}
// Auto-migrate all models
if err := db.AutoMigrate(&User{}, &Domain{}, &Message{}, &Attachment{}, &BanEntry{}, &OutboundMessage{}); err != nil {
if err := db.AutoMigrate(&User{}, &Domain{}, &Message{}, &Attachment{}, &BanEntry{}, &OutboundMessage{}, &ProtocolLog{}); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}
+28
View File
@@ -122,6 +122,34 @@ type BanEntry struct {
// TableName specifies the table name for BanEntry.
func (BanEntry) TableName() string { return "ban_entries" }
// Protocol log statuses.
const (
ProtocolSMTP = "smtp"
ProtocolIMAP = "imap"
ProtocolPOP3 = "pop3"
)
// ProtocolLog records one SMTP/IMAP/POP3 connection session: auth result,
// failure reason and source IP, for admin analysis of attacks/abuse.
type ProtocolLog struct {
ID uint `gorm:"primaryKey" json:"id"`
Protocol string `gorm:"size:16;index;not null" json:"protocol"` // smtp | imap | pop3
Port int `json:"port"` // 25/465/587/143/993/110/995
ClientIP string `gorm:"size:64;index;not null" json:"client_ip"`
Username string `gorm:"size:255;index" json:"username"`
Success bool `gorm:"index" json:"success"`
FailReason string `gorm:"size:512" json:"fail_reason"`
Detail string `gorm:"size:2048" json:"detail"`
MsgCount int `json:"msg_count"`
DurationMs int64 `json:"duration_ms"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// TableName specifies the table name for ProtocolLog.
func (ProtocolLog) TableName() string {
return "protocol_logs"
}
// Attachment represents a file attached to an email message.
type Attachment struct {
ID uint `gorm:"primaryKey" json:"id"`