Skip to content

Commit

Permalink
improve timeout configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
eos175 committed Jan 28, 2024
1 parent 833d301 commit 61b48c0
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 50 deletions.
76 changes: 31 additions & 45 deletions smtp.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package emailverifier

import (
"context"
"errors"
"fmt"
"math/rand"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
16 changes: 11 additions & 5 deletions smtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"
"syscall"
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -214,37 +215,42 @@ 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)
}

func TestNewSMTPClientFailed_WithInvalidProxy(t *testing.T) {
domain := "gmail.com"
proxyURI := "socks5://user:[email protected]: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"))
}

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"))
Expand Down
18 changes: 18 additions & 0 deletions verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,6 +54,8 @@ func NewVerifier() *Verifier {
helloName: defaultHelloName,
catchAllCheckEnabled: true,
apiVerifiers: map[string]smtpAPIVerifier{},
connectTimeout: 10 * time.Second,
operationTimeout: 10 * time.Second,
}
}

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 61b48c0

Please sign in to comment.