API reference

Grouped by task rather than alphabetically. For full signatures and doc comments, see pkg.go.dev.

The project is three modules. Unqualified names below live in the core module github.com/bakhod1r/emailx, which has no dependencies and never opens a network connection. Names written dnsx.… live in github.com/bakhod1r/emailx/dns and smtpx.… in github.com/bakhod1r/emailx/smtp; those two are where all networking lives.

Every function that touches the network has a …Context variant taking a context.Context as its first argument. Only the plain form is listed below; assume the context form exists.

Parsing

FunctionReturns
Parse(input)*Email, error — plain or header-style
ParseAddress(input)*Email, error"Name <a@b.com>"
ParseMany(inputs)[]*Email, skipping failures
Normalize(input)string, error — parse and normalize
NormalizeMany(inputs)[]string
Unique(inputs)[]string, deduplicated by normalized form

Inspecting an address

MethodReturns
e.Address()the full address, normalized if Normalize ran
e.String()same as Address, falling back to the raw input
e.LocalPart()everything before @
e.BaseLocalPart()local part without the plus tag
e.PlusTag() / e.HasPlusTag()the sub-address tag
e.HasDot()the local part contains a dot
e.Domain()the domain, always lower-case
e.DomainName()domain without the TLD
e.TLD() / e.Subdomain()domain parts
e.DomainASCII() / e.DomainUnicode()IDN conversion
e.Normalize()rewrites in place, provider-aware
e.Equal(other)normalized comparison
e.EqualNormalized(other)same mailbox
e.EqualExact(other)byte-for-byte

Classification

CallReturns
e.IsDisposable()domain is in the throwaway blocklist
IsDisposableDomain(domain)the same check without parsing
DisposableDomainCount()size of the bundled list
DisposableDomains()[]string — the whole list, as a fresh copy
AllDisposableDomains()iter.Seq[string] — the same, without copying
e.DisposableProvider()name derived from the domain
e.IsRole()local part is a function, not a person
e.IsRoleWithPrefixes(…)against your own prefix list
e.Provider() / e.ProviderID()*Provider{ID, Name, Domains, Free}
e.IsFree()a known free consumer provider
Providers()[]*Provider — one per provider, sorted by ID, Domains filled
AllProviders()iter.Seq[*Provider] — the same, without copying
ProviderByID(id) / ProviderForDomain(domain)*Provider, or nil
ProviderDomains()every mapped domain, sorted
ProviderCount() / ProviderDomainCount()table sizes
e.Country()country name for the ccTLD
e.Suggestion()corrected address for a typo domain, else ""
e.DomainSimilarity(target)float64, 0 to 1

Validation

CallReturns
IsValid(input)bool, parse and check in one step
e.IsPossible()cheap structural check
e.IsValid()syntax check — ValidateSyntax without the reason
e.ValidateSyntax()error naming the problem
IsValidSMTPUTF8(input) / e.IsValidSMTPUTF8()the same, with RFC 6531 internationalized addresses allowed
e.ValidateSyntaxSMTPUTF8()error, the SMTPUTF8 form
e.ValidateDomain()error if the domain has no TLD
e.Validate()ValidationResult — every offline check; the DNS fields stay false
ValidateMany(inputs, opts…)map[string]ValidationResult, offline
dnsx.Validate(e)the same result with MXValid, SPFValid, DMARCValid filled in
dnsx.ValidateMany(inputs, concurrency)the batch form
dnsx.Analyze(input)*Profile, error — everything, one round of lookups

What counts as valid. The accepted grammar is RFC 5322 dot-atom on both sides of the @: atext characters in dot-separated atoms, and a domain of letter-digit-hyphen labels, within the RFC 5321 limits of 64 octets for the local part and 253 for the domain. IsValid, ValidateSyntax and Validate all run the same check and cannot disagree.

Quoted local parts ("a b"@example.com) and address literals (user@[192.0.2.1]) are valid per RFC 5321 but are not accepted, because most receiving systems reject them too. Inside dot-atom the rules are strict, and in places stricter than net/mail: a label may not start or end with a hyphen, and a local part may not begin with, end with, or contain consecutive dots.

The plain form is ASCII-only; the …SMTPUTF8 variants allow a non-ASCII local part and a U-label domain.

DNS records

CallReturns
dnsx.LookupInfo(domain, selectors…)dnsx.Info — MX, SPF, DMARC, DKIM, concurrently
dnsx.LookupSPF(domain)*SPF, following includes and redirects
dnsx.LookupDMARC(domain)*DMARC
dnsx.LookupDKIM(domain, selectors…)*DKIM, probing common selectors
dnsx.LookupDKIMSelector(domain, selector)*DKIM for exactly one selector
dnsx.LookupMX(domain)[]MXRecord, error
dnsx.HasMX, HasSPF, HasDMARC, HasDKIMbool — presence only
dnsx.HasBIMI(domain)*BIMIInfo{Enabled, LogoURL, VMC}
dnsx.HasMTASTS(domain)*MTASTSInfo{Enabled, Raw}
dnsx.HasTLSRPT(domain)*TLSRPTInfo{Enabled, RUAs}
dnsx.HasDNSSEC(domain)bool — DNSKEY records exist
dnsx.CheckDomainHealth(domain, selectors…)*DomainHealth — all of the above at once

