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:
@@ -1,19 +1,23 @@
|
|||||||
package validate
|
package validate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"net/mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidEmail = NewError("invalid email address")
|
ErrInvalidEmail = NewError("invalid email address")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/a/201378
|
// Email validates an email address using net/mail.ParseAddress.
|
||||||
var emailRegexp = regexp.MustCompile("^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\\])$")
|
//
|
||||||
|
// Only a bare address is accepted (alice@example.com). Anything with a
|
||||||
// Email validates an email address.
|
// display name, angle brackets, a comment, or trailing content is rejected,
|
||||||
|
// as is a list of addresses. Note that ParseAddress does not require the
|
||||||
|
// domain to have a dot, so "alice@localhost" is considered valid; layer on
|
||||||
|
// Match or a DNS check if you need to be stricter.
|
||||||
func Email(value string) error {
|
func Email(value string) error {
|
||||||
if !emailRegexp.MatchString(value) {
|
addr, err := mail.ParseAddress(value)
|
||||||
|
if err != nil || addr.Name != "" || addr.Address != value {
|
||||||
return ErrInvalidEmail
|
return ErrInvalidEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-7
@@ -12,14 +12,26 @@ func ExampleEmail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FuzzEmail(f *testing.F) {
|
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) {
|
f.Fuzz(func(t *testing.T, input string) {
|
||||||
got := Email(input)
|
err := Email(input)
|
||||||
|
|
||||||
if !errors.Is(got, want) {
|
// The only error Email ever returns is ErrInvalidEmail.
|
||||||
t.Error("got", got)
|
if err != nil && !errors.Is(err, ErrInvalidEmail) {
|
||||||
t.Error("want", want)
|
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{
|
testCases := map[string]error{
|
||||||
"test@example.com": nil,
|
"test@example.com": nil,
|
||||||
"firstname.lastname@some-website.co.uk": 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,
|
"not an email": ErrInvalidEmail,
|
||||||
"testexample.com": 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 {
|
for input, want := range testCases {
|
||||||
|
|||||||
Reference in New Issue
Block a user