ev

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 43 Imported by: 0

README

emailvalidator

A production-oriented Go email validation library, CLI, HTTP service, and authenticated ESP webhook receiver.

The module combines conservative address parsing, DNS mail-routing checks, active SMTP recipient probing, catch-all detection, disposable-domain detection, role-account flags, and privacy-preserving reputation signals.

What is included

  • Syntax and normalization
    • 254-byte address and 64-byte local-part limits
    • dot-atom validation and optional quoted local parts
    • optional SMTPUTF8 local parts
    • Unicode-domain conversion to Punycode
    • optional pluggable full IDNA/UTS #46 mapper
    • domain-literal support only when explicitly enabled
    • control-character and SMTP command-injection rejection
  • DNS verification
    • MX lookup and preference ordering
    • Null MX detection
    • RFC 5321 implicit MX fallback to A/AAAA
    • bounded positive/negative caches and duplicate-request coalescing
  • SMTP verification
    • direct SMTP protocol client with EHLO/HELO fallback
    • RCPT TO mailbox classification
    • enhanced status-code parsing
    • MX failover, deadlines, bounded responses, concurrency limits, and per-domain pacing
    • STARTTLS with certificate validation and TLS 1.2 minimum
    • SMTPUTF8 capability enforcement
    • catch-all sampling using cryptographically random recipients
    • positive, negative, unknown, and catch-all caches
    • DNS-rebinding/SSRF defense: MX hosts are resolved once, public IPs are pinned, and non-public targets are blocked by default
  • Disposable detection
    • 22,000+ embedded domains from an MIT-licensed community dataset
    • parent-domain matching
    • lock-free reads, atomic blocklist replacement, and application allowlist override
  • Role and provider flags
    • generic role addresses such as info@, support@, sales@, admin@, and noreply@
    • common free-mail provider flag
  • Reputation tracking
    • delivered, hard/soft bounce, complaint, unsubscribe, and deferral signals
    • clear-text addresses are accepted only transiently
    • persisted keys are HMAC-SHA-256 hashes using an operator-owned 32+ byte pepper
    • address and domain aggregates
    • provider event-ID idempotency, including across restarts
    • CRC32C append log, torn-tail recovery, exclusive lock, optional fsync, atomic compaction, and retention of hashed idempotency markers
  • Webhook adapters
    • generic HMAC endpoint
    • Twilio SendGrid ECDSA signature verification
    • Mailgun timestamp/token HMAC verification
    • AWS SES/SNS certificate and message-signature verification with strict certificate-URL validation
    • Postmark operator-configured secret header or HTTP Basic authentication
    • body limits, timestamp windows, replay rejection, provider event normalization, and no recipient echo in responses
  • Operational surfaces
    • library API
    • bulk validation preserving input order
    • CLI
    • HTTP daemon with API-key authentication, rate limiting, strict JSON, request/body limits, security headers, health check, TLS option, and graceful shutdown
    • Dockerfile, Compose example, Makefile, tests, race tests, and dataset update script

Important SMTP limitation

An SMTP 250 response means the remote server accepted the recipient command at that moment. It is not absolute proof that a human-controlled mailbox exists: providers may accept then bounce, tarpitting and greylisting are common, and some providers intentionally obscure mailbox state. Treat exists as strong evidence, does_not_exist as strong negative evidence, and temporary/blocked/unknown results as inconclusive. Never send message content during verification.

Active SMTP probing is disabled by default. Enable it only from infrastructure permitted to make outbound TCP/25 connections, use a valid HELO identity and envelope sender under your control, apply strict rate limits, and review provider policies and applicable law.

Requirements

  • Go 1.23 or newer for source compatibility; use a currently security-supported Go toolchain for production builds (the Dockerfile pins Go 1.26.5)
  • Network DNS access for MX checks
  • Outbound TCP/25 for active SMTP checks
  • A stable, secret 32-byte-or-longer reputation pepper for durable reputation storage

The core module has no third-party runtime dependencies.

Quick start: library

package main

import (
    "context"
    "encoding/json"
    "log"
    "os"
    "time"

    ev "github.com/oarkflow/ev"
)

