feat(imap): 新邮件实时推送(IDLE)+ 后台当前连接监控

- IMAP 推送:imapBackend 实现 backend.BackendUpdater,SMTP 本地投递
  与 Web 写信投递成功后 NotifyNewMessage,挂起 IDLE 的客户端即时收到
  新邮件 FETCH 通知(按用户名+INBOX 过滤广播,通道满非阻塞丢弃)
- 当前连接:新增 internal/connhub 连接注册中心,SMTP/IMAP/POP3 三协议
  注册/注销/用户名/TLS/活跃时间追踪;后台新增「当前连接」页
  (/admin/connections,统计卡片+连接表格,每 5 秒自动刷新)
- 新增测试:connhub 并发安全、推送内容/非阻塞/nil 安全、
  后台页面渲染;全量 -race 通过
This commit is contained in:
2026-08-19 19:40:05 +08:00
parent b158b8f1f5
commit ede85e0698
27 changed files with 783 additions and 53 deletions
+16 -2
View File
@@ -12,6 +12,7 @@ import (
"time"
"mail_go/config"
"mail_go/internal/connhub"
"mail_go/internal/db"
"mail_go/internal/store"
"mail_go/internal/tlsutil"
@@ -24,13 +25,14 @@ type POP3Server struct {
cfg config.POP3Config
banCfg config.BanConfig
tlsLoader *tlsutil.Loader
hub *connhub.Hub
wg sync.WaitGroup
}
// NewPOP3Server creates a new POP3 server instance. tlsLoader may be nil
// when TLS is not configured.
func NewPOP3Server(cfg config.POP3Config, stores *store.Stores, tlsLoader *tlsutil.Loader, banCfg config.BanConfig) *POP3Server {
return &POP3Server{stores: stores, cfg: cfg, banCfg: banCfg, tlsLoader: tlsLoader}
func NewPOP3Server(cfg config.POP3Config, stores *store.Stores, tlsLoader *tlsutil.Loader, banCfg config.BanConfig, hub *connhub.Hub) *POP3Server {
return &POP3Server{stores: stores, cfg: cfg, banCfg: banCfg, tlsLoader: tlsLoader, hub: hub}
}
func (s *POP3Server) tlsConfig() (*tls.Config, error) {
@@ -133,6 +135,9 @@ func (s *POP3Server) handleConn(conn net.Conn, port int) {
return
}
// 连接追踪:注册到当前连接中心,连接结束时注销
activeConn := s.hub.Register("pop3", clientIP, port, false)
// 会话状态(供协议日志汇总)
var (
authUser *db.User
@@ -147,6 +152,12 @@ func (s *POP3Server) handleConn(conn net.Conn, port int) {
var deleted map[int]bool
tlsActive := false
defer func() {
if activeConn != nil {
activeConn.Close()
}
}()
sendResponse(conn, "+OK MailGo POP3 server ready")
for {
@@ -163,6 +174,7 @@ func (s *POP3Server) handleConn(conn net.Conn, port int) {
parts := strings.SplitN(line, " ", 2)
cmd := strings.ToUpper(parts[0])
commandCount[cmd]++
activeConn.Touch()
arg := ""
if len(parts) > 1 {
arg = strings.TrimSpace(parts[1])
@@ -186,6 +198,7 @@ func (s *POP3Server) handleConn(conn net.Conn, port int) {
}
} else {
authFailReason = ""
activeConn.SetUser(authUsername)
}
case "STAT":
s.handleSTAT(conn, messages, deleted)
@@ -233,6 +246,7 @@ func (s *POP3Server) handleConn(conn net.Conn, port int) {
conn = tlsConn
reader = bufio.NewReader(conn)
tlsActive = true
activeConn.SetTLS(true)
case "TOP":
s.handleTOP(conn, arg, messages, deleted)
case "UIDL":