Files
dualfroz 87744aa7c1 fix: name the proxy schemes that work, and stop panicking on the rest
"Which proxy does this support?" has a narrower answer than the docs
gave. `golang.org/x/net/proxy.FromURL` resolves `socks5` and `socks5h`
and nothing else, yet the README and `Proxy`'s doc comment both offered
"socks5, socks4 and socks4a". A `socks4://` URI fails every dial with
`proxy: unknown scheme: socks4`, which is what taking the docs at their
word looks like from the caller's side.

Corrected both, and pointed at `proxy.RegisterDialerType` -- the way
x/net/proxy expects a caller to add a scheme it does not carry.

That escape hatch had a second problem waiting. `establishProxyConnection`
asserted the dialer to `proxy.ContextDialer` unconditionally, but a
registered dialer need only implement `Dial`, and the assertion panicked
on those:

    interface conversion: T is not proxy.ContextDialer: missing method
    DialContext

It now falls back to dialing on a goroutine and abandoning the attempt
when the context expires, which is what x/net/proxy itself does for the
same case.

Closes: #95
2026-09-13 15:47:32 +02:00

304 lines
11 KiB
Go

package emailverifier
import (
"fmt"
"net"
"time"
)
// Verifier is an email verifier. Create one by calling NewVerifier
type Verifier struct {
smtpCheckEnabled bool // SMTP check enabled or disabled (disabled by default)
catchAllCheckEnabled bool // SMTP catchAll check enabled or disabled (enabled by default)
domainSuggestEnabled bool // whether suggest a most similar correct domain or not (disabled by default)
gravatarCheckEnabled bool // gravatar check enabled or disabled (disabled by default)
fromEmail string // name to use in the `EHLO:` SMTP command, defaults to "user@example.org"
helloName string // email to use in the `MAIL FROM:` SMTP command. defaults to `localhost`
schedule *schedule // schedule represents a job schedule
proxyURI string // use a SOCKS5 proxy to verify the email,
apiVerifiers map[string]smtpAPIVerifier // per-vendor API verifiers; no built-in vendors currently, contributions are welcomed.
resolver *net.Resolver // resolver used to perform MX and SMTP DNS lookups; nil means net.DefaultResolver, read at call time
// 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
type Result struct {
Email string `json:"email"` // passed email address
Reachable string `json:"reachable"` // an enumeration to describe whether the recipient address is real
Syntax Syntax `json:"syntax"` // details about the email address syntax
SMTP *SMTP `json:"smtp"` // details about the SMTP response of the email
Gravatar *Gravatar `json:"gravatar"` // whether or not have gravatar for the email
Suggestion string `json:"suggestion"` // domain suggestion when domain is misspelled
Disposable bool `json:"disposable"` // is this a DEA (disposable email address)
RoleAccount bool `json:"role_account"` // is account a role-based account
Free bool `json:"free"` // is domain a free email domain
HasMxRecords bool `json:"has_mx_records"` // whether or not MX-Records for the domain
}
// init loads disposable_domain meta data to disposableSyncDomains which are safe for concurrent use
func init() {
for d := range disposableDomains {
disposableSyncDomains.Store(d, struct{}{})
}
}
// NewVerifier creates a new email verifier
func NewVerifier() *Verifier {
return &Verifier{
fromEmail: defaultFromEmail,
helloName: defaultHelloName,
catchAllCheckEnabled: true,
apiVerifiers: map[string]smtpAPIVerifier{},
connectTimeout: 10 * time.Second,
operationTimeout: 10 * time.Second,
}
}
// Verify performs address, misc, mx and smtp checks
func (v *Verifier) Verify(email string) (*Result, error) {
ret := Result{
Email: email,
Reachable: reachableUnknown,
}
syntax := v.ParseAddress(email)
ret.Syntax = syntax
if !syntax.Valid {
return &ret, nil
}
ret.Free = v.IsFreeDomain(syntax.Domain)
ret.RoleAccount = v.IsRoleAccount(syntax.Username)
ret.Disposable = v.IsDisposable(syntax.Domain)
if v.domainSuggestEnabled {
ret.Suggestion = v.SuggestDomain(syntax.Domain)
}
// If the domain name is disposable, mx and smtp are not checked.
if ret.Disposable {
return &ret, nil
}
mx, err := v.CheckMX(syntax.Domain)
if err != nil {
errStr := err.Error()
if insContains(errStr, "no such host") {
ret.Reachable = reachableNo
return &ret, newLookupError(ErrNoSuchHost, errStr).withCause(err)
}
return &ret, err
}
ret.HasMxRecords = mx.HasMXRecord
smtp, catchAll, err := v.checkSMTP(syntax.Domain, syntax.Username)
// Kept whether or not the check succeeded. A failed exchange still says how
// far it got, which is the difference between a host that never answered and
// one that answered and refused our sender.
ret.SMTP = smtp
if err != nil {
return &ret, err
}
ret.Reachable = v.calculateReachable(smtp, catchAll)
if v.gravatarCheckEnabled {
gravatar, err := v.CheckGravatar(email)
if err != nil {
return &ret, err
}
ret.Gravatar = gravatar
}
return &ret, nil
}
// AddDisposableDomains adds additional domains as disposable domains.
func (v *Verifier) AddDisposableDomains(domains []string) *Verifier {
for _, d := range domains {
additionalDisposableDomains.Store(d, struct{}{})
disposableSyncDomains.Store(d, struct{}{})
}
return v
}
// EnableGravatarCheck enables check gravatar,
// we don't check gravatar by default
func (v *Verifier) EnableGravatarCheck() *Verifier {
v.gravatarCheckEnabled = true
return v
}
// DisableGravatarCheck disables check gravatar,
func (v *Verifier) DisableGravatarCheck() *Verifier {
v.gravatarCheckEnabled = false
return v
}
// EnableSMTPCheck enables check email by smtp,
// for most ISPs block outgoing SMTP requests through port 25, to prevent spam,
// we don't check smtp by default
func (v *Verifier) EnableSMTPCheck() *Verifier {
v.smtpCheckEnabled = true
return v
}
// EnableAPIVerifier activates an API-based existence check for the given vendor.
// ** Please know ** that this is a tricky way (but relatively stable) to check if target vendor's email exists.
// If you use this feature in a production environment, please ensure that you have sufficient backup measures in place, as this may encounter rate limiting or other API issues.
//
// There are currently no built-in vendors: the Yahoo verifier was removed once its
// endpoint stopped existing (see https://github.com/AfterShip/email-verifier/issues/195),
// following the same fate as the Gmail verifier removed in #113. This remains an
// extension point; contributions adding a working vendor are welcome.
func (v *Verifier) EnableAPIVerifier(name string) error {
return fmt.Errorf("unsupported to enable the API verifier for vendor: %s", name)
}
func (v *Verifier) DisableAPIVerifier(name string) {
delete(v.apiVerifiers, name)
}
// DisableSMTPCheck disables check email by smtp
func (v *Verifier) DisableSMTPCheck() *Verifier {
v.smtpCheckEnabled = false
return v
}
// EnableCatchAllCheck enables the catchAll check by smtp. It is on by default;
// this undoes a previous DisableCatchAllCheck.
func (v *Verifier) EnableCatchAllCheck() *Verifier {
v.catchAllCheckEnabled = true
return v
}
// DisableCatchAllCheck disables catchAll check by smtp
func (v *Verifier) DisableCatchAllCheck() *Verifier {
v.catchAllCheckEnabled = false
return v
}
// EnableDomainSuggest will suggest a most similar correct domain when domain misspelled
func (v *Verifier) EnableDomainSuggest() *Verifier {
v.domainSuggestEnabled = true
return v
}
// DisableDomainSuggest will not suggest anything
func (v *Verifier) DisableDomainSuggest() *Verifier {
v.domainSuggestEnabled = false
return v
}
// EnableAutoUpdateDisposable enables update disposable domains automatically
func (v *Verifier) EnableAutoUpdateDisposable() *Verifier {
v.stopCurrentSchedule()
// fetch latest disposable domains before next schedule
_ = updateDisposableDomains(disposableDataURL)
// update disposable domains records daily
v.schedule = newSchedule(24*time.Hour, updateDisposableDomains, disposableDataURL)
v.schedule.start()
return v
}
// DisableAutoUpdateDisposable stops previously started schedule job
func (v *Verifier) DisableAutoUpdateDisposable() *Verifier {
v.stopCurrentSchedule()
return v
}
// FromEmail sets the emails to use in the `MAIL FROM:` smtp command
func (v *Verifier) FromEmail(email string) *Verifier {
v.fromEmail = email
return v
}
// HelloName sets the name to use in the `EHLO:` SMTP command
func (v *Verifier) HelloName(domain string) *Verifier {
v.helloName = domain
return v
}
// Proxy sets a SOCKS5 proxy to verify the email,
// proxyURI should be in the format: "socks5://user:password@127.0.0.1:1080".
//
// The scheme must be socks5 or socks5h -- the only two golang.org/x/net/proxy
// resolves. Anything else, socks4 and socks4a included, fails the dial with
// "proxy: unknown scheme"; add it with proxy.RegisterDialerType.
//
// Only the scheme, credentials and host are read; a query string is ignored, so
// a "?timeout=5s" has no effect. Use ConnectTimeout and OperationTimeout.
//
// The proxy carries the connection to the mail server, not DNS: MX lookups
// still go out from the local machine. See Resolver for routing those through
// the proxy too.
func (v *Verifier) Proxy(proxyURI string) *Verifier {
v.proxyURI = proxyURI
return v
}
// Resolver sets a custom DNS resolver to use for MX and SMTP host lookups,
// allowing users to configure custom DNS servers instead of relying on the
// system's default resolver (e.g. /etc/resolv.conf). Passing nil restores
// the default resolver (net.DefaultResolver).
func (v *Verifier) Resolver(resolver *net.Resolver) *Verifier {
v.resolver = resolver
return v
}
// dnsResolver returns the resolver to use for lookups. net.DefaultResolver is
// read here rather than captured in NewVerifier so that callers who replace the
// global keep the behaviour they had before this option existed -- the previous
// code called net.LookupMX, which reads net.DefaultResolver on every call.
func (v *Verifier) dnsResolver() *net.Resolver {
if v.resolver == nil {
return net.DefaultResolver
}
return v.resolver
}
// 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
}
// calculateReachable turns the SMTP observations into a verdict. Callers reach
// it only from Verify, which always has a username: a syntactically valid
// address cannot have an empty local part, so the address itself was always
// probed unless the catch-all probe returned first.
func (v *Verifier) calculateReachable(s *SMTP, catchAll catchAllOutcome) string {
if !v.smtpCheckEnabled {
return reachableUnknown
}
if s.Deliverable {
return reachableYes
}
// Deliberately not s.CatchAll, which is also true when the probe never ran
// or could not conclude. Only a probe that established a catch-all domain,
// or one that returned before the address was probed, leaves the address
// unverifiable; otherwise the address was refused and that is a definite no.
if catchAll == catchAllAccepted || catchAll == catchAllInconclusive {
return reachableUnknown
}
return reachableNo
}
// stopCurrentSchedule stops current running schedule (if exists)
func (v *Verifier) stopCurrentSchedule() {
if v.schedule != nil {
v.schedule.stop()
}
}