emailx

Email address parsing, validation, and mail-domain intelligence for Go.

v0.4.0 MIT Go 1.26+ 95% covered zero-network tests

Install

go get github.com/bakhod1r/emailx       // parse, normalize, classify — zero dependencies
go get github.com/bakhod1r/emailx/dns  // MX, SPF, DMARC, DKIM, ...
go get github.com/bakhod1r/emailx/smtp // mailbox probing

Three modules, so you only take on the dependencies of the part you use. The core module imports nothing outside the standard library and never opens a network connection; the examples below import github.com/bakhod1r/emailx/dns as dnsx and github.com/bakhod1r/emailx/smtp as smtpx.

e, err := emailx.Parse("John.Doe+news@Gmail.com")

e.BaseLocalPart()  // "John.Doe"
e.PlusTag()        // "news"
e.Provider().Name  // "Gmail"
e.IsDisposable()   // false

e.Normalize()
e.Address()        // "johndoe@gmail.com"

Normalization is provider-aware, so two addresses that reach the same mailbox compare equal with EqualNormalized. Gmail ignores dots and +tags; Yahoo uses -; an unknown provider gets case folding only, because stripping dots there would merge two genuinely separate mailboxes.

A domain can publish SPF, DKIM and DMARC and still be spoofable

Most libraries answer "does the record exist". That question does not distinguish a domain that blocks forged mail from one that only files reports about it.

An SPF record over the ten-lookup budget is a permerror. Receivers stop evaluating it, so the policy silently stops protecting the domain — while every "has SPF?" check still returns true.

p=none reports and blocks nothing. Nor does p=reject; pct=20, four times out of five.

A DKIM record with an empty p= is a revoked key, and t=y means failures must be ignored.

info := dnsx.LookupInfo("example.com")

info.SPF.IsStrict()       // the record ends in -all
info.SPF.Lookups          // DNS lookups the record costs
info.SPF.TooManyLookups   // over the RFC 7208 limit of 10
info.DMARC.IsEnforcing()  // p=reject or quarantine at pct=100
info.DKIM.Revoked         // empty p=
info.IsProtected()        // all of it, correctly configured

MX, SPF, DMARC and DKIM are fetched concurrently, so the call costs about one round trip. Risk and Deliverability score these parsed policies rather than booleans, and explain every deduction.

What it knows

8,201 disposable domains

The full upstream blocklist, bundled at build time. The generator refuses to shrink the table and never lets a known provider land in it.

18 DKIM selectors

DKIM offers no way to discover a selector from DNS, so the common ones for every major provider are probed in turn.

248 country TLDs

ISO 3166 intersected with the TLDs IANA actually delegates, plus the historical codes still in use.

SMTP mailbox probes

MX in priority order, RCPT TO, and a second random address to detect a catch-all server that accepts everything.

Transport security

BIMI, MTA-STS, TLS-RPT and DNSSEC alongside the authentication records.

Privacy helpers

Mask, HashSHA256, and keyed Fingerprint for comparing addresses without storing them.

Built for batches

Every network call has a context variant. The resolver is pluggable and the cache is doing real work.

dnsx.SetResolver(dnsx.NewDNSResolver("1.1.1.1:53", 2*time.Second))
dnsx.EnableCache(5 * time.Minute)
BehaviourWhy it matters
Caches failuresA batch of addresses on a dead domain costs one lookup, not one per address.
Collapses concurrent lookupsFifty simultaneous requests for one domain make one upstream query, not fifty.
Honours record TTLsNewDNSResolver exposes the TTL the system resolver hides; entries expire when the zone says they do.
Bounded sizeA long-running service that sees many domains cannot grow the cache forever.
Safe reconfigurationSetResolver and SetDefaultTimeout may be called while lookups are in flight.

Command line

go install github.com/bakhod1r/emailx/cmd/emailx@latest
$ emailx domain google.com

Domain: google.com

MX:        yes (smtp.google.com.)
SPF:       yes (~all, 1 lookup)
DMARC:     yes (p=reject, pct=100, enforcing)
DKIM:      no key found on the common selectors
Protected: no
DNSSEC: no  BIMI: no  MTA-STS: yes  TLS-RPT: yes

Five commands — analyze, validate, domain, verify, batch — each taking -json, -timeout, -dns and -cache. See the CLI reference.

Before you trust an SMTP probe

Mailbox verification is the least reliable signal here, and the documentation says so rather than selling it.

  • Most cloud providers block outbound port 25. You will get unknown everywhere unless the network allows it.
  • Unthrottled probing gets the sending IP blocklisted. Pass an smtpx.Options.Limiter; the package does not throttle on its own.
  • Many large providers answer every probe identically by design, which is what the catch-all status reports.
  • A 4xx reply is greylisting, not a rejection. It is reported as unknown, never as invalid.