Smail/backend/utils/send/routing.go
smail bf1e69f480
Some checks are pending
Security Checks / security (push) Waiting to run
修复生产审查凭据泄露风险并增加持久投递队列
2026-09-17 10:17:09 +08:00

94 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package send
import (
stdcontext "context"
"errors"
"fmt"
"net"
"net/textproto"
"sort"
"strings"
"time"
"git.pcux.cn/xuans/Smail/utils/smtp"
)
var errNullMX = errors.New("收件域名声明不接收邮件")
type mxLookup func(stdcontext.Context, string) ([]*net.MX, error)
func resolveMX(domain string, lookup mxLookup) ([]string, error) {
ctx, cancel := stdcontext.WithTimeout(stdcontext.Background(), 10*time.Second)
defer cancel()
records, err := lookup(ctx, domain)
if err != nil {
var dnsErr *net.DNSError
if !errors.As(err, &dnsErr) || !dnsErr.IsNotFound || dnsErr.IsTemporary || dnsErr.IsTimeout {
return nil, fmt.Errorf("查询收件域名 MX 失败: %w", err)
}
// 无 MX 时使用域名自身的地址记录,不猜测 smtp 子域名。
return []string{domain}, nil
}
if len(records) == 0 {
return []string{domain}, nil
}
records = append([]*net.MX(nil), records...)
for _, record := range records {
if record == nil || record.Host == "" {
return nil, errors.New("收件域名 MX 记录无效")
}
if record.Host == "." {
return nil, errNullMX
}
}
sort.SliceStable(records, func(i, j int) bool { return records[i].Pref < records[j].Pref })
hosts := make([]string, 0, len(records))
seen := make(map[string]bool)
for _, record := range records {
host := strings.ToLower(strings.TrimSuffix(record.Host, "."))
if !seen[host] {
hosts = append(hosts, host)
seen[host] = true
}
}
return hosts, nil
}
func stopFallback(err error) bool {
return smtp.IsDeliveryUncertain(err) || isCertificateVerificationError(err) || isPermanentSMTPResponse(err) || errors.Is(err, smtp.ErrUnsafeRemoteAddress)
}
// deliverMX 只在确认本次未接收时切换安全通道,DATA 不确定结果不进入下一条路由。
func deliverMX(hosts []string, attempt func(host, port string) error) error {
var lastErr error
for _, host := range hosts {
for _, port := range []string{"25", "587", "465"} {
lastErr = attempt(host, port)
if lastErr == nil || stopFallback(lastErr) {
return lastErr
}
}
}
if lastErr == nil {
lastErr = errors.New("收件域名没有可用邮件服务器")
}
return lastErr
}
// Retryable 仅允许有明确临时失败语义的结果进入持久队列重试。
func Retryable(err error) bool {
if err == nil || stopFallback(err) {
return false
}
var response *textproto.Error
if errors.As(err, &response) {
return response.Code >= 400 && response.Code <= 499
}
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
return dnsErr.IsTimeout || dnsErr.IsTemporary
}
var networkErr net.Error
return errors.As(err, &networkErr)
}