From 61b48c0805e5473f2564ddc486e7df41a901d7cd Mon Sep 17 00:00:00 2001 From: Elliot Alderson Date: Sun, 28 Jan 2024 14:01:46 -0600 Subject: [PATCH] improve timeout configuration --- smtp.go | 76 +++++++++++++++++++++------------------------------- smtp_test.go | 16 +++++++---- verifier.go | 18 +++++++++++++ 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/smtp.go b/smtp.go index f9f10d5..3245c5d 100644 --- a/smtp.go +++ b/smtp.go @@ -1,6 +1,7 @@ package emailverifier import ( + "context" "errors" "fmt" "math/rand" @@ -38,7 +39,7 @@ func (v *Verifier) CheckSMTP(domain, username string) (*SMTP, error) { email := fmt.Sprintf("%s@%s", username, domain) // Dial any SMTP server that will accept a connection - client, mx, err := newSMTPClient(domain, v.proxyURI) + client, mx, err := newSMTPClient(domain, v.proxyURI, v.connectTimeout, v.operationTimeout) if err != nil { return &ret, ParseSMTPError(err) } @@ -112,7 +113,7 @@ func (v *Verifier) CheckSMTP(domain, username string) (*SMTP, error) { } // newSMTPClient generates a new available SMTP client -func newSMTPClient(domain, proxyURI string) (*smtp.Client, *net.MX, error) { +func newSMTPClient(domain, proxyURI string, connectTimeout, operationTimeout time.Duration) (*smtp.Client, *net.MX, error) { domain = domainToASCII(domain) mxRecords, err := net.LookupMX(domain) if err != nil { @@ -137,7 +138,7 @@ func newSMTPClient(domain, proxyURI string) (*smtp.Client, *net.MX, error) { addr := r.Host + smtpPort index := i go func() { - c, err := dialSMTP(addr, proxyURI) + c, err := dialSMTP(addr, proxyURI, connectTimeout, operationTimeout) if err != nil { if !done { ch <- err @@ -181,48 +182,28 @@ func newSMTPClient(domain, proxyURI string) (*smtp.Client, *net.MX, error) { // dialSMTP is a timeout wrapper for smtp.Dial. It attempts to dial an // SMTP server (socks5 proxy supported) and fails with a timeout if timeout is reached while // attempting to establish a new connection -func dialSMTP(addr, proxyURI string) (*smtp.Client, error) { - // Channel holding the new smtp.Client or error - ch := make(chan interface{}, 1) - +func dialSMTP(addr, proxyURI string, connectTimeout, operationTimeout time.Duration) (*smtp.Client, error) { // Dial the new smtp connection - go func() { - var conn net.Conn - var err error - - if proxyURI != "" { - conn, err = establishProxyConnection(addr, proxyURI) - } else { - conn, err = establishConnection(addr) - } - if err != nil { - ch <- err - return - } + var conn net.Conn + var err error - host, _, _ := net.SplitHostPort(addr) - client, err := smtp.NewClient(conn, host) - if err != nil { - ch <- err - return - } - ch <- client - }() + if proxyURI != "" { + conn, err = establishProxyConnection(addr, proxyURI, connectTimeout) + } else { + conn, err = establishConnection(addr, connectTimeout) + } + if err != nil { + return nil, err + } - // Retrieve the smtp client from our client channel or timeout - select { - case res := <-ch: - switch r := res.(type) { - case *smtp.Client: - return r, nil - case error: - return nil, r - default: - return nil, errors.New("Unexpected response dialing SMTP server") - } - case <-time.After(smtpTimeout): - return nil, errors.New("Timeout connecting to mail-exchanger") + // Set specific timeouts for writing and reading + err = conn.SetDeadline(time.Now().Add(operationTimeout)) + if err != nil { + return nil, err } + + host, _, _ := net.SplitHostPort(addr) + return smtp.NewClient(conn, host) } // GenerateRandomEmail generates a random email address using the domain passed. Used @@ -237,13 +218,13 @@ func GenerateRandomEmail(domain string) string { } // establishConnection connects to the address on the named network address. -func establishConnection(addr string) (net.Conn, error) { - return net.Dial("tcp", addr) +func establishConnection(addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout("tcp", addr, timeout) } // establishProxyConnection connects to the address on the named network address // via proxy protocol -func establishProxyConnection(addr, proxyURI string) (net.Conn, error) { +func establishProxyConnection(addr, proxyURI string, timeout time.Duration) (net.Conn, error) { u, err := url.Parse(proxyURI) if err != nil { return nil, err @@ -252,5 +233,10 @@ func establishProxyConnection(addr, proxyURI string) (net.Conn, error) { if err != nil { return nil, err } - return dialer.Dial("tcp", addr) + + // https://github.com/golang/go/issues/37549#issuecomment-1178745487 + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + return dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", addr) } diff --git a/smtp_test.go b/smtp_test.go index 3e92436..f9495da 100644 --- a/smtp_test.go +++ b/smtp_test.go @@ -4,6 +4,7 @@ import ( "strings" "syscall" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -214,7 +215,8 @@ func TestCheckSMTPOK_HostNotExists(t *testing.T) { func TestNewSMTPClientOK(t *testing.T) { domain := "gmail.com" - ret, _, err := newSMTPClient(domain, "") + timeout := 5 * time.Second + ret, _, err := newSMTPClient(domain, "", timeout, timeout) assert.NotNil(t, ret) assert.Nil(t, err) } @@ -222,21 +224,24 @@ func TestNewSMTPClientOK(t *testing.T) { func TestNewSMTPClientFailed_WithInvalidProxy(t *testing.T) { domain := "gmail.com" proxyURI := "socks5://user:password@127.0.0.1:1080?timeout=5s" - ret, _, err := newSMTPClient(domain, proxyURI) + timeout := 5 * time.Second + ret, _, err := newSMTPClient(domain, proxyURI, timeout, timeout) assert.Nil(t, ret) assert.Error(t, err, syscall.ECONNREFUSED) } func TestNewSMTPClientFailed(t *testing.T) { domain := "zzzz171777.com" - ret, _, err := newSMTPClient(domain, "") + timeout := 5 * time.Second + ret, _, err := newSMTPClient(domain, "", timeout, timeout) assert.Nil(t, ret) assert.Error(t, err) } func TestDialSMTPFailed_NoPortIsConfigured(t *testing.T) { disposableDomain := "zzzz1717.com" - ret, err := dialSMTP(disposableDomain, "") + timeout := 5 * time.Second + ret, err := dialSMTP(disposableDomain, "", timeout, timeout) assert.Nil(t, ret) assert.Error(t, err) assert.True(t, strings.Contains(err.Error(), "missing port")) @@ -244,7 +249,8 @@ func TestDialSMTPFailed_NoPortIsConfigured(t *testing.T) { func TestDialSMTPFailed_NoSuchHost(t *testing.T) { disposableDomain := "zzzzyyyyaaa123.com:25" - ret, err := dialSMTP(disposableDomain, "") + timeout := 5 * time.Second + ret, err := dialSMTP(disposableDomain, "", timeout, timeout) assert.Nil(t, ret) assert.Error(t, err) assert.True(t, strings.Contains(err.Error(), "no such host")) diff --git a/verifier.go b/verifier.go index f3cb487..2ede140 100644 --- a/verifier.go +++ b/verifier.go @@ -17,6 +17,10 @@ type Verifier struct { schedule *schedule // schedule represents a job schedule proxyURI string // use a SOCKS5 proxy to verify the email, apiVerifiers map[string]smtpAPIVerifier // currently support gmail & yahoo, further contributions are welcomed. + + // Timeouts + connectTimeout time.Duration // Timeout for establishing connections + operationTimeout time.Duration // Timeout for SMTP operations (e.g., EHLO, MAIL FROM, etc.) } // Result is the result of Email Verification @@ -50,6 +54,8 @@ func NewVerifier() *Verifier { helloName: defaultHelloName, catchAllCheckEnabled: true, apiVerifiers: map[string]smtpAPIVerifier{}, + connectTimeout: 10 * time.Second, + operationTimeout: 10 * time.Second, } } @@ -223,6 +229,18 @@ func (v *Verifier) Proxy(proxyURI string) *Verifier { return v } +// ConnectTimeout sets the timeout for establishing connections. +func (v *Verifier) ConnectTimeout(timeout time.Duration) *Verifier { + v.connectTimeout = timeout + return v +} + +// OperationTimeout sets the timeout for SMTP operations (e.g., EHLO, MAIL FROM, etc.). +func (v *Verifier) OperationTimeout(timeout time.Duration) *Verifier { + v.operationTimeout = timeout + return v +} + func (v *Verifier) calculateReachable(s *SMTP) string { if !v.smtpCheckEnabled { return reachableUnknown