email

package
v1.11.2 Latest Latest
Warning

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

Go to latest
Published: Jun 29, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package email provides email address validation and normalization for authcore.

Validation follows RFC 5321 and RFC 5322 rules:

  • Total length 1–254 characters
  • Exactly one @ separating a non-empty local part and domain
  • Local part ≤ 64 characters
  • Domain contains at least one dot; no leading, trailing, or consecutive dots
  • Each domain label is 1–63 characters

Internationalised domain names (IDN) are supported: a Unicode domain such as "münchen.de" is converted to its ASCII punycode form ("xn--mnchen-3ya.de") during normalisation, matching what DNS actually resolves. Store the canonical ASCII form in your database so lookups are deterministic.

The single entry point is Email.ValidateAndNormalize — it normalizes (lowercase + trim + punycode) and validates in one step, returning the canonical form. Always store and query emails using this canonical form:

emailMod, _ := email.New(auth)

// Registration
normalized, err := emailMod.ValidateAndNormalize(req.Email)
if err != nil {
    c.JSON(400, map[string]string{"error": errors.Unwrap(err).Error()})
    return
}
db.StoreUser(normalized, ...)

// Login lookup — same call, same canonical form, consistent results
normalized, err = emailMod.ValidateAndNormalize(req.Email)
if err != nil { ... }
user := db.FindByEmail(normalized)

Domain MX verification

VerifyDomain performs an optional DNS MX lookup to confirm the domain can receive email. Results are cached per domain using the DNS TTL (capped at DefaultCacheTTL) to avoid repeated lookups for the same domain. This check is network I/O — always call it after ValidateAndNormalize and handle ErrDomainUnresolvable as a soft failure:

err = emailMod.VerifyDomain(ctx, normalized)
if errors.Is(err, email.ErrDomainNoMX) {
    c.JSON(400, map[string]string{"error": "email domain cannot receive messages"})
    return
}
if errors.Is(err, email.ErrDomainUnresolvable) {
    log.Warn("DNS check unavailable: %v", err) // do not block the user
}

Index

Examples

Constants

View Source
const DefaultCacheTTL = 5 * time.Minute

DefaultCacheTTL is the maximum duration a domain MX lookup result is cached. Tune this value if your workload has strict freshness requirements.

Variables

View Source
var ErrDomainNoMX = errors.New("email: domain has no MX records")

ErrDomainNoMX is CLIENT-SAFE: the domain exists but has no MX records, meaning it cannot receive email. Safe to return as a 400 response:

if errors.Is(err, email.ErrDomainNoMX) {
    c.JSON(400, map[string]string{"error": "email domain cannot receive messages"})
    return
}
View Source
var ErrDomainUnresolvable = errors.New("email: domain could not be resolved")

ErrDomainUnresolvable is INTERNAL: the DNS lookup failed due to a network error, timeout, or resolver unavailability. Do NOT block the user on this — log the error and let the request proceed:

if errors.Is(err, email.ErrDomainUnresolvable) {
    log.Warn("DNS check failed, skipping: %v", err)
    // continue — don't block the user
}
View Source
var ErrInvalidEmail = errors.New("email: invalid address")

ErrInvalidEmail signals that an address failed RFC 5321/5322 validation.

CLIENT-SAFE: the wrapped reason describes exactly which rule failed and is suitable for returning in a 400 response:

normalized, err := emailMod.ValidateAndNormalize(req.Email)
if err != nil {
    c.JSON(400, map[string]string{"error": errors.Unwrap(err).Error()})
    return
}

Use errors.Is to check for this in calling code.

Functions

This section is empty.

Types

type Email

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

Email is the email validation and normalization module. Create one instance at startup via New and reuse it — it is safe for concurrent use after construction. It owns no background goroutine and needs no cleanup; like the other modules, you construct it and use it.

func New

func New(p authcore.Provider) (*Email, error)

New creates an Email module using the provider's logger.

emailMod, err := email.New(auth)
if err != nil { ... }
Example
package main

import (
	"fmt"
	"os"

	"github.com/Glyndor/authcore"
	"github.com/Glyndor/authcore/auth/email"
)