func main() {
    cfg := ev.DefaultConfig() // DNS on, SMTP off

    smtp := cfg.SMTP.Options
    smtp.Enabled = true
    smtp.HELODomain = "validator.example.com"
    smtp.MailFrom = "email-validation@example.com"
    smtp.RequireTLS = false // many MX servers still do not require STARTTLS
    smtp.MaxConcurrent = 16
    smtp.MinDomainInterval = 500 * time.Millisecond
    cfg.SMTP = ev.NewSMTPChecker(smtp)

    validator := ev.New(cfg)
    defer validator.Close()

    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    result, err := validator.Validate(ctx, "person@example.com")
    if err != nil {
        log.Fatal(err)
    }
    json.NewEncoder(os.Stdout).Encode(result)
}
Full IDNA mapping without a core dependency

The default normalizer is conservative and performs direct Unicode-to-Punycode conversion. Applications needing complete UTS #46 mapping can supply any object with ToASCII(string) (string, error), for example a profile from golang.org/x/net/idna:

cfg := emailvalidator.DefaultConfig()
cfg.Address.DomainMapper = idna.Lookup

Syntax-only validation

result := emailvalidator.ValidateSyntax("person@example.com", emailvalidator.AddressOptions{})
if result.Status != emailvalidator.StatusPass {
    // result.Error contains the reason
}

Durable privacy-preserving reputation

Generate the pepper once and store it in a secret manager. Never rotate or lose it without a migration plan; a different pepper produces different keys and makes existing aggregates unreachable.

openssl rand -base64 32
pepper := []byte("at-least-32-random-secret-bytes...")
store, err := emailvalidator.OpenFileReputationStore(emailvalidator.FileReputationOptions{
    Path:                 "/var/lib/emailvalidator/reputation.db",
    Pepper:               pepper,
    SyncWrites:           true,
    CompactAfterRecords:  100_000,
    MaxLogBytes:          64 << 20,
    IdempotencyRetention: 30 * 24 * time.Hour,
})
if err != nil {
    log.Fatal(err)
}

cfg := emailvalidator.DefaultConfig()
cfg.Reputation = store
validator := emailvalidator.New(cfg) // Close closes the store

Record signals directly:

err := validator.RecordReputationEvent(ctx, emailvalidator.ReputationEvent{
    ID:    "my-esp:event-unique-id",
    Email: "person@example.com",
    Type:  emailvalidator.EventHardBounce,
    At:    time.Now().UTC(),
})

The file contains HMAC keys and aggregate counters only. It does not persist email addresses, domains, webhook bodies, SMTP responses, or bounce reasons. Hashing is pseudonymization, not anonymization: protect the pepper, restrict file access, define retention, and apply your privacy obligations.

The bundled file store is intentionally single-process and uses an exclusive lock. For a replicated service, implement the small ReputationStore interface against a transactional shared database or route all reputation writes to one owner.

CLI

Build and validate arguments:

go build -o bin/emailvalidator ./cmd/emailvalidator
./bin/emailvalidator person@example.com support@example.org

Read newline-delimited addresses and emit JSON Lines:

cat emails.txt | ./bin/emailvalidator -json -workers 16

Enable SMTP explicitly:

./bin/emailvalidator \
  -smtp \
  -helo validator.example.com \
  -mail-from email-validation@example.com \
  person@example.com

HTTP service

cp .env.example .env
# Edit secrets and identities, then:
docker compose up --build

Or run directly:

export EV_LISTEN=:8080
export EV_API_KEY='replace-with-a-long-random-key'
go run ./cmd/emailvalidatord

Validate one address:

curl -sS http://127.0.0.1:8080/v1/validate \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: replace-with-a-long-random-key' \
  -d '{"email":"person@example.com"}'

Bulk validation:

curl -sS http://127.0.0.1:8080/v1/validate/bulk \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer replace-with-a-long-random-key' \
  -d '{"emails":["a@example.com","support@example.org"],"workers":8}'

Endpoints:

Method Path Authentication Purpose
GET /healthz none liveness
POST /v1/validate API key when configured one address
POST /v1/validate/bulk API key when configured bounded batch
POST /webhooks/generic HMAC signature generic reputation events
POST /webhooks/sendgrid SendGrid ECDSA signature SendGrid events
POST /webhooks/mailgun Mailgun HMAC signature Mailgun events
POST /webhooks/postmark secret header or Basic Auth Postmark events
POST /webhooks/ses SNS signature SES events

See docs/CONFIGURATION.md and docs/WEBHOOKS.md.

Verdict model

