2020-12-21 15:43:22 +08:00
|
|
|
package emailverifier
|
|
|
|
|
|
|
|
import (
|
2024-01-28 14:01:46 -06:00
|
|
|
"context"
|
2020-12-21 15:43:22 +08:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"math/rand"
|
|
|
|
"net"
|
|
|
|
"net/smtp"
|
2022-11-22 22:06:41 +02:00
|
|
|
"net/url"
|
2023-06-19 17:29:57 +08:00
|
|
|
"strings"
|
2020-12-21 15:43:22 +08:00
|
|
|
"sync"
|
|
|
|
"time"
|
2021-09-22 22:39:46 +08:00
|
|
|
|
2022-11-22 22:06:41 +02:00
|
|
|
"golang.org/x/net/proxy"
|
2020-12-21 15:43:22 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
// SMTP stores all information for SMTP verification lookup
|
|
|
|
type SMTP struct {
|
|
|
|
HostExists bool `json:"host_exists"` // is the host exists?
|
|
|
|
FullInbox bool `json:"full_inbox"` // is the email account's inbox full?
|
|
|
|
CatchAll bool `json:"catch_all"` // does the domain have a catch-all email address?
|
|
|
|
Deliverable bool `json:"deliverable"` // can send an email to the email server?
|
|
|
|
Disabled bool `json:"disabled"` // is the email blocked or disabled by the provider?
|
|
|
|
}
|
|
|
|
|
|
|
|
// CheckSMTP performs an email verification on the passed domain via SMTP
|
2023-02-27 17:28:38 +01:00
|
|
|
// - the domain is the passed email domain
|
|
|
|
// - username is used to check the deliverability of specific email address,
|
|
|
|
//
|
2021-04-07 13:50:28 +08:00
|
|
|
// if server is catch-all server, username will not be checked
|
2020-12-21 15:43:22 +08:00
|
|
|
func (v *Verifier) CheckSMTP(domain, username string) (*SMTP, error) {
|
|
|
|
if !v.smtpCheckEnabled {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret SMTP
|
2023-06-19 17:29:57 +08:00
|
|
|
var err error
|
|
|
|
email := fmt.Sprintf("%s@%s", username, domain)
|
2020-12-21 15:43:22 +08:00
|
|
|
|
|
|
|
// Dial any SMTP server that will accept a connection
|
2024-01-28 14:01:46 -06:00
|
|
|
client, mx, err := newSMTPClient(domain, v.proxyURI, v.connectTimeout, v.operationTimeout)
|
2020-12-21 15:43:22 +08:00
|
|
|
if err != nil {
|
|
|
|
return &ret, ParseSMTPError(err)
|
|
|
|
}
|
|
|
|
|
2023-06-19 17:29:57 +08:00
|
|
|
// Defer quit the SMTP connection
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
// Check by api when enabled and host recognized.
|
|
|
|
for _, apiVerifier := range v.apiVerifiers {
|
|
|
|
if apiVerifier.isSupported(strings.ToLower(mx.Host)) {
|
|
|
|
return apiVerifier.check(domain, username)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-21 15:43:22 +08:00
|
|
|
// Sets the HELO/EHLO hostname
|
2023-06-19 17:29:57 +08:00
|
|
|
if err = client.Hello(v.helloName); err != nil {
|
2020-12-21 15:43:22 +08:00
|
|
|
return &ret, ParseSMTPError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sets the from email
|
2023-06-19 17:29:57 +08:00
|
|
|
if err = client.Mail(v.fromEmail); err != nil {
|
2020-12-21 15:43:22 +08:00
|
|
|
return &ret, ParseSMTPError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Host exists if we've successfully formed a connection
|
|
|
|
ret.HostExists = true
|
|
|
|
|
|
|
|
// Default sets catch-all to true
|
|
|
|
ret.CatchAll = true
|
|
|
|
|
2023-02-27 17:28:38 +01:00
|
|
|
if v.catchAllCheckEnabled {
|
|
|
|
// Checks the deliver ability of a randomly generated address in
|
|
|
|
// order to verify the existence of a catch-all and etc.
|
|
|
|
randomEmail := GenerateRandomEmail(domain)
|
2023-06-19 17:29:57 +08:00
|
|
|
if err = client.Rcpt(randomEmail); err != nil {
|
2023-02-27 17:28:38 +01:00
|
|
|
if e := ParseSMTPError(err); e != nil {
|
|
|
|
switch e.Message {
|
|
|
|
case ErrFullInbox:
|
|
|
|
ret.FullInbox = true
|
|
|
|
case ErrNotAllowed:
|
|
|
|
ret.Disabled = true
|
|
|
|
// If The client typically receives a `550 5.1.1` code as a reply to RCPT TO command,
|
|
|
|
// In most cases, this is because the recipient address does not exist.
|
|
|
|
case ErrServerUnavailable:
|
|
|
|
ret.CatchAll = false
|
|
|
|
default:
|
|
|
|
|
|
|
|
}
|
2020-12-21 15:43:22 +08:00
|
|
|
|
|
|
|
}
|
2023-02-27 17:28:38 +01:00
|
|
|
}
|
2020-12-21 15:43:22 +08:00
|
|
|
|
2023-02-27 17:28:38 +01:00
|
|
|
// If the email server is a catch-all email server,
|
|
|
|
// no need to calibrate deliverable on a specific user
|
|
|
|
if ret.CatchAll {
|
|
|
|
return &ret, nil
|
2020-12-21 15:43:22 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-27 17:28:38 +01:00
|
|
|
// If no username provided,
|
2020-12-21 15:43:22 +08:00
|
|
|
// no need to calibrate deliverable on a specific user
|
2023-02-27 17:28:38 +01:00
|
|
|
if username == "" {
|
2020-12-21 15:43:22 +08:00
|
|
|
return &ret, nil
|
|
|
|
}
|
|
|
|
|
2023-06-19 17:29:57 +08:00
|
|
|
if err = client.Rcpt(email); err == nil {
|
2020-12-21 15:43:22 +08:00
|
|
|
ret.Deliverable = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ret, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// newSMTPClient generates a new available SMTP client
|
2024-01-28 14:01:46 -06:00
|
|
|
func newSMTPClient(domain, proxyURI string, connectTimeout, operationTimeout time.Duration) (*smtp.Client, *net.MX, error) {
|
2020-12-21 15:43:22 +08:00
|
|
|
domain = domainToASCII(domain)
|
|
|
|
mxRecords, err := net.LookupMX(domain)
|
|
|
|
if err != nil {
|
2023-06-19 17:29:57 +08:00
|
|
|
return nil, nil, err
|
2020-12-21 15:43:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(mxRecords) == 0 {
|
2023-06-19 17:29:57 +08:00
|
|
|
return nil, nil, errors.New("No MX records found")
|
2020-12-21 15:43:22 +08:00
|
|
|
}
|
|
|
|
// Create a channel for receiving response from
|
|
|
|
ch := make(chan interface{}, 1)
|
2023-06-19 17:29:57 +08:00
|
|
|
selectedMXCh := make(chan *net.MX, 1)
|
2020-12-21 15:43:22 +08:00
|
|
|
|
|
|
|
// Done indicates if we're still waiting on dial responses
|
|
|
|
var done bool
|
|
|
|
|
|
|
|
// mutex for data race
|
|
|
|
var mutex sync.Mutex
|
|
|
|
|
|
|
|
// Attempt to connect to all SMTP servers concurrently
|
2023-06-19 17:29:57 +08:00
|
|
|
for i, r := range mxRecords {
|
2020-12-21 15:43:22 +08:00
|
|
|
addr := r.Host + smtpPort
|
2023-06-19 17:29:57 +08:00
|
|
|
index := i
|
2020-12-21 15:43:22 +08:00
|
|
|
go func() {
|
2024-01-28 14:01:46 -06:00
|
|
|
c, err := dialSMTP(addr, proxyURI, connectTimeout, operationTimeout)
|
2020-12-21 15:43:22 +08:00
|
|
|
if err != nil {
|
|
|
|
if !done {
|
|
|
|
ch <- err
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Place the client on the channel or close it
|
|
|
|
mutex.Lock()
|
|
|
|
switch {
|
|
|
|
case !done:
|
|
|
|
done = true
|
|
|
|
ch <- c
|
2023-06-19 17:29:57 +08:00
|
|
|
selectedMXCh <- mxRecords[index]
|
2020-12-21 15:43:22 +08:00
|
|
|
default:
|
|
|
|
c.Close()
|
|
|
|
}
|
|
|
|
mutex.Unlock()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect errors or return a client
|
|
|
|
var errs []error
|
|
|
|
for {
|
|
|
|
res := <-ch
|
|
|
|
switch r := res.(type) {
|
|
|
|
case *smtp.Client:
|
2023-06-19 17:29:57 +08:00
|
|
|
return r, <-selectedMXCh, nil
|
2020-12-21 15:43:22 +08:00
|
|
|
case error:
|
|
|
|
errs = append(errs, r)
|
|
|
|
if len(errs) == len(mxRecords) {
|
2023-06-19 17:29:57 +08:00
|
|
|
return nil, nil, errs[0]
|
2020-12-21 15:43:22 +08:00
|
|
|
}
|
|
|
|
default:
|
2023-06-19 17:29:57 +08:00
|
|
|
return nil, nil, errors.New("Unexpected response dialing SMTP server")
|
2020-12-21 15:43:22 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// dialSMTP is a timeout wrapper for smtp.Dial. It attempts to dial an
|
2021-09-22 22:39:46 +08:00
|
|
|
// SMTP server (socks5 proxy supported) and fails with a timeout if timeout is reached while
|
2020-12-21 15:43:22 +08:00
|
|
|
// attempting to establish a new connection
|
2024-01-28 14:01:46 -06:00
|
|
|
func dialSMTP(addr, proxyURI string, connectTimeout, operationTimeout time.Duration) (*smtp.Client, error) {
|
2020-12-21 15:43:22 +08:00
|
|
|
// Dial the new smtp connection
|
2024-01-28 14:01:46 -06:00
|
|
|
var conn net.Conn
|
|
|
|
var err error
|
2021-09-22 22:39:46 +08:00
|
|
|
|
2024-01-28 14:01:46 -06:00
|
|
|
if proxyURI != "" {
|
|
|
|
conn, err = establishProxyConnection(addr, proxyURI, connectTimeout)
|
|
|
|
} else {
|
|
|
|
conn, err = establishConnection(addr, connectTimeout)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-12-21 15:43:22 +08:00
|
|
|
|
2024-01-28 14:01:46 -06:00
|
|
|
// Set specific timeouts for writing and reading
|
|
|
|
err = conn.SetDeadline(time.Now().Add(operationTimeout))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2020-12-21 15:43:22 +08:00
|
|
|
}
|
2024-01-28 14:01:46 -06:00
|
|
|
|
|
|
|
host, _, _ := net.SplitHostPort(addr)
|
|
|
|
return smtp.NewClient(conn, host)
|
2020-12-21 15:43:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// GenerateRandomEmail generates a random email address using the domain passed. Used
|
|
|
|
// primarily for checking the existence of a catch-all address
|
|
|
|
func GenerateRandomEmail(domain string) string {
|
|
|
|
r := make([]byte, 32)
|
|
|
|
for i := 0; i < 32; i++ {
|
|
|
|
r[i] = alphanumeric[rand.Intn(len(alphanumeric))]
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%s@%s", string(r), domain)
|
|
|
|
|
|
|
|
}
|
2021-09-22 22:39:46 +08:00
|
|
|
|
|
|
|
// establishConnection connects to the address on the named network address.
|
2024-01-28 14:01:46 -06:00
|
|
|
func establishConnection(addr string, timeout time.Duration) (net.Conn, error) {
|
|
|
|
return net.DialTimeout("tcp", addr, timeout)
|
2021-09-22 22:39:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// establishProxyConnection connects to the address on the named network address
|
|
|
|
// via proxy protocol
|
2024-01-28 14:01:46 -06:00
|
|
|
func establishProxyConnection(addr, proxyURI string, timeout time.Duration) (net.Conn, error) {
|
2022-11-22 22:06:41 +02:00
|
|
|
u, err := url.Parse(proxyURI)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
dialer, err := proxy.FromURL(u, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-01-28 14:01:46 -06:00
|
|
|
|
|
|
|
// 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)
|
2021-09-22 22:39:46 +08:00
|
|
|
}
|