emailkit

package module
v0.1.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Feb 27, 2026 License: BSD-3-Clause Imports: 12 Imported by: 0

README

emailkit

CI Go Reference Go Report Card License

Modular Go email validation library with a fluent builder API. Validate email addresses from basic syntax checks through DNS verification to SMTP mailbox probing — pick only the levels you need.

Features

  • Fluent builder API — compose your validation pipeline with New().WithDNS().WithDomain().WithSMTP()
  • RFC 5321/5322 syntax validation with local part and domain checks
  • Internationalized Domain Names (IDN) — automatic IDNA2008 Punycode conversion
  • Internationalized email local parts (EAI) — RFC 6531 / SMTPUTF8 support
  • DNS validation with MX record lookup and optional A record fallback
  • Disposable email detection — built-in list of ~100 known throwaway domains
  • Domain typo detection — Levenshtein distance matching against major providers
  • SMTP RCPT TO probe with multi-MX host support
  • SMTP connection pool — RSET-based connection reuse for bulk validation
  • DNS MX cache — singleflight deduplication and configurable TTL
  • Bulk validation — concurrent processing with domain-sorted ordering for optimal cache/pool locality
  • Context support — timeout and cancellation on all network operations
  • Single runtime dependencygolang.org/x/net/idna (Go official extended library)

Requirements

  • Go 1.25 or later

Installation

go get github.com/optimode/emailkit

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/optimode/emailkit"
)

func main() {
    ctx := context.Background()

    result, err := emailkit.New().
        WithDNS().
        WithDomain().
        WithSMTP(emailkit.SMTPOptions{
            HeloDomain: "myapp.com",
            MailFrom:   "verify@myapp.com",
        }).
        Validate(ctx, "user@example.com")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(result.Valid)    // true or false
    for _, c := range result.Checks {
        fmt.Printf("  [%s] passed=%v  %s\n", c.Level, c.Passed, c.Details)
    }
}

Usage

Every validation level is optional except syntax (which always runs as a prerequisite). Add levels with the With* methods — order doesn't matter, but the pipeline executes them in registration order and short-circuits on the first failure.

Syntax Validation

Validates email format according to RFC 5321/5322 with full internationalization support. Catches malformed addresses, overlong parts, invalid characters, and structural errors before any network calls are made.

This level always runs — it's the foundation for every other check.

result, err := emailkit.New().Validate(ctx, "user@example.com")
// result.Valid == true
// result.Checks[0].Details == "syntax ok"

result, _ = emailkit.New().Validate(ctx, "missing-at-sign")
// result.Valid == false
// result.Checks[0].Details == "invalid email syntax"

result, _ = emailkit.New().Validate(ctx, "user@münchen.de")
// result.Valid == true (IDN domain, converted to Punycode internally)

result, _ = emailkit.New().Validate(ctx, "用户@example.com")
// result.Valid == true (EAI / RFC 6531 Unicode local part)
DNS Validation

Verifies that the email domain has valid MX records — confirming it can actually receive mail. Prevents accepting addresses at domains with no mail infrastructure.

v := emailkit.New().WithDNS()

result, _ := v.Validate(ctx, "user@example.com")
// result.Checks[1].MXHost == "mx.example.com" (primary MX host)

// With A record fallback for domains that use A records instead of MX:
v = emailkit.New().WithDNS(emailkit.DNSOptions{
    Timeout:     10 * time.Second, // default: 5s
    FallbackToA: true,             // default: false
})
Domain Validation

Detects disposable (throwaway) email domains and typos in common provider names. Useful for catching user@gmial.com or blocking user@mailinator.com at the form level.

Disposable detection fails the check. Typo detection never fails — it only populates the Suggestion field so your application can prompt the user ("Did you mean gmail.com?").

v := emailkit.New().WithDomain()

// Disposable domain:
result, _ := v.Validate(ctx, "user@mailinator.com")
// result.Valid == false
// result.Checks[1].Details == "disposable email domain detected"

// Typo detection:
result, _ = v.Validate(ctx, "user@gmial.com")
// result.Valid == true (typo doesn't fail)
// result.Checks[1].Suggestion == "gmail.com"

// Configure sensitivity:
v = emailkit.New().WithDomain(emailkit.DomainOptions{
    CheckDisposable: true, // default: true
    CheckTypos:      true, // default: true
    TypoThreshold:   2,    // default: 2 (Levenshtein distance)
})
SMTP Validation