The API deliberately avoids a misleading boolean result:

  • deliverable: mail routing passed and SMTP explicitly accepted the mailbox without catch-all behavior.
  • risky: disposable, role account, catch-all, or meaningful negative reputation signals.
  • undeliverable: invalid syntax, non-mail-routable domain, Null MX, or a strong permanent SMTP rejection.
  • unknown: checks were disabled or remote systems returned inconclusive/transient/policy-blocked outcomes.

Use risk_score, stable reasons[].code, and individual stage results to make business-specific decisions. Do not reject users solely because an address is a role account or because an SMTP probe was blocked.

Security defaults

  • SMTP disabled by default.
  • Private, loopback, link-local, multicast, and unspecified SMTP targets blocked by default.
  • MX names are resolved once and dials are pinned to the reviewed IP.
  • STARTTLS validates the MX hostname and enforces TLS 1.2 or newer.
  • SMTP commands reject CR/LF injection.
  • SMTP response lines and total responses are bounded.
  • HTTP JSON rejects unknown fields, trailing values, and oversized bodies.
  • API secrets are compared in constant time.
  • SNS certificate downloads accept only strict AWS SNS HTTPS hosts and paths, reject redirects, validate certificate chains, and cache briefly.
  • Durable reputation uses HMAC-SHA-256 rather than unsalted hashes.
  • On-disk files and lock files are created with owner-only permissions.

For deployment guidance, see SECURITY.md.

Disposable-domain data

The embedded compressed list currently contains 22,102 domains, substantially more than the requested 3,000. It is derived from the MIT-licensed disposable/disposable-email-domains project. Lists can be stale or contain false positives, so use SetAllowlist for domains you trust and update the snapshot regularly:

make update-disposable

The updater validates, sorts, deduplicates, requires at least 3,000 entries, and creates deterministic gzip output.

Customization

The main extension points are:

  • DNSResolver for a private resolver, DNS-over-HTTPS gateway, or deterministic test resolver.
  • SMTPDialer and SMTPIPResolver for controlled networking.
  • DomainMapper for full IDNA policy.
  • ReputationStore for PostgreSQL, MySQL, Redis, or another transactional backend.
  • RoleAccountDetector and FreeProviderDetector for organization-specific classification.
  • Scorer / ScorerFunc for business-specific weights and verdict policy.
  • Observer for metrics and traces. Observer callbacks are synchronous and must return quickly.
  • DisposableDetector.ReplaceBlocklist and SetAllowlist for local policy.

Build and verification

make check

This runs formatting verification, go vet, unit/integration tests, race tests, and builds both executables.

Individual commands:

go test ./...
go test -race ./...
go vet ./...
go build ./cmd/emailvalidator ./cmd/emailvalidatord

The SMTP tests use a local fake server and never contact third-party mail systems.

Package layout

.
├── address.go                 syntax, SMTPUTF8, domain normalization
├── dns.go                     MX/Null-MX/implicit-MX checks
├── smtp.go                    SMTP protocol and catch-all verification
├── disposable.go              embedded and replaceable domain detection
├── reputation*.go             HMAC aggregate stores and durable log
├── webhook*.go                authenticated ESP adapters
├── validator.go / score.go    orchestration and verdict model
├── httpapi/                   hardened HTTP handler
├── cmd/emailvalidator/        CLI
├── cmd/emailvalidatord/       HTTP daemon
├── examples/library/          embedding example
└── data/                      embedded dataset and license

Non-goals

  • Sending verification emails or message bodies
  • Bypassing providers that intentionally block mailbox enumeration
  • Claiming a mailbox belongs to a person
  • Guaranteeing future delivery
  • Replacing consent, double opt-in, suppression lists, or real delivery telemetry

License

The Go code is MIT licensed. The disposable-domain snapshot has its own included MIT notice in data/LICENSE-disposable-email-domains.

High-throughput bulk validation

ValidateBulk is the recommended in-memory batch API. It uses a bounded worker pool, collapses duplicate normalized addresses, distributes domains across workers, preserves input order, supports per-item deadlines, continues past individual failures when configured, and returns throughput/verdict statistics.

bulk, err := validator.ValidateBulk(ctx, emails, emailvalidator.BulkOptions{
    Workers:         64,
    QueueSize:       256,
    Deduplicate:     true,
    ContinueOnError: true,
    PerItemTimeout:  15 * time.Second,
    ProgressEvery:   1000,
    OnProgress: func(p emailvalidator.BulkProgress) {
        log.Printf("completed=%d/%d rate=%.0f/s", p.Completed, p.Total, p.RatePerSecond)
    },
})

