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)
| Behaviour | Why it matters |
|---|---|
| Caches failures | A batch of addresses on a dead domain costs one lookup, not one per address. |
| Collapses concurrent lookups | Fifty simultaneous requests for one domain make one upstream query, not fifty. |
| Honours record TTLs | NewDNSResolver exposes the TTL the system resolver hides; entries expire when the zone says they do. |
| Bounded size | A long-running service that sees many domains cannot grow the cache forever. |
| Safe reconfiguration | SetResolver 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
unknowneverywhere 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-allstatus reports. - A
4xxreply is greylisting, not a rejection. It is reported asunknown, never asinvalid.