Performs an SMTP RCPT TO probe against the domain's mail servers to check whether the mailbox actually exists. This is the most thorough validation level — it catches addresses that look valid and have working DNS but where the mailbox doesn't exist.

Connections are pooled and reused via the SMTP RSET command, making bulk validation efficient. Always call Close() when done to release pooled connections.

v := emailkit.New().
    WithDNS().
    WithSMTP(emailkit.SMTPOptions{
        HeloDomain: "myapp.com",        // required: your domain
        MailFrom:   "verify@myapp.com", // required: envelope sender
    })
defer v.Close()

result, _ := v.Validate(ctx, "user@example.com")
// result.Checks[2].SMTPCode == 250 (accepted)
// result.Checks[2].MXHost == "mx.example.com"

// Full options:
v = emailkit.New().WithSMTP(emailkit.SMTPOptions{
    HeloDomain:      "myapp.com",
    MailFrom:        "verify@myapp.com",
    ConnectTimeout:  5 * time.Second,  // default: 5s
    CommandTimeout:  10 * time.Second, // default: 10s
    MaxMXHosts:      2,                // default: 2 (MX hosts to try)
    Port:            "25",             // default: "25"
    MaxConnsPerHost: 3,                // default: 3 (pooled connections per MX host)
})
defer v.Close()
Non-Short-Circuit Validation

By default, Validate() stops at the first failing level. Use ValidateAll() when you need to know exactly which levels pass and which fail — useful for diagnostics or detailed user feedback.

result, _ := v.ValidateAll(ctx, "user@nonexistent-domain.example")
// result.Valid == false
// result.Checks contains results for ALL configured levels, not just the first failure
for _, c := range result.FailedChecks() {
    fmt.Printf("failed: [%s] %s\n", c.Level, c.Details)
}
Bulk Validation

ValidateMany() validates a slice of emails concurrently. Internally, emails are sorted by domain for optimal DNS cache and SMTP connection pool utilization. Result order always matches input order.

v := emailkit.New().
    WithDNS().
    WithDomain().
    WithSMTP(emailkit.SMTPOptions{
        HeloDomain: "myapp.com",
        MailFrom:   "verify@myapp.com",
    })
defer v.Close()

emails := []string{
    "alice@example.com",
    "bob@gmail.com",
    "carol@example.com", // same domain as alice — processed together
}

results, err := v.ValidateMany(ctx, emails, emailkit.ConcurrencyOptions{
    Workers: 10, // default: 5
})
// results[0] corresponds to alice, results[1] to bob, etc.
Inspecting Results

The Result struct provides helpers for examining validation outcomes.

result, _ := v.Validate(ctx, "user@example.com")

// Check overall validity:
result.Valid // true if all checks passed

// Get a specific level's result:
if dns, ok := result.CheckFor(emailkit.LevelDNS); ok {
    fmt.Println(dns.MXHost) // primary MX host
}

// Get all failures:
for _, c := range result.FailedChecks() {
    fmt.Printf("[%s] %s\n", c.Level, c.Details)
}

// JSON serialization (all fields have json tags):
data, _ := json.Marshal(result)

Contributing

Contributions are welcome. Please follow these guidelines:

  1. Fork and branch — create a feature branch from main
  2. Code style — run make check (vet + lint + test) before submitting
  3. Tests — add tests for new functionality; aim to maintain or improve coverage (make cover)
  4. Commits — use Conventional Commits format (e.g. feat: add rate limiting, fix: handle nil MX response)
  5. One concern per PR — keep pull requests focused on a single change
Development
make check      # run vet + lint + tests
make test-race  # run tests with race detector
make cover      # show test coverage report
make tidy       # tidy and verify module dependencies

License

This project is licensed under the BSD 3-Clause License.

Documentation

Overview

Package emailkit is an email validation library that validates email addresses at the syntax, DNS, domain and SMTP levels.

Basic usage:

result, err := emailkit.New().Validate(ctx, "user@example.com")

Full pipeline:

result, err := emailkit.New().
    WithDNS().
    WithDomain().
    WithSMTP(emailkit.SMTPOptions{
        HeloDomain: "myapp.com",
        MailFrom:   "verify@myapp.com",
    }).
    Validate(ctx, "user@example.com")

Index

Examples

Constants

View Source
const (
	LevelSyntax = types.LevelSyntax
	LevelDNS    = types.LevelDNS
	LevelDomain = types.LevelDomain
	LevelSMTP   = types.LevelSMTP
)

