Documentation
¶
Overview ¶
Package emailx parses, normalizes, and classifies email addresses.
It answers the questions that can be answered from the address itself: is the syntax valid, is the domain a throwaway, is this a role account, which provider serves it, and do two addresses reach the same mailbox.
No network, no dependencies ¶
This package never opens a network connection and imports nothing outside the standard library. That is a guarantee, not an accident: the code that does touch the network lives in two separate modules, github.com/bakhod1r/emailx/dns and github.com/bakhod1r/emailx/smtp, so a program that only needs the offline half links no third-party code and takes on no transitive dependencies.
import ( "github.com/bakhod1r/emailx" // offline, zero dependencies dnsx "github.com/bakhod1r/emailx/dns" // MX, SPF, DMARC, DKIM, ... smtpx "github.com/bakhod1r/emailx/smtp" // mailbox probing )
Parsing ¶
Parse returns an *Email whose parts can be inspected and compared:
e, err := emailx.Parse("John.Doe+news@Gmail.com")
e.BaseLocalPart() // "John.Doe"
e.PlusTag() // "news"
e.Normalize()
e.Address() // "johndoe@gmail.com"
Normalization is provider-aware, so two addresses that reach the same mailbox compare equal with EqualNormalized.
What counts as valid ¶
The accepted grammar is RFC 5322 dot-atom on both sides of the "@": atext characters grouped into dot-separated atoms, and a domain of letter-digit-hyphen labels, within the RFC 5321 length limits of 64 octets for the local part and 253 for the domain.
Two forms that are valid per RFC 5321 are deliberately not accepted, because most systems that receive an address will not accept them either:
- quoted local parts — `"a b"@example.com`, `"with@at"@example.com`
- address literals — user@[192.0.2.1], user@[IPv6:2001:db8::1]
Within dot-atom the rules are enforced strictly, and more strictly than net/mail in places: a label may not start or end with a hyphen, and the local part may not begin with, end with, or contain consecutive dots.
IsValid, ValidateSyntax, and Validate all run the same check, so they can never disagree about an address. IsValid is the boolean form of ValidateSyntax, which returns the reason.
The plain form is ASCII-only. IsValidSMTPUTF8 and ValidateSyntaxSMTPUTF8 apply the same rules with RFC 6531 internationalized addresses allowed — a non-ASCII local part and a U-label domain. Delivery to those needs a server advertising the SMTPUTF8 extension.
Classification ¶
IsDisposableDomain tests a domain against the bundled throwaway-mail list. Provider reports which mail provider serves an address, if it is a known one. Validate runs every offline check at once.
Enumerating the bundled data ¶
The provider table and the disposable-domain list can be walked, not only queried, which is what a generator of test or synthetic data needs:
for p := range emailx.AllProviders() {
fmt.Println(p.ID, p.Domains) // "gmail", ["gmail.com" "googlemail.com"]
}
for d := range emailx.AllDisposableDomains() {
fmt.Println(d)
}
Providers and DisposableDomains return the same data as fresh slices when a copy is wanted; the iterator forms avoid copying several thousand entries.
Concurrency ¶
All exported functions are safe for concurrent use.
Index ¶
- Variables
- func AllDisposableDomains() iter.Seq[string]
- func AllProviders() iter.Seq[*Provider]
- func DisposableDomainCount() int
- func DisposableDomains() []string
- func IsDisposableDomain(domain string) bool
- func IsValid(input string) bool
- func IsValidSMTPUTF8(input string) bool
- func Normalize(input string) (string, error)
- func NormalizeMany(inputs []string) []string
- func ProviderCount() int
- func ProviderDomainCount() int
- func ProviderDomains() []string
- func Unique(inputs []string) []string
- func ValidateMany(inputs []string, opts ...BatchOptions) map[string]ValidationResult
- type BatchOptions
- type Email
- func (e *Email) Address() string
- func (e *Email) BaseLocalPart() string
- func (e *Email) Country() string
- func (e *Email) DisposableProvider() string
- func (e *Email) Domain() string
- func (e *Email) DomainASCII() (string, error)
- func (e *Email) DomainName() string
- func (e *Email) DomainSimilarity(target string) float64
- func (e *Email) DomainUnicode() (string, error)
- func (e *Email) Equal(other *Email) bool
- func (e *Email) EqualExact(other *Email) bool
- func (e *Email) EqualNormalized(other *Email) bool
- func (e *Email) Fingerprint(opts ...FingerprintOptions) string
- func (e *Email) HasDot() bool
- func (e *Email) HasPlusTag() bool
- func (e *Email) HashSHA256() string
- func (e *Email) HashSHA512() string
- func (e *Email) IsDisposable() bool
- func (e *Email) IsFree() bool
- func (e *Email) IsPossible() bool
- func (e *Email) IsRole() bool
- func (e *Email) IsRoleWithPrefixes(prefixes ...string) bool
- func (e *Email) IsValid() bool
- func (e *Email) IsValidSMTPUTF8() bool
- func (e *Email) LocalPart() string
- func (e Email) MarshalJSON() ([]byte, error)
- func (e Email) MarshalText() ([]byte, error)
- func (e *Email) Mask(opts ...MaskOptions) string
- func (e *Email) Normalize()
- func (e *Email) PlusTag() string
- func (e *Email) Provider() *Provider
- func (e *Email) ProviderID() string
- func (e *Email) Scan(value interface{}) error
- func (e *Email) String() string
- func (e *Email) Subdomain() string
- func (e *Email) Suggestion() string
- func (e *Email) TLD() string
- func (e *Email) UnmarshalJSON(data []byte) error
- func (e *Email) UnmarshalText(text []byte) error
- func (e *Email) Validate() ValidationResult
- func (e *Email) ValidateDomain() error
- func (e *Email) ValidateSyntax() error
- func (e *Email) ValidateSyntaxSMTPUTF8() error
- func (e Email) Value() (driver.Value, error)
- type EmailIndex
- type EmailSet
- type ErrorCode
- type FingerprintOptions
- type MaskOptions
- type ParseError
- type Provider
- type ValidationResult
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrEmpty = errors.New("emailx: empty input") ErrInvalidSyntax = errors.New("emailx: invalid syntax") ErrInvalidLocalPart = errors.New("emailx: invalid local part") ErrInvalidDomain = errors.New("emailx: invalid domain") ErrInvalidTLD = errors.New("emailx: invalid top-level domain") ErrInvalidUnicode = errors.New("emailx: invalid unicode") ErrDomainTooLong = errors.New("emailx: domain too long") ErrLocalPartTooLong = errors.New("emailx: local part too long") )
var ErrInvalidPunycode = errors.New("emailx: invalid punycode label")
ErrInvalidPunycode reports a label that is not decodable Punycode.
Functions ¶
func AllDisposableDomains ¶ added in v0.3.0
AllDisposableDomains iterates every bundled throwaway-mail domain without allocating a copy. Returning false from the loop body stops the walk.
for d := range emailx.AllDisposableDomains() {
fmt.Println(d)
}
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
// The full list is a few thousand entries; iterating avoids copying it.
n := 0
for range emailx.AllDisposableDomains() {
n++
}
fmt.Println(n == emailx.DisposableDomainCount())
}
Output: true
func AllProviders ¶ added in v0.3.0
AllProviders iterates every known provider in ID order without allocating a slice. The values it yields are the package's own entries and must not be modified; use Providers when a mutable copy is wanted.
for p := range emailx.AllProviders() {
fmt.Println(p.ID, len(p.Domains))
}
func DisposableDomainCount ¶
func DisposableDomainCount() int
DisposableDomainCount reports how many domains are in the bundled list, which is useful for confirming the data was regenerated.
func DisposableDomains ¶ added in v0.3.0
func DisposableDomains() []string
DisposableDomains returns every bundled throwaway-mail domain, in the sorted order of the upstream list. The result is a fresh copy of a multi-thousand-entry slice; prefer AllDisposableDomains when the list is only being read.
func IsDisposableDomain ¶
IsDisposableDomain reports whether a domain belongs to a known throwaway-mail provider.
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
fmt.Println(emailx.IsDisposableDomain("mailinator.com"))
fmt.Println(emailx.IsDisposableDomain("gmail.com"))
}
Output: true false
func IsValid ¶
IsValid parses input and reports whether it is a syntactically valid address. It is the one-step form of Parse followed by (*Email).IsValid.
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
fmt.Println(emailx.IsValid("user@example.com"))
fmt.Println(emailx.IsValid("not-an-address"))
}
Output: true false
func IsValidSMTPUTF8 ¶ added in v0.4.0
IsValidSMTPUTF8 parses input and reports whether it is a syntactically valid RFC 6531 internationalized address.
func NormalizeMany ¶
NormalizeMany normalizes a slice of email strings.
func ProviderCount ¶ added in v0.3.0
func ProviderCount() int
ProviderCount reports how many distinct providers are known.
func ProviderDomainCount ¶ added in v0.3.0
func ProviderDomainCount() int
ProviderDomainCount reports how many domains the provider table maps.
func ProviderDomains ¶ added in v0.3.0
func ProviderDomains() []string
ProviderDomains returns every domain in the provider table, sorted. The result is a fresh copy.
func Unique ¶
Unique returns a slice of unique normalized email addresses from the inputs.
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
for _, addr := range emailx.Unique([]string{
"john.doe@gmail.com",
"JohnDoe@gmail.com",
"other@example.com",
}) {
fmt.Println(addr)
}
}
Output: johndoe@gmail.com other@example.com
func ValidateMany ¶
func ValidateMany(inputs []string, opts ...BatchOptions) map[string]ValidationResult
ValidateMany validates multiple emails, optionally concurrently.
Types ¶
type BatchOptions ¶
type BatchOptions struct {
Concurrency int
}
func WithConcurrency ¶
func WithConcurrency(n int) BatchOptions
type Email ¶
type Email struct {
// contains filtered or unexported fields
}
Email represents a parsed email address.
func Parse ¶
Parse parses an email string into an Email struct. It supports plain addresses and header-style addresses with angle brackets.
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
e, err := emailx.Parse("John.Doe+news@Gmail.com")
if err != nil {
panic(err)
}
fmt.Println(e.LocalPart())
fmt.Println(e.BaseLocalPart())
fmt.Println(e.PlusTag())
fmt.Println(e.Domain())
}
Output: John.Doe+news John.Doe news gmail.com
func ParseAddress ¶
ParseAddress parses a header-style address like "John Doe <john@example.com>"
func (*Email) Address ¶
Address returns the complete email address in its normalized form. It is an alias for String().
func (*Email) BaseLocalPart ¶
BaseLocalPart returns the local part without the plus tag.
func (*Email) Country ¶
Country returns the country name associated with the email's ccTLD, if any. E.g. for "user@mail.uz" it returns "Uzbekistan".
func (*Email) DisposableProvider ¶
DisposableProvider returns the provider name for a disposable address, or an empty string. The upstream list carries no brand names, so the name is derived from the domain: "mailinator.com" gives "mailinator".
func (*Email) DomainASCII ¶
DomainASCII returns the punycode (A-label) representation of the domain. See punycode.go for what this does and does not validate.
func (*Email) DomainName ¶
DomainName returns the domain name without the TLD. E.g., for "gmail.com" it returns "gmail".
func (*Email) DomainSimilarity ¶
DomainSimilarity calculates the similarity between the email's domain and another domain. Uses a basic Levenshtein distance normalized to [0, 1].
func (*Email) DomainUnicode ¶
DomainUnicode returns the unicode (U-label) representation of the domain.
func (*Email) Equal ¶
Equal checks if two emails are considered equal. This is an alias for EqualNormalized.
func (*Email) EqualExact ¶
EqualExact checks if the raw input of the two emails is exactly the same.
func (*Email) EqualNormalized ¶
EqualNormalized checks if the normalized form of the two emails is the same.
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
a, _ := emailx.Parse("john.doe@gmail.com")
b, _ := emailx.Parse("JohnDoe+shopping@gmail.com")
// Both addresses reach the same mailbox.
fmt.Println(a.EqualNormalized(b))
}
Output: true
func (*Email) Fingerprint ¶
func (e *Email) Fingerprint(opts ...FingerprintOptions) string
Fingerprint returns a unique identifier for the email, useful for database deduplication.
func (*Email) HasPlusTag ¶
HasPlusTag returns true if the local part has a plus tag.
func (*Email) HashSHA256 ¶
HashSHA256 returns the SHA256 hash of the normalized email.
func (*Email) HashSHA512 ¶
HashSHA512 returns the SHA512 hash of the normalized email.
func (*Email) IsDisposable ¶
IsDisposable returns true if the email domain is a known disposable provider.
func (*Email) IsPossible ¶
IsPossible does a fast check on the email.
func (*Email) IsRole ¶
IsRole returns true if the email local part represents a common role account.
func (*Email) IsRoleWithPrefixes ¶
IsRoleWithPrefixes checks against a custom list of prefixes.
func (*Email) IsValid ¶
IsValid reports whether the address is syntactically valid: RFC 5322 dot-atom on both sides, within the RFC 5321 length limits. It is exactly ValidateSyntax without the reason, so the two can never disagree.
The local part must be ASCII. Use IsValidSMTPUTF8 for RFC 6531 addresses.
func (*Email) IsValidSMTPUTF8 ¶ added in v0.4.0
IsValidSMTPUTF8 is IsValid with RFC 6531 internationalized addresses allowed: the local part may contain non-ASCII characters and the domain may be a U-label. Sending to such an address needs a server advertising the SMTPUTF8 extension.
func (Email) MarshalJSON ¶
MarshalJSON implements the json.Marshaler interface.
func (Email) MarshalText ¶
MarshalText implements encoding.TextMarshaler.
func (*Email) Mask ¶
func (e *Email) Mask(opts ...MaskOptions) string
Mask masks the email address for privacy.
func (*Email) Normalize ¶
func (e *Email) Normalize()
Normalize normalizes the email address in-place.
The domain is lower-cased, and so is the local part: local parts are case-sensitive in the RFCs, but no mail provider in practice treats them that way, and comparing addresses case-sensitively causes duplicate signups.
Where the provider is known, provider-specific rules also apply: Gmail ignores dots, and most providers ignore a "+tag" suffix. Addresses that reach the same mailbox therefore normalize to the same string.
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
e, _ := emailx.Parse("John.Doe+newsletter@Gmail.com")
e.Normalize()
fmt.Println(e.Address())
}
Output: johndoe@gmail.com
func (*Email) ProviderID ¶
ProviderID returns the ID of the provider, or empty string.
func (*Email) String ¶
String returns the normalized email address if available, otherwise the raw email.
func (*Email) Subdomain ¶
Subdomain returns the subdomain if present, otherwise empty. E.g., for "mail.yahoo.com" it returns "mail".
func (*Email) Suggestion ¶
Suggestion returns a suggested correction if the domain looks like a typo of a popular domain.
func (*Email) UnmarshalJSON ¶
UnmarshalJSON implements the json.Unmarshaler interface.
func (*Email) UnmarshalText ¶
UnmarshalText implements encoding.TextUnmarshaler.
func (*Email) Validate ¶
func (e *Email) Validate() ValidationResult
Validate performs every check this package can make without a network call: syntax, domain shape, disposability, and role accounts.
func (*Email) ValidateDomain ¶
ValidateDomain checks basic domain correctness.
func (*Email) ValidateSyntax ¶
ValidateSyntax validates the syntax strictly, returning the reason it failed. IsValid is the boolean form of the same check.
func (*Email) ValidateSyntaxSMTPUTF8 ¶ added in v0.4.0
ValidateSyntaxSMTPUTF8 is ValidateSyntax with RFC 6531 addresses allowed.
type EmailIndex ¶
type EmailIndex struct {
// contains filtered or unexported fields
}
EmailIndex provides fast lookups for millions of emails.
func (*EmailIndex) Exists ¶
func (idx *EmailIndex) Exists(input string) bool
Exists checks if a normalized email exists in the index.
func (*EmailIndex) FindByDomain ¶
func (idx *EmailIndex) FindByDomain(domain string) []*Email
FindByDomain returns emails matching the domain.
func (*EmailIndex) FindByProvider ¶
func (idx *EmailIndex) FindByProvider(providerID string) []*Email
FindByProvider returns emails matching the provider ID.
type EmailSet ¶
type EmailSet struct {
// contains filtered or unexported fields
}
EmailSet is a thread-safe set of emails.
type ErrorCode ¶
type ErrorCode string
const ( ErrorCodeEmpty ErrorCode = "empty" ErrorCodeInvalidSyntax ErrorCode = "invalid_syntax" ErrorCodeInvalidLocalPart ErrorCode = "invalid_local_part" ErrorCodeInvalidDomain ErrorCode = "invalid_domain" ErrorCodeInvalidTLD ErrorCode = "invalid_tld" ErrorCodeInvalidUnicode ErrorCode = "invalid_unicode" ErrorCodeDomainTooLong ErrorCode = "domain_too_long" ErrorCodeLocalPartTooLong ErrorCode = "local_part_too_long" )
type FingerprintOptions ¶
type FingerprintOptions struct {
Secret string
}
func WithSecret ¶
func WithSecret(secret string) FingerprintOptions
type MaskOptions ¶
type ParseError ¶
func (*ParseError) Error ¶
func (e *ParseError) Error() string
type Provider ¶
Provider represents information about an email provider.
func ProviderByID ¶ added in v0.3.0
ProviderByID returns the provider with the given ID, or nil. The result is a fresh copy.
func ProviderForDomain ¶ added in v0.3.0
ProviderForDomain returns the provider serving a domain, or nil. It is the package-level form of (*Email).Provider. The result is a fresh copy.
func Providers ¶ added in v0.3.0
func Providers() []*Provider
Providers returns every known email provider, one entry per provider, sorted by ID. Each entry carries the full sorted list of domains that provider serves, which is what a generator needs to pick a realistic address. The result is a fresh copy; mutating it does not affect the package.
Example ¶
package main
import (
"fmt"
"github.com/bakhod1r/emailx"
)
func main() {
// Enumerating the table is what a generator needs: pick a provider, then
// pick one of its domains.
for _, p := range emailx.Providers() {
if p.ID == "gmail" {
fmt.Println(p.Name, p.Domains)
}
}
}
Output: Gmail [gmail.com googlemail.com]
type ValidationResult ¶
type ValidationResult struct {
Valid bool
SyntaxValid bool
DomainValid bool
MXValid bool
SPFValid bool
DMARCValid bool
Disposable bool
Role bool
}
ValidationResult holds the result of extensive validation.
Validate fills only the offline fields. MXValid, SPFValid and DMARCValid require DNS and are left false; the emailx/dns subpackage's Validate fills them in.