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() } }