Level constants re-exported.

Variables

View Source
var (
	// ErrNoChecksConfigured is returned when Validate() is called
	// but no validation level is configured (not even syntax).
	ErrNoChecksConfigured = errors.New("emailkit: no validation checks configured")

	// ErrInvalidSMTPOptions is returned when WithSMTP is called
	// but HeloDomain or MailFrom is missing.
	ErrInvalidSMTPOptions = errors.New("emailkit: SMTPOptions requires HeloDomain and MailFrom")
)

Functions

This section is empty.

Types

type CheckLevel

type CheckLevel = types.CheckLevel

CheckLevel is a re-export.

type CheckResult

type CheckResult = types.CheckResult

CheckResult is a re-export from the types package so that consumers don't need to import the types package directly.

type ConcurrencyOptions

type ConcurrencyOptions struct {
	// Workers is the number of concurrent goroutines. Default: 5
	Workers int
}

ConcurrencyOptions configures concurrent processing for ValidateMany.

type DNSOptions

type DNSOptions struct {
	// Timeout is the maximum time for MX lookup. Default: 5s
	Timeout time.Duration
	// FallbackToA when true accepts A records when no MX record is found.
	// Default: false (strict MX requirement)
	FallbackToA bool
}

DNSOptions configures the DNS validation level.

type DomainOptions

type DomainOptions struct {
	// CheckDisposable when true fails on known disposable domains. Default: true
	CheckDisposable bool
	// CheckTypos when true suggests corrections for close-match domains. Default: true
	// This never fails an email, only provides a suggestion (Suggestion field).
	CheckTypos bool
	// TypoThreshold is the Levenshtein distance threshold for typo detection. Default: 2
	TypoThreshold int
}

DomainOptions configures the domain-level validation.

type Result

type Result struct {
	Email  string        `json:"email"`
	Valid  bool          `json:"valid"`
	Checks []CheckResult `json:"checks"`
}

Result is the full outcome of an email validation. The Valid field is true only if all configured checks passed.

func (Result) CheckFor

func (r Result) CheckFor(level CheckLevel) (CheckResult, bool)

CheckFor returns the CheckResult for the given level, if it exists. The second return value indicates whether the given level was executed.

Example
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New()
	result, _ := v.Validate(context.Background(), "user@example.com")

	if syntax, ok := result.CheckFor(emailkit.LevelSyntax); ok {
		fmt.Println(syntax.Passed, syntax.Details)
	}
}
Output:
true syntax ok

func (Result) FailedChecks

func (r Result) FailedChecks() []CheckResult

FailedChecks returns those CheckResults that did not pass.

Example
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New()
	result, _ := v.Validate(context.Background(), "missing-at-sign")

	for _, c := range result.FailedChecks() {
		fmt.Printf("[%s] %s\n", c.Level, c.Details)
	}
}
Output:
[syntax] invalid email syntax

type SMTPOptions

type SMTPOptions struct {
	// HeloDomain is the domain sent in the EHLO command. Required, e.g. "myapp.com"
	HeloDomain string
	// MailFrom is the address sent in the MAIL FROM command. Required, e.g. "verify@myapp.com"
	MailFrom string
	// ConnectTimeout is the maximum time for TCP connection. Default: 5s
	ConnectTimeout time.Duration
	// CommandTimeout is the maximum response time for SMTP commands. Default: 10s
	CommandTimeout time.Duration
	// MaxMXHosts is how many MX hosts to try sequentially. Default: 2
	MaxMXHosts int
	// Port is the SMTP port. Default: 25
	Port string
	// MaxConnsPerHost is the max pooled SMTP connections per MX host. Default: 3
	MaxConnsPerHost int
}

SMTPOptions configures the SMTP probe level.

type Validator

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

Validator is the main fluent builder struct. Instantiate with the New() function. When using SMTP validation, call Close() when done to release pooled connections.

func New

func New() *Validator

New creates a new Validator. By default it only performs syntax checking. Syntax checking always runs and cannot be disabled, because a valid email address is a prerequisite for the other levels.

Example
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New()
	result, _ := v.Validate(context.Background(), "user@example.com")
	fmt.Println(result.Valid)
}
Output:
true

func (*Validator) Close

func (v *Validator) Close() error

Close releases resources held by the Validator. Must be called when using SMTP validation to close pooled connections. Safe to call multiple times. No-op if no pooled resources exist.