For lists too large to retain in memory, use ValidateStream. It consumes a channel and emits completion-order results using bounded memory and backpressure.

input := make(chan string, 256)
go readEmails(input)
for item := range validator.ValidateStream(ctx, input, emailvalidator.BulkOptions{
    Workers: 64, QueueSize: 256, PerItemTimeout: 15 * time.Second,
}) {
    // write item immediately to NDJSON, CSV, a queue, or a database
}

The daemon exposes:

  • POST /v1/validate/bulk — JSON request, ordered JSON response with summary.
  • POST /v1/validate/bulk/stream — newline-delimited email input and NDJSON output as results complete.

Example streaming request:

printf 'a@example.com\nb@example.com\n' | curl -N \
  -H 'Content-Type: text/plain' \
  -H "X-API-Key: $EV_API_KEY" \
  --data-binary @- http://localhost:8080/v1/validate/bulk/stream
Performance guidance
  • Keep SMTP concurrency below provider and network limits; EV_SMTP_MAX_CONCURRENT=32 is intentionally conservative.
  • For syntax, disposable, role, reputation, and cached DNS validation, start with 4–8 workers per CPU.
  • Enable de-duplication for imported lists; duplicate results reuse the original validation without repeated DNS/SMTP work.
  • Streaming is preferable above tens of thousands of addresses because memory remains bounded.
  • Domain round-robin scheduling avoids letting one heavily represented provider monopolize workers while SMTP pacing is active.
  • DNS, SMTP mailbox, and catch-all caches are shared across all bulk workers.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrClosed             = errors.New("emailvalidator: closed")
	ErrReputationDisabled = errors.New("emailvalidator: reputation store is not configured")
)

Functions

func GenericWebhook

func GenericWebhook(recorder ReputationRecorder, cfg GenericWebhookConfig) http.Handler

GenericWebhook accepts {"events":[{"email":"...","type":"hard_bounce", "timestamp":"..."}]}. Signature = hex(HMAC-SHA256(secret, timestamp + "." + raw_body)).

func IsFreeProviderDomain

func IsFreeProviderDomain(domain string) bool

IsFreeProviderDomain reports whether a domain is a common consumer mailbox provider.

func IsRoleAccountLocal

func IsRoleAccountLocal(local string) bool

IsRoleAccountLocal reports whether a local part is a generic role account.

func MailgunWebhook

func MailgunWebhook(recorder ReputationRecorder, cfg MailgunWebhookConfig) http.Handler

func PostmarkWebhook

func PostmarkWebhook(recorder ReputationRecorder, cfg PostmarkWebhookConfig) http.Handler

func SESWebhook

func SESWebhook(recorder ReputationRecorder, cfg SESWebhookConfig) http.Handler

func SendGridWebhook

func SendGridWebhook(recorder ReputationRecorder, cfg SendGridWebhookConfig) http.Handler

Types

type AddressOptions

type AddressOptions struct {
	AllowQuotedLocal   bool
	AllowDomainLiteral bool
	AllowSMTPUTF8      bool
	DomainMapper       DomainMapper
}

type BulkItem

type BulkItem struct {
	Index  int    `json:"index"`
	Email  string `json:"email"`
	Result Result `json:"result"`
	Error  string `json:"error,omitempty"`
}

BulkItem is one completed item. Index always refers to the original input.

type BulkOptions

type BulkOptions struct {
	Workers         int
	QueueSize       int
	Deduplicate     bool
	Ordered         bool
	ContinueOnError bool
	PerItemTimeout  time.Duration
	ProgressEvery   int
	OnProgress      func(BulkProgress)
}

BulkOptions controls high-throughput validation. Zero values select safe production defaults. Results preserve input order unless Ordered is false.

type BulkProgress

type BulkProgress struct {
	Total         int           `json:"total"`
	Submitted     int           `json:"submitted"`
	Completed     int           `json:"completed"`
	Unique        int           `json:"unique"`
	Duplicates    int           `json:"duplicates"`
	Failed        int           `json:"failed"`
	Elapsed       time.Duration `json:"elapsed_ns"`
	RatePerSecond float64       `json:"rate_per_second"`
}

BulkProgress is a lock-free snapshot emitted during a bulk run.

type BulkResult

