修正邮件外域多地址连接预算并保留失败原因
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-02 14:15:18 +08:00
parent 3389f188f3
commit eba1b9d101
2 changed files with 82 additions and 8 deletions

View File

@ -2,11 +2,70 @@ package smtp
import (
"context"
"errors"
"net"
"net/netip"
"testing"
"time"
)
func TestDialResolvedAddressesFallsBackAfterTimeout(t *testing.T) {
client, server := net.Pipe()
defer client.Close()
defer server.Close()
addresses := []netip.Addr{netip.MustParseAddr("192.0.2.1"), netip.MustParseAddr("2001:db8::1")}
calls := 0
conn, err := dialResolvedAddresses(context.Background(), addresses, "25", 10*time.Millisecond, func(ctx context.Context, network, address string) (net.Conn, error) {
calls++
if network != "tcp" || address != net.JoinHostPort(addresses[calls-1].String(), "25") {
t.Fatalf("unexpected dial target: %s %s", network, address)
}
if calls == 1 {
<-ctx.Done()
return nil, ctx.Err()
}
if ctx.Err() != nil {
t.Fatal("fallback inherited exhausted attempt context")
}
return client, nil
})
if err != nil || conn != client || calls != 2 {
t.Fatalf("fallback failed: conn=%v calls=%d err=%v", conn, calls, err)
}
}
func TestDialResolvedAddressesHonorsCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
addresses := []netip.Addr{netip.MustParseAddr("192.0.2.1"), netip.MustParseAddr("192.0.2.2")}
calls := 0
_, err := dialResolvedAddresses(ctx, addresses, "25", time.Second, func(context.Context, string, string) (net.Conn, error) {
calls++
cancel()
return nil, context.Canceled
})
if calls != 1 || !errors.Is(err, context.Canceled) {
t.Fatalf("canceled parent was ignored: calls=%d err=%v", calls, err)
}
}
func TestDialResolvedAddressesRetainsFailures(t *testing.T) {
first := errors.New("first address failed")
second := errors.New("second address failed")
addresses := []netip.Addr{netip.MustParseAddr("192.0.2.1"), netip.MustParseAddr("192.0.2.2")}
calls := 0
_, err := dialResolvedAddresses(context.Background(), addresses, "25", time.Second, func(context.Context, string, string) (net.Conn, error) {
calls++
if calls == 1 {
return nil, first
}
return nil, second
})
if calls != 2 || !errors.Is(err, first) || !errors.Is(err, second) {
t.Fatalf("connection failures were lost: calls=%d err=%v", calls, err)
}
}
func TestSMTPContextCancelsGreeting(t *testing.T) {
client, server := net.Pipe()
defer server.Close()

View File

@ -62,7 +62,8 @@ func IsDeliveryUncertain(err error) bool {
}
const (
outboundSMTPDialTimeout = 2 * time.Second
outboundSMTPDialTimeout = 10 * time.Second
outboundSMTPConnectTimeout = 30 * time.Second
outboundSMTPGreetingTimeout = 5 * time.Second
outboundSMTPTLSHandshakeTimeout = 2 * time.Second
outboundSMTPTransactionTimeout = 2 * time.Minute
@ -215,9 +216,11 @@ func dialTCPContext(parent context.Context, addr string, publicOnly bool) (net.C
return nil, errors.New("smtp: invalid remote address")
}
ctx, cancel := context.WithTimeout(parent, outboundSMTPDialTimeout)
ctx, cancel := context.WithTimeout(parent, outboundSMTPConnectTimeout)
defer cancel()
addresses, err := resolveRemoteAddresses(ctx, host)
resolveCtx, resolveCancel := context.WithTimeout(ctx, outboundSMTPDialTimeout)
addresses, err := resolveRemoteAddresses(resolveCtx, host)
resolveCancel()
if err != nil {
return nil, err
}
@ -228,18 +231,30 @@ func dialTCPContext(parent context.Context, addr string, publicOnly bool) (net.C
}
dialer := &net.Dialer{Timeout: outboundSMTPDialTimeout, KeepAlive: 30 * time.Second}
var lastErr error
return dialResolvedAddresses(ctx, addresses, port, outboundSMTPDialTimeout, dialer.DialContext)
}
// 为各地址分配独立连接预算,同时遵守父级总时限,避免首个地址耗尽全部回退时间。
func dialResolvedAddresses(ctx context.Context, addresses []netip.Addr, port string, timeout time.Duration, dial func(context.Context, string, string) (net.Conn, error)) (net.Conn, error) {
var failures []error
for _, address := range addresses {
conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(address.String(), port))
if err := ctx.Err(); err != nil {
failures = append(failures, err)
break
}
attemptCtx, cancel := context.WithTimeout(ctx, timeout)
conn, err := dial(attemptCtx, "tcp", net.JoinHostPort(address.String(), port))
cancel()
if err == nil {
return conn, nil
}
lastErr = err
failures = append(failures, err)
}
if lastErr == nil {
if len(failures) == 0 {
return nil, errors.New("smtp: remote address is unavailable")
}
return nil, fmt.Errorf("smtp: connect to remote address: %w", lastErr)
// 保留各地址的失败原因,不能把最后一个未成功连接的地址当成唯一故障。
return nil, fmt.Errorf("smtp: connect to remote address: %w", errors.Join(failures...))
}
func resolveRemoteAddresses(ctx context.Context, host string) ([]netip.Addr, error) {