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:
@@ -78,6 +78,18 @@ type BanConfig struct {
|
||||
BanDurationMin int `toml:"ban_duration_min"` // Default: 30 (minutes)
|
||||
}
|
||||
|
||||
// OutboundConfig holds outbound (external) mail delivery settings.
|
||||
type OutboundConfig struct {
|
||||
Hostname string `toml:"hostname"` // EHLO 主机名,留空使用 [smtp] domain
|
||||
PollInterval int `toml:"poll_interval"` // 队列扫描间隔(秒)
|
||||
MaxAttempts int `toml:"max_attempts"` // 单封邮件最大投递尝试次数
|
||||
RetryBaseMin int `toml:"retry_base_min"` // 重试退避基数(分钟),指数增长
|
||||
MaxRecipients int `toml:"max_recipients"` // 单封邮件最大外部收件人数
|
||||
MaxPerMin int `toml:"max_per_min"` // 每用户每分钟最大外发数
|
||||
MaxPerDay int `toml:"max_per_day"` // 每用户每日最大外发数,0 表示禁用外部投递
|
||||
ConnectTimeout int `toml:"connect_timeout"` // 连接远程 MX 超时(秒)
|
||||
}
|
||||
|
||||
// Config is the top-level configuration structure.
|
||||
type Config struct {
|
||||
Database DatabaseConfig `toml:"database"`
|
||||
@@ -88,6 +100,7 @@ type Config struct {
|
||||
POP3 POP3Config `toml:"pop3"`
|
||||
Auth AuthConfig `toml:"auth"`
|
||||
Ban BanConfig `toml:"ban"`
|
||||
Outbound OutboundConfig `toml:"outbound"`
|
||||
}
|
||||
|
||||
// isWindows returns true if the current OS is Windows.
|
||||
@@ -157,6 +170,15 @@ func defaultConfig() *Config {
|
||||
MaxFailAttempts: 5,
|
||||
BanDurationMin: 30,
|
||||
},
|
||||
Outbound: OutboundConfig{
|
||||
PollInterval: 15, // 15 秒扫描一次队列
|
||||
MaxAttempts: 12, // 最多尝试 12 次
|
||||
RetryBaseMin: 5, // 5/10/20/40/... 分钟指数退避
|
||||
MaxRecipients: 50, // 单封最多 50 个外部收件人
|
||||
MaxPerMin: 30, // 每用户每分钟 30 封
|
||||
MaxPerDay: 500,
|
||||
ConnectTimeout: 30, // 连接远程 MX 超时 30 秒
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +239,27 @@ func mergeDefaults(cfg *Config, defaults *Config) *Config {
|
||||
if cfg.Ban.BanDurationMin == 0 {
|
||||
cfg.Ban.BanDurationMin = defaults.Ban.BanDurationMin
|
||||
}
|
||||
if cfg.Outbound.PollInterval == 0 {
|
||||
cfg.Outbound.PollInterval = defaults.Outbound.PollInterval
|
||||
}
|
||||
if cfg.Outbound.MaxAttempts == 0 {
|
||||
cfg.Outbound.MaxAttempts = defaults.Outbound.MaxAttempts
|
||||
}
|
||||
if cfg.Outbound.RetryBaseMin == 0 {
|
||||
cfg.Outbound.RetryBaseMin = defaults.Outbound.RetryBaseMin
|
||||
}
|
||||
if cfg.Outbound.MaxRecipients == 0 {
|
||||
cfg.Outbound.MaxRecipients = defaults.Outbound.MaxRecipients
|
||||
}
|
||||
if cfg.Outbound.MaxPerMin == 0 {
|
||||
cfg.Outbound.MaxPerMin = defaults.Outbound.MaxPerMin
|
||||
}
|
||||
if cfg.Outbound.MaxPerDay == 0 {
|
||||
cfg.Outbound.MaxPerDay = defaults.Outbound.MaxPerDay
|
||||
}
|
||||
if cfg.Outbound.ConnectTimeout == 0 {
|
||||
cfg.Outbound.ConnectTimeout = defaults.Outbound.ConnectTimeout
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user