These all live in github.com/bakhod1r/emailx/dns, imported as dnsx throughout this page. To apply them to a parsed address, pass e.Domain(). Every call has a …Context variant.

Record types

dnsx.Info

Field or methodMeaning
Domain, MX, MXErrthe domain, its MX records, and the lookup error
SPF, DMARC, DKIMthe parsed records
HasMX()any MX record was found
IsProtected()-all, enforcing DMARC, and a live DKIM key

SPF

Field or methodMeaning
Found, Raw, Versionthe record as published
Allqualifier on the terminating all
IsStrict()All == "-"
Mechanisms[]SPFMechanism{Qualifier, Name, Value}
Modifiersredirect, exp, unknown name=value
Includesevery domain reached, in resolution order
Lookups, TooManyLookupscost against the RFC 7208 limit of 10
Errorsloops, missing records, malformed terms

Qualifiers: SPFPass +, SPFFail -, SPFSoftFail ~, SPFNeutral ?.

DMARC

Field or methodMeaning
Policy, SubdomainPolicyDMARCNone, DMARCQuarantine, DMARCReject
Percentpct=, defaulting to 100
RUA, RUFaggregate and forensic report destinations
SPFAlignment, DKIMAlignmentAlignmentRelaxed or AlignmentStrict
Tagsevery tag as parsed, including unknown ones
IsEnforcing()reject or quarantine, applied to all mail

DKIM

FieldMeaning
Found, Selector, Rawwhich selector answered
PublicKeyp=, whitespace stripped
Revokedp= is empty — the key was withdrawn
Testingt=y — receivers must ignore failures
KeyType, HashAlgos, Flagsk=, h=, t=

CommonDKIMSelectors holds the 18 selectors probed by default.

Risk and deliverability

Call or fieldMeaning
dnsx.CheckRisk(e)*Risk{Score, Level, Signals}
RiskLow, RiskMedium, RiskHighthe levels
Signal{Impact, Description}one entry per contributing factor
dnsx.CheckDeliverability(domain)*Deliverability
d.Score0–100
d.SPFStrict, d.SPFBroken-all; over the lookup limit
d.DMARCEnforcingthe policy actually blocks
d.Reasonsone line per deduction
Profileeverything dnsx.Analyze returns
DomainHealth.Protected()the setup works, not merely exists

SMTP

Call or fieldMeaning
smtpx.Verify(address, opts…)*smtpx.Result
smtpx.VerifyContext(ctx, address, opts…)the same, bounded by a context
smtpx.StatusValidthe server accepted RCPT TO
smtpx.StatusInvalidpermanent 5xx rejection
smtpx.StatusCatchAllevery address is accepted, so this proves nothing
smtpx.StatusUnknownno MX, unreachable, or a 4xx refusal
smtpx.OptionsHelloName, FromAddress, Timeout, Port, SkipCatchAllCheck, Limiter
smtpx.ResultStatus, MXHost, Code, Message, CatchAll, Greylisted, STARTTLS, Duration, Err

Resolvers, cache, limits

CallMeaning
dnsx.SetResolver(r) / CurrentResolver()install or read the active resolver; nil restores the system one
dnsx.NewResolver(addr, timeout)a specific DNS server via the system machinery
dnsx.NewDNSResolver(addr, timeout)direct queries; reports TTLs, rejoins chunked TXT
ResolverLookupMX, LookupTXT — implement it to plug in your own
TTLResolveradds LookupMXTTL, LookupTXTTTL
EnableCache(ttl)wrap the active resolver in a cache
NewCachingResolver(next, ttl)build one without installing it
cache.SetMaxSize(n), Len(), Purge()cache control
DefaultCacheSize, MinCacheTTL10,000 entries; a 30s TTL floor
SetDefaultTimeout(d) / DefaultTimeout()deadline for the non-context API
smtpx.NewRateLimiter(perSecond, burst)token bucket satisfying smtpx.Limiter
smtpx.LimiterWait(ctx) error

Batches and collections

CallMeaning
ValidateMany(inputs, WithConcurrency(n))concurrent validation, offline
dnsx.ValidateMany(inputs, n)the same plus MX, SPF and DMARC
NewSet()*EmailSetAdd, Len, ByDomain
NewIndex()*EmailIndexAdd, Exists, FindByDomain, FindByProvider

Privacy

CallMeaning
e.Mask(opts…)"j******e@example.com"
e.HashSHA256() / e.HashSHA512()hash of the normalized address
e.Fingerprint(WithSecret(s))keyed, so it resists dictionary reversal

Email also implements MarshalJSON, UnmarshalJSON, MarshalText, UnmarshalText, Scan and Value, so it works directly in JSON structs and database columns.

Errors

ErrEmpty, ErrInvalidSyntax, ErrInvalidLocalPart, ErrInvalidDomain, ErrInvalidTLD, ErrLocalPartTooLong, ErrDomainTooLong. ParseError carries an ErrorCode for programmatic handling.