type BulkResult struct {
	Items         []BulkItem      `json:"items"`
	Total         int             `json:"total"`
	Unique        int             `json:"unique"`
	Duplicates    int             `json:"duplicates"`
	Succeeded     int             `json:"succeeded"`
	Failed        int             `json:"failed"`
	Verdicts      map[Verdict]int `json:"verdicts"`
	Duration      time.Duration   `json:"duration_ns"`
	RatePerSecond float64         `json:"rate_per_second"`
}

BulkResult contains ordered results and aggregate performance information.

type CatchAllStatus

type CatchAllStatus string

CatchAllStatus captures whether a domain accepts randomly generated recipients.

const (
	CatchAllNotChecked CatchAllStatus = "not_checked"
	CatchAllYes        CatchAllStatus = "yes"
	CatchAllNo         CatchAllStatus = "no"
	CatchAllUnknown    CatchAllStatus = "unknown"
)

type Config

type Config struct {
	Address              AddressOptions
	CheckDNS             bool
	DNS                  *DNSChecker
	SMTP                 *SMTPChecker
	Disposable           *DisposableDetector
	Reputation           ReputationStore
	RoleAccountDetector  func(local string) bool
	FreeProviderDetector func(domain string) bool
	Scorer               Scorer
	Observer             Observer
	Now                  func() time.Time
}

Config configures the layered validator.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns conservative production defaults. DNS checking is enabled. Active SMTP probing remains disabled until explicitly enabled.

type DNSChecker

type DNSChecker struct {
	Resolver    DNSResolver
	Timeout     time.Duration
	PositiveTTL time.Duration
	NegativeTTL time.Duration
	CacheSize   int
	// contains filtered or unexported fields
}

func NewDNSChecker

func NewDNSChecker() *DNSChecker

func (*DNSChecker) Check

func (d *DNSChecker) Check(ctx context.Context, domain string, literal bool) DNSResult

type DNSResolver

type DNSResolver interface {
	LookupMX(context.Context, string) ([]*net.MX, error)
	LookupIP(context.Context, string, string) ([]net.IP, error)
}

DNSResolver permits deterministic testing and custom resolver integration.

type DNSResult

type DNSResult struct {
	Status     Status        `json:"status"`
	MX         []MXRecord    `json:"mx,omitempty"`
	ImplicitMX bool          `json:"implicit_mx,omitempty"`
	NullMX     bool          `json:"null_mx,omitempty"`
	HasA       bool          `json:"has_a,omitempty"`
	HasAAAA    bool          `json:"has_aaaa,omitempty"`
	Error      string        `json:"error,omitempty"`
	Duration   time.Duration `json:"duration_ns,omitempty"`
}

DNSResult describes domain mail-routing capability.

type DisposableDetector

type DisposableDetector struct {
	// contains filtered or unexported fields
}

DisposableDetector supports lock-free reads and atomic list replacement.

func NewDisposableDetector

func NewDisposableDetector() *DisposableDetector

func (*DisposableDetector) Count

func (d *DisposableDetector) Count() int

func (*DisposableDetector) IsDisposable

func (d *DisposableDetector) IsDisposable(domain string) bool

IsDisposable checks exact domains and parent suffixes. An allowlist always overrides the bundled or custom blocklist.

func (*DisposableDetector) ReplaceBlocklist

func (d *DisposableDetector) ReplaceBlocklist(r io.Reader) error

ReplaceBlocklist atomically replaces the blocklist from newline-delimited domains. Blank lines and # comments are ignored.

func (*DisposableDetector) SetAllowlist

func (d *DisposableDetector) SetAllowlist(domains []string) error

SetAllowlist atomically replaces the allowlist.

type DomainMapper

type DomainMapper interface {
	ToASCII(string) (string, error)
}

AddressOptions controls syntax acceptance. The defaults intentionally reject uncommon address forms that are technically legal but frequently unsafe in web registration flows. DomainMapper can provide full IDNA/UTS #46 processing without forcing a dependency on applications that only accept ASCII domains. Compatible implementations expose ToASCII, such as golang.org/x/net/idna profiles.

type FileReputationOptions

type FileReputationOptions struct {
	Path                string
	Pepper              []byte
	SyncWrites          bool
	CompactAfterRecords uint64
	MaxLogBytes         int64
	BreakStaleLockAfter time.Duration
	// IdempotencyRetention controls how long HMAC event markers are retained.
	// Zero defaults to 30 days.
	IdempotencyRetention time.Duration
}

