email: validate with net/mail.ParseAddress

Replace the RFC 5322 mega-regexp with net/mail.ParseAddress, which is the
standard library's address parser and far easier to audit. A bare address
is required: inputs with a display name, angle brackets, a comment, or
trailing content are rejected, as is an address list.

Behaviour change: ParseAddress does not require the domain to contain a
dot, so "alice@localhost" now validates. Layer Match or a DNS lookup on
top if a stricter domain is needed.

Also rewrites FuzzEmail, which previously asserted that *every* input is
invalid, into checks that Email never returns an unexpected error type,
never panics, and gives a stable verdict.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit was merged in pull request #8.
This commit is contained in:
2026-09-07 12:53:02 +00:00
committed by aneurin
co-authored by Claude Sonnet 5
parent 82611a5e96
commit a992065d6c
2 changed files with 35 additions and 13 deletions
+25 -7
View File
@@ -12,14 +12,26 @@ func ExampleEmail() {
}
func FuzzEmail(f *testing.F) {
want := ErrInvalidEmail
for _, seed := range []string{
"", "alice@example.com", "not an email", "a@b",
"Alice <alice@example.com>", "alice@example.com ",
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, input string) {
got := Email(input)
err := Email(input)
if !errors.Is(got, want) {
t.Error("got", got)
t.Error("want", want)
// The only error Email ever returns is ErrInvalidEmail.
if err != nil && !errors.Is(err, ErrInvalidEmail) {
t.Fatalf("unexpected error for %q: %v", input, err)
}
// A verdict of "valid" must be stable on re-validation.
if err == nil {
if err2 := Email(input); err2 != nil {
t.Fatalf("inconsistent verdict for %q: first nil, then %v", input, err2)
}
}
})
}
@@ -28,9 +40,15 @@ func TestEmail(t *testing.T) {
testCases := map[string]error{
"test@example.com": nil,
"firstname.lastname@some-website.co.uk": nil,
"alice+tag@example.com": nil,
"alice@localhost": nil, // ParseAddress does not require a dotted domain
"not an email": ErrInvalidEmail,
"testexample.com": ErrInvalidEmail,
"not an email": ErrInvalidEmail,
"testexample.com": ErrInvalidEmail,
"Alice <alice@example.com>": ErrInvalidEmail, // display name
"<alice@example.com>": ErrInvalidEmail, // angle brackets
"alice@example.com ": ErrInvalidEmail, // trailing space
"a@b.com, c@d.com": ErrInvalidEmail, // address list
}
for input, want := range testCases {