Upstream AfterShip/email-verifier state of the touched files before 072e335125
This commit is contained in:
commit
74c6cde98a
4 files changed
+1273
No files matched your search
+300
@@ -0,0 +1,300 @@
|
||||
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 protocol could be socks5, socks4 and socks4a.
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user