FileReputationOptions configures the durable hashed reputation log.

type FileReputationStore

type FileReputationStore struct {
	// contains filtered or unexported fields
}

FileReputationStore is an exclusive-process, append-only store with CRC32C recovery and atomic compaction. The on-disk file contains hashes and counters only; no email address, domain, reason, or message body is persisted.

func OpenFileReputationStore

func OpenFileReputationStore(opts FileReputationOptions) (*FileReputationStore, error)

func (*FileReputationStore) Close

func (s *FileReputationStore) Close() error

func (*FileReputationStore) Compact

func (s *FileReputationStore) Compact() error

func (*FileReputationStore) Lookup

func (s *FileReputationStore) Lookup(ctx context.Context, email, domain string) (ReputationStats, error)

func (*FileReputationStore) Record

type GenericWebhookConfig

type GenericWebhookConfig struct {
	Secret       []byte
	MaxBodyBytes int64
	MaxClockSkew time.Duration
}

GenericWebhookConfig configures a compact HMAC-signed webhook endpoint.

type MXRecord

type MXRecord struct {
	Host string `json:"host"`
	Pref uint16 `json:"preference"`
}

MXRecord is a normalized DNS MX record.

type MailboxStatus

type MailboxStatus string

MailboxStatus is the SMTP mailbox-level outcome.

const (
	MailboxNotChecked             MailboxStatus = "not_checked"
	MailboxExists                 MailboxStatus = "exists"
	MailboxDoesNotExist           MailboxStatus = "does_not_exist"
	MailboxAcceptAll              MailboxStatus = "accept_all"
	MailboxTemporarilyUnavailable MailboxStatus = "temporarily_unavailable"
	MailboxFull                   MailboxStatus = "mailbox_full"
	MailboxDisabled               MailboxStatus = "mailbox_disabled"
	MailboxBlocked                MailboxStatus = "blocked"
	MailboxUnknown                MailboxStatus = "unknown"
)

type MailgunWebhookConfig

type MailgunWebhookConfig struct {
	SigningKey   []byte
	MaxBodyBytes int64
	MaxClockSkew time.Duration
}

type MemoryReputationStore

type MemoryReputationStore struct {
	// contains filtered or unexported fields
}

MemoryReputationStore is useful for tests and ephemeral services. It applies the same HMAC privacy model as the file store.

func NewMemoryReputationStore

func NewMemoryReputationStore(pepper []byte) (*MemoryReputationStore, error)

func (*MemoryReputationStore) Close

func (s *MemoryReputationStore) Close() error

func (*MemoryReputationStore) Lookup

func (s *MemoryReputationStore) Lookup(ctx context.Context, email, domain string) (ReputationStats, error)

func (*MemoryReputationStore) Record

type Observer

type Observer interface {
	ValidationCompleted(context.Context, Result)
}

Observer receives operational events. Implementations should return quickly and must be safe for concurrent use.

type PostmarkWebhookConfig

type PostmarkWebhookConfig struct {
	Token         string
	TokenHeader   string
	BasicUser     string
	BasicPassword string
	MaxBodyBytes  int64
}

type Reason

type Reason struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Weight  int    `json:"weight,omitempty"`
}

Reason is a stable machine-readable explanation for a verdict or risk.

type ReputationEvent

type ReputationEvent struct {
	// ID is an optional provider-scoped event identifier. When present, durable
	// stores use its HMAC as an idempotency marker so retries are not counted
	// twice, including after process restarts.
	ID     string
	Email  string
	Domain string
	Type   ReputationEventType
	At     time.Time
}

ReputationEvent contains transient clear-text input. Stores hash the address before persistence and never persist Email or Domain.

type ReputationEventType

type ReputationEventType string

ReputationEventType is a normalized ESP signal.

const (
	EventDelivered   ReputationEventType = "delivered"
	EventHardBounce  ReputationEventType = "hard_bounce"
	EventSoftBounce  ReputationEventType = "soft_bounce"
	EventComplaint   ReputationEventType = "complaint"
	EventUnsubscribe ReputationEventType = "unsubscribe"
	EventDeferred    ReputationEventType = "deferred"
)

type ReputationRecorder

type ReputationRecorder interface {
	RecordReputationEvent(context.Context, ReputationEvent) error
}

ReputationRecorder is implemented by Validator.

type ReputationStats

