feat: 实现对外邮件投递(外发队列 + MX 直投 + DKIM + 退信 + 管理后台)
- 新增 internal/outbound 模块:MX 查询、SMTP 出站客户端(EHLO/STARTTLS/ MAIL/RCPT/DATA/QUIT)、4xx 临时失败与 5xx 永久失败分类、8BITMIME 支持 - 新增 outbound_messages 队列表与 OutboundStore,后台 worker 指数退避重试 - 永久失败/超限退信到发件人收件箱,包含原因与目标收件人 - 外发邮件使用域名 DKIM 私钥签名(go-msgauth) - SMTP 提交集成:认证用户可发外部收件人,MAIL FROM 必须等于登录用户邮箱, 未认证外部投递明确拒绝(防开放中继) - Web 发信集成:外部收件人自动入队,附件以 multipart/mixed + base64 编码 加入邮件正文 - 每用户每分钟/每日发送限速(max_per_day=0 可禁用外部投递) - 管理后台新增外发队列页面:状态统计、失败原因、手动重试/取消 - 新增 [outbound] 配置段并更新 README / todo.md
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"mail_go/config"
|
||||
"mail_go/internal/mailutil"
|
||||
"mail_go/internal/outbound"
|
||||
"mail_go/internal/storage"
|
||||
"mail_go/internal/store"
|
||||
"mail_go/internal/web/handlers"
|
||||
@@ -44,6 +45,7 @@ type WebServer struct {
|
||||
storageCfg config.StorageConfig
|
||||
authCfg config.AuthConfig
|
||||
banCfg config.BanConfig
|
||||
outbound *outbound.Manager
|
||||
}
|
||||
|
||||
// templateFuncs returns custom template functions for rendering.
|
||||
@@ -82,7 +84,7 @@ func templateFuncs() template.FuncMap {
|
||||
|
||||
// NewWebServer creates a new WebServer, initializes the Gin engine,
|
||||
// configures sessions, middleware, and registers all routes.
|
||||
func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storage.AttachmentStorage, storageCfg config.StorageConfig, authCfg config.AuthConfig, banCfg config.BanConfig) *WebServer {
|
||||
func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storage.AttachmentStorage, storageCfg config.StorageConfig, authCfg config.AuthConfig, banCfg config.BanConfig, ob *outbound.Manager) *WebServer {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Logger())
|
||||
@@ -112,6 +114,7 @@ func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storag
|
||||
storageCfg: storageCfg,
|
||||
authCfg: authCfg,
|
||||
banCfg: banCfg,
|
||||
outbound: ob,
|
||||
}
|
||||
|
||||
ws.registerRoutes()
|
||||
@@ -121,8 +124,8 @@ func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storag
|
||||
// registerRoutes sets up all HTTP routes with their handlers and middleware.
|
||||
func (ws *WebServer) registerRoutes() {
|
||||
authHandler := handlers.NewAuthHandler(ws.stores, ws.authCfg, ws.banCfg)
|
||||
mailHandler := handlers.NewMailHandler(ws.stores, ws.storage)
|
||||
adminHandler := handlers.NewAdminHandler(ws.stores, ws.storage, filepath.Join(ws.storageCfg.BaseDir, "tls", "domains"))
|
||||
mailHandler := handlers.NewMailHandler(ws.stores, ws.storage, ws.outbound)
|
||||
adminHandler := handlers.NewAdminHandler(ws.stores, ws.storage, filepath.Join(ws.storageCfg.BaseDir, "tls", "domains"), ws.outbound)
|
||||
|
||||
// Apply BanMiddleware globally before public routes
|
||||
ws.engine.Use(middleware.BanMiddleware(ws.stores))
|
||||
@@ -182,6 +185,9 @@ func (ws *WebServer) registerRoutes() {
|
||||
admin.GET("/mails", adminHandler.ListMails)
|
||||
admin.GET("/mails/:id", adminHandler.AdminViewMail)
|
||||
admin.GET("/attachment/:id", adminHandler.AdminDownloadAttachment)
|
||||
admin.GET("/outbound", adminHandler.ListOutbound)
|
||||
admin.POST("/outbound/:id/retry", adminHandler.RetryOutbound)
|
||||
admin.POST("/outbound/:id/cancel", adminHandler.CancelOutbound)
|
||||
admin.GET("/bans", adminHandler.ListBans)
|
||||
admin.POST("/bans/:id/unban", adminHandler.UnbanIP)
|
||||
admin.POST("/bans/cleanup", adminHandler.CleanupBans)
|
||||
|
||||
Reference in New Issue
Block a user