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) }