func main() {
	dir, _ := os.MkdirTemp("", "authcore-example-")
	defer func() { _ = os.RemoveAll(dir) }()

	auth, _ := authcore.New(authcore.Config{EnableLogs: false, KeysDir: dir})
	mod, err := email.New(auth)
	if err != nil {
		panic(err)
	}
	defer mod.Close()
	fmt.Println(mod.Name())
}
Output:
email

func (*Email) Close

func (e *Email) Close()

Close is a no-op retained for backward compatibility.

The module no longer runs a background goroutine: the MX cache evicts expired entries lazily when it fills (see store), and reads always ignore expired entries. Nothing needs to be released, so calling Close is optional and always safe — including multiple times and from multiple goroutines.

func (*Email) Name

func (e *Email) Name() string

Name implements authcore.Module.

func (*Email) ValidateAndNormalize

func (e *Email) ValidateAndNormalize(address string) (string, error)

ValidateAndNormalize is the single entry point for email validation. It lowercases, trims surrounding whitespace, and validates the address against RFC 5321 / RFC 5322 rules in one atomic step.

Always use this function — never normalize and validate separately. The returned string is the canonical form that must be stored and queried:

normalized, err := emailMod.ValidateAndNormalize(req.Email)
if err != nil {
    // errors.Unwrap(err).Error() contains the specific rule that failed.
    c.JSON(400, map[string]string{"error": errors.Unwrap(err).Error()})
    return
}
db.StoreUser(normalized, ...) // always lowercase, trimmed, validated
Example
package main

import (
	"fmt"
	"os"

	"github.com/Glyndor/authcore"
	"github.com/Glyndor/authcore/auth/email"
)

func main() {
	dir, _ := os.MkdirTemp("", "authcore-example-")
	defer func() { _ = os.RemoveAll(dir) }()

	auth, _ := authcore.New(authcore.Config{EnableLogs: false, KeysDir: dir})
	mod, _ := email.New(auth)
	defer mod.Close()

	got, err := mod.ValidateAndNormalize("  User@Example.COM  ")
	if err != nil {
		fmt.Println("invalid:", err)
		return
	}
	fmt.Println(got)
}
Output:
user@example.com

func (*Email) VerifyDomain

func (e *Email) VerifyDomain(ctx context.Context, address string) error

VerifyDomain performs a DNS MX lookup to confirm that the email's domain can receive messages. It is an optional, network-bound complement to ValidateAndNormalize — call it only after format validation succeeds.

Results are cached per domain for the duration of the DNS TTL, capped at DefaultCacheTTL, to avoid repeated lookups for the same domain.

The cache and the single-flight de-duplication bound repeated and concurrent lookups for the SAME domain, but each uncached distinct domain still costs one outbound DNS query — a flood of unique domains is not bounded here. Rate-limit the endpoint that calls VerifyDomain (registration) so an attacker cannot turn it into a DNS-amplification vector.

On success it returns nil. On failure it returns one of:

[ErrDomainNoMX]         — domain exists but has no MX records (CLIENT-SAFE, return 400)
[ErrDomainUnresolvable] — DNS lookup failed; treat as a soft failure and do not block the user

ctx controls the deadline of the DNS query. Use a short timeout (1–3 s) to avoid slowing down your registration endpoint:

ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
if err := emailMod.VerifyDomain(ctx, normalized); errors.Is(err, email.ErrDomainNoMX) {
    c.JSON(400, map[string]string{"error": "email domain cannot receive messages"})
    return
}
Example
package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/Glyndor/authcore"
	"github.com/Glyndor/authcore/auth/email"
)

func main() {
	dir, _ := os.MkdirTemp("", "authcore-example-")
	defer func() { _ = os.RemoveAll(dir) }()

	auth, _ := authcore.New(authcore.Config{EnableLogs: false, KeysDir: dir})
	mod, _ := email.New(auth)
	defer mod.Close()

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

	err := mod.VerifyDomain(ctx, "user@example.com")
	switch {
	case errors.Is(err, email.ErrDomainNoMX):
		fmt.Println("no MX records — block registration")
	case errors.Is(err, email.ErrDomainUnresolvable):
		fmt.Println("DNS soft failure — log and proceed")
	case err != nil:
		fmt.Println("error:", err)
	default:
		fmt.Println("MX OK")
	}
}

Jump to

Keyboard shortcuts

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