Example
package main

import (
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New().WithSMTP(emailkit.SMTPOptions{
		HeloDomain: "myapp.com",
		MailFrom:   "verify@myapp.com",
	})
	defer func() { _ = v.Close() }()

	fmt.Println("validator created with SMTP pool")
}
Output:
validator created with SMTP pool

func (*Validator) Validate

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

Validate runs all configured checks on the given email. The pipeline short-circuits: if a level fails, subsequent levels are skipped. Context can be used for timeout or cancellation.

Example
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New()

	result, _ := v.Validate(context.Background(), "user@example.com")
	fmt.Println(result.Valid, result.Checks[0].Details)

	result, _ = v.Validate(context.Background(), "invalid")
	fmt.Println(result.Valid, result.Checks[0].Details)
}
Output:
true syntax ok
false invalid email syntax
Example (Idn)
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New()

	// Internationalized Domain Name (German)
	result, _ := v.Validate(context.Background(), "user@münchen.de")
	fmt.Println(result.Valid)

	// Email Address Internationalization / RFC 6531 (Chinese local part)
	result, _ = v.Validate(context.Background(), "用户@example.com")
	fmt.Println(result.Valid)
}
Output:
true
true

func (*Validator) ValidateAll

func (v *Validator) ValidateAll(ctx context.Context, email string) (Result, error)

ValidateAll runs all checks without short-circuiting. Useful when you want to know exactly which levels fail.

Example
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New()
	result, _ := v.ValidateAll(context.Background(), "bad email")

	for _, c := range result.FailedChecks() {
		fmt.Printf("[%s] %s\n", c.Level, c.Details)
	}
}
Output:
[syntax] invalid email syntax

func (*Validator) ValidateMany

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

ValidateMany validates multiple emails concurrently. The result order matches the input slice order. Emails are sorted by domain internally for optimal DNS cache and SMTP connection pool utilization.

Example
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New()
	emails := []string{"alice@example.com", "invalid", "bob@example.com"}

	results, _ := v.ValidateMany(context.Background(), emails, emailkit.ConcurrencyOptions{
		Workers: 2,
	})

	for _, r := range results {
		fmt.Printf("%-20s valid=%v\n", r.Email, r.Valid)
	}
}
Output:
alice@example.com    valid=true
invalid              valid=false
bob@example.com      valid=true

func (*Validator) WithDNS

func (v *Validator) WithDNS(opts ...DNSOptions) *Validator

WithDNS adds MX lookup validation to the pipeline. Optionally overrides the default DNSOptions. MX lookup results are cached and shared with the SMTP checker.

func (*Validator) WithDomain

func (v *Validator) WithDomain(opts ...DomainOptions) *Validator

WithDomain adds domain-level validation (disposable + typo).

Example
package main

import (
	"context"
	"fmt"

	"github.com/optimode/emailkit"
)

func main() {
	v := emailkit.New().WithDomain()

	// Typo detection (does not fail, populates Suggestion)
	result, _ := v.Validate(context.Background(), "user@gmial.com")
	domain, _ := result.CheckFor(emailkit.LevelDomain)
	fmt.Println(result.Valid, domain.Suggestion)
}
Output:
true gmail.com

func (*Validator) WithSMTP

func (v *Validator) WithSMTP(opts SMTPOptions) *Validator

WithSMTP adds the SMTP RCPT TO probe to the pipeline. SMTPOptions.HeloDomain and MailFrom are required. Uses a connection pool for efficient bulk validation (connections reused via RSET). Call Close() when done to release pooled connections.

Directories

Path Synopsis
_examples
advanced command
basic command
Package check contains the internal validation levels for emailkit.
Package check contains the internal validation levels for emailkit.
internal
dnscache
Package dnscache provides a thread-safe, TTL-based cache for DNS MX lookups with singleflight deduplication for concurrent requests to the same domain.
Package dnscache provides a thread-safe, TTL-based cache for DNS MX lookups with singleflight deduplication for concurrent requests to the same domain.
smtppool
Package smtppool provides a thread-safe SMTP connection pool that reuses TCP connections via the RSET command for efficient bulk email validation.
Package smtppool provides a thread-safe SMTP connection pool that reuses TCP connections via the RSET command for efficient bulk email validation.
Package types contains the shared types for emailkit.
Package types contains the shared types for emailkit.

Jump to

Keyboard shortcuts

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