fix: 外发优先走 IPv4(Gmail 拒收无 PTR 的 IPv6);新增 smarthost 中继支持
- 出站 SMTP 连接强制 IPv4(tcp4),无 MX 回退时 IPv4 优先: 住宅/动态 IP 的 IPv6 临时地址通常无 PTR,Gmail 会以 5.7.25 拒收, 而 IPv4 一般具备正反向一致的 PTR(实测 Gmail 250 OK) - [outbound] 新增 relay_host/relay_port/relay_user/relay_password/ relay_starttls:配置后所有外部投递经智能主机中继(AUTH PLAIN、 465 隐式 TLS / 其他端口 STARTTLS),解决服务器 IP 被 Spamhaus PBL 收录时 Outlook/Hotmail 拒收的问题 - 新增 smarthost 中继单元测试
This commit is contained in:
+22
-1
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
@@ -88,6 +89,15 @@ type OutboundConfig struct {
|
||||
MaxPerMin int `toml:"max_per_min"` // 每用户每分钟最大外发数
|
||||
MaxPerDay int `toml:"max_per_day"` // 每用户每日最大外发数,0 表示禁用外部投递
|
||||
ConnectTimeout int `toml:"connect_timeout"` // 连接远程 MX 超时(秒)
|
||||
|
||||
// Smarthost relay: when relay_host is non-empty, all external mail is
|
||||
// delivered through this relay instead of direct MX delivery. Useful when
|
||||
// the server IP is listed in PBL/blocklists (residential/dynamic IPs).
|
||||
RelayHost string `toml:"relay_host"` // 中继服务器地址,留空则直投 MX
|
||||
RelayPort int `toml:"relay_port"` // 465 = 隐式 TLS,其他端口先尝试 STARTTLS
|
||||
RelayUser string `toml:"relay_user"` // 中继认证用户名(AUTH PLAIN)
|
||||
RelayPassword string `toml:"relay_password"` // 中继认证密码
|
||||
RelayStartTLS bool `toml:"relay_starttls"` // 非 465 端口是否使用 STARTTLS
|
||||
}
|
||||
|
||||
// Config is the top-level configuration structure.
|
||||
@@ -177,7 +187,9 @@ func defaultConfig() *Config {
|
||||
MaxRecipients: 50, // 单封最多 50 个外部收件人
|
||||
MaxPerMin: 30, // 每用户每分钟 30 封
|
||||
MaxPerDay: 500,
|
||||
ConnectTimeout: 30, // 连接远程 MX 超时 30 秒
|
||||
ConnectTimeout: 30, // 连接远程 MX 超时 30 秒
|
||||
RelayPort: 587, // smarthost 默认提交端口
|
||||
RelayStartTLS: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -260,6 +272,9 @@ func mergeDefaults(cfg *Config, defaults *Config) *Config {
|
||||
if cfg.Outbound.ConnectTimeout == 0 {
|
||||
cfg.Outbound.ConnectTimeout = defaults.Outbound.ConnectTimeout
|
||||
}
|
||||
if cfg.Outbound.RelayPort == 0 {
|
||||
cfg.Outbound.RelayPort = defaults.Outbound.RelayPort
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -310,6 +325,12 @@ func LoadConfig() (*Config, error) {
|
||||
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
// relay_starttls defaults to true for safety; the raw file is checked
|
||||
// because TOML decoding cannot distinguish an absent bool from false.
|
||||
if cfg.Outbound.RelayHost != "" && !strings.Contains(string(data), "relay_starttls") {
|
||||
cfg.Outbound.RelayStartTLS = defaults.Outbound.RelayStartTLS
|
||||
}
|
||||
|
||||
// Merge defaults for any missing fields
|
||||
merged := mergeDefaults(cfg, defaults)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user