type ReputationStats struct {
	Delivered    uint64    `json:"delivered"`
	HardBounces  uint64    `json:"hard_bounces"`
	SoftBounces  uint64    `json:"soft_bounces"`
	Complaints   uint64    `json:"complaints"`
	Unsubscribes uint64    `json:"unsubscribes"`
	Deferred     uint64    `json:"deferred"`
	LastEventAt  time.Time `json:"last_event_at,omitempty"`
	Score        int       `json:"score"`
}

ReputationStats contains only aggregate counters. Implementations must not persist the clear-text address.

type ReputationStore

type ReputationStore interface {
	Record(context.Context, ReputationEvent) error
	Lookup(context.Context, string, string) (ReputationStats, error)
	Close() error
}

ReputationStore persists and retrieves aggregate delivery signals.

type Result

type Result struct {
	Input        string          `json:"input"`
	Normalized   string          `json:"normalized,omitempty"`
	Verdict      Verdict         `json:"verdict"`
	RiskScore    int             `json:"risk_score"`
	Syntax       SyntaxResult    `json:"syntax"`
	DNS          DNSResult       `json:"dns"`
	SMTP         SMTPResult      `json:"smtp"`
	Disposable   bool            `json:"disposable"`
	RoleAccount  bool            `json:"role_account"`
	FreeProvider bool            `json:"free_provider"`
	Reputation   ReputationStats `json:"reputation"`
	Reasons      []Reason        `json:"reasons,omitempty"`
	CheckedAt    time.Time       `json:"checked_at"`
	Duration     time.Duration   `json:"duration_ns"`
}

Result is a complete validation result.

type SESWebhookConfig

type SESWebhookConfig struct {
	MaxBodyBytes     int64
	MaxClockSkew     time.Duration
	HTTPClient       *http.Client
	AllowedTopicARNs []string
}

type SMTPChecker

type SMTPChecker struct {
	Options    SMTPOptions
	Dialer     SMTPDialer
	IPResolver SMTPIPResolver
	// contains filtered or unexported fields
}

func NewSMTPChecker

func NewSMTPChecker(opts SMTPOptions) *SMTPChecker

func (*SMTPChecker) Check

func (s *SMTPChecker) Check(ctx context.Context, address, domain string, dns DNSResult, smtpUTF8 bool) SMTPResult

type SMTPDialer

type SMTPDialer interface {
	DialContext(context.Context, string, string) (net.Conn, error)
}

SMTPDialer makes SMTP transport testable and allows proxy/custom-network use.

type SMTPIPResolver

type SMTPIPResolver interface {
	LookupIPAddr(context.Context, string) ([]net.IPAddr, error)
}

SMTPIPResolver is used to resolve MX hosts once before dialing. This prevents DNS rebinding between policy checks and the TCP connection.

type SMTPOptions

type SMTPOptions struct {
	Enabled            bool
	Port               int
	Timeout            time.Duration
	HELODomain         string
	MailFrom           string
	TrySTARTTLS        bool
	RequireTLS         bool
	InsecureSkipVerify bool
	CatchAllSamples    int
	MaxMXAttempts      int
	MaxConcurrent      int
	MinDomainInterval  time.Duration
	MaxResponseBytes   int
	DisableCatchAll    bool
	AllowPrivateIPs    bool
	PositiveTTL        time.Duration
	NegativeTTL        time.Duration
	UnknownTTL         time.Duration
	CatchAllTTL        time.Duration
	CatchAllUnknownTTL time.Duration
	TLSConfig          *tls.Config
}

SMTPOptions controls active mailbox probing. SMTP probing should only be enabled where outbound TCP/25 is permitted and after reviewing the target providers' acceptable-use policies.

func DefaultSMTPOptions

func DefaultSMTPOptions() SMTPOptions

type SMTPResult

type SMTPResult struct {
	Status       Status         `json:"status"`
	Mailbox      MailboxStatus  `json:"mailbox"`
	CatchAll     CatchAllStatus `json:"catch_all"`
	Host         string         `json:"host,omitempty"`
	Code         int            `json:"code,omitempty"`
	EnhancedCode string         `json:"enhanced_code,omitempty"`
	Message      string         `json:"message,omitempty"`
	TLS          bool           `json:"tls,omitempty"`
	Cached       bool           `json:"cached,omitempty"`
	Attempts     int            `json:"attempts,omitempty"`
	Duration     time.Duration  `json:"duration_ns,omitempty"`
	Error        string         `json:"error,omitempty"`
}

SMTPResult describes an SMTP RCPT probe. A positive response is evidence of acceptance, not proof that a human-owned mailbox exists.

type Scorer

type Scorer interface {
	Score(*Result)
}

Scorer converts completed stage results into a business verdict and reasons. Implementations must be safe for concurrent use.

type ScorerFunc

type ScorerFunc func(*Result)

ScorerFunc adapts a function to Scorer.

func (ScorerFunc) Score

func (f ScorerFunc) Score(r *Result)

type SendGridWebhookConfig

type SendGridWebhookConfig struct {
	PublicKeyBase64 string
	MaxBodyBytes    int64
	MaxClockSkew    time.Duration
}

type Status

type Status string

Status is the state of an individual validation stage.

const (
	StatusNotChecked Status = "not_checked"
	StatusPass       Status = "pass"
	StatusFail       Status = "fail"
	StatusUnknown    Status = "unknown"
)

type SyntaxResult

type SyntaxResult struct {
	Status        Status `json:"status"`
	Normalized    string `json:"normalized,omitempty"`
	Local         string `json:"local,omitempty"`
	Domain        string `json:"domain,omitempty"`
	SMTPUTF8      bool   `json:"smtp_utf8,omitempty"`
	DomainLiteral bool   `json:"domain_literal,omitempty"`
	Error         string `json:"error,omitempty"`
}

SyntaxResult describes parsing and normalization.

func ValidateSyntax

func ValidateSyntax(input string, opts AddressOptions) SyntaxResult

ValidateSyntax performs syntax and normalization only.

type ValidationError

type ValidationError struct {
	Code string
	Msg  string
}

ValidationError identifies an input error without conflating it with DNS or SMTP uncertainty.

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator is safe for concurrent use.

func New

func New(cfg Config) *Validator

func NewDefault

func NewDefault() *Validator

NewDefault constructs a validator with DNS enabled and SMTP disabled.

func (*Validator) Close

func (v *Validator) Close() error

func (*Validator) RecordReputation

func (v *Validator) RecordReputation(ctx context.Context, email string, typ ReputationEventType, at time.Time) error

RecordReputation normalizes an address and records an ESP signal. The store receives clear text only in memory and hashes it before persistence.

func (*Validator) RecordReputationEvent

func (v *Validator) RecordReputationEvent(ctx context.Context, event ReputationEvent) error

RecordReputationEvent records an event with an optional provider-scoped ID. Supplying ID enables restart-safe idempotency in the bundled stores.

func (*Validator) Validate

func (v *Validator) Validate(ctx context.Context, input string) (Result, error)

Validate runs all configured checks. DNS and SMTP uncertainty are represented in the result rather than returned as fatal errors; errors are reserved for cancellation or a closed validator.

func (*Validator) ValidateBulk

func (v *Validator) ValidateBulk(ctx context.Context, emails []string, opts BulkOptions) (BulkResult, error)

ValidateBulk validates a bounded in-memory batch. It de-duplicates identical normalized inputs, performs one validation for each unique address, fans the result back to all original positions, and never creates one goroutine per item.

func (*Validator) ValidateMany

func (v *Validator) ValidateMany(ctx context.Context, emails []string, workers int) ([]Result, error)

ValidateMany is retained for compatibility and delegates to the optimized bulk engine with ordered output and de-duplication enabled.

func (*Validator) ValidateStream

func (v *Validator) ValidateStream(ctx context.Context, input <-chan string, opts BulkOptions) <-chan BulkItem

ValidateStream validates an arbitrarily large stream with bounded memory. Input is consumed until closed; output arrives in completion order. The caller must drain the returned channel. Cancellation closes output promptly.

type Verdict

type Verdict string

Verdict is the overall recommendation produced by the validator.

const (
	VerdictDeliverable   Verdict = "deliverable"
	VerdictRisky         Verdict = "risky"
	VerdictUndeliverable Verdict = "undeliverable"
	VerdictUnknown       Verdict = "unknown"
)

type WebhookResponse

type WebhookResponse struct {
	Accepted int      `json:"accepted"`
	Ignored  int      `json:"ignored"`
	Errors   []string `json:"errors,omitempty"`
}

WebhookResponse summarizes ingestion without echoing recipient addresses.

Directories

Path Synopsis
cmd
emailvalidator command
emailvalidatord command
examples
bulk command
library command
stream command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL