forked from tangled.org/core
Monorepo for Tangled

appview/email: replace regex check with mail.ParseAddress

Signed-off-by: Evan Jarrett <evan@evanjarrett.com>

authored by evan.jarrett.net and committed by Tangled 59f43eb1 a4ee430c

Changed files
+60 -12
appview
+7 -12
appview/email/email.go
··· 3 3 import ( 4 4 "fmt" 5 5 "net" 6 - "regexp" 6 + "net/mail" 7 7 "strings" 8 8 9 9 "github.com/resend/resend-go/v2" ··· 34 34 } 35 35 36 36 func IsValidEmail(email string) bool { 37 - // Basic length check 38 - if len(email) < 3 || len(email) > 254 { 37 + // Reject whitespace (ParseAddress normalizes it away) 38 + if strings.ContainsAny(email, " \t\n\r") { 39 39 return false 40 40 } 41 41 42 - // Regular expression for email validation (RFC 5322 compliant) 43 - pattern := `^[a-zA-Z0-9.!#$%&'*+/=?^_\x60{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$` 44 - 45 - // Compile regex 46 - regex := regexp.MustCompile(pattern) 47 - 48 - // Check if email matches regex pattern 49 - if !regex.MatchString(email) { 42 + // Use stdlib RFC 5322 parser 43 + addr, err := mail.ParseAddress(email) 44 + if err != nil { 50 45 return false 51 46 } 52 47 53 48 // Split email into local and domain parts 54 - parts := strings.Split(email, "@") 49 + parts := strings.Split(addr.Address, "@") 55 50 domain := parts[1] 56 51 57 52 mx, err := net.LookupMX(domain)
+53
appview/email/email_test.go
··· 1 + package email 2 + 3 + import ( 4 + "testing" 5 + ) 6 + 7 + func TestIsValidEmail(t *testing.T) { 8 + tests := []struct { 9 + name string 10 + email string 11 + want bool 12 + }{ 13 + // Valid emails using RFC 2606 reserved domains 14 + {"standard email", "user@example.com", true}, 15 + {"single char local", "a@example.com", true}, 16 + {"dot in middle", "first.last@example.com", true}, 17 + {"multiple dots", "a.b.c@example.com", true}, 18 + {"plus tag", "user+tag@example.com", true}, 19 + {"numbers", "user123@example.com", true}, 20 + {"example.org", "user@example.org", true}, 21 + {"example.net", "user@example.net", true}, 22 + 23 + // Invalid format - rejected by mail.ParseAddress 24 + {"empty string", "", false}, 25 + {"no at sign", "userexample.com", false}, 26 + {"no domain", "user@", false}, 27 + {"no local part", "@example.com", false}, 28 + {"double at", "user@@example.com", false}, 29 + {"just at sign", "@", false}, 30 + {"leading dot", ".user@example.com", false}, 31 + {"trailing dot", "user.@example.com", false}, 32 + {"consecutive dots", "user..name@example.com", false}, 33 + 34 + // Whitespace - rejected before parsing 35 + {"space in local", "user @example.com", false}, 36 + {"space in domain", "user@ example.com", false}, 37 + {"tab", "user\t@example.com", false}, 38 + {"newline", "user\n@example.com", false}, 39 + 40 + // MX lookup - using RFC 2606 reserved TLDs (guaranteed no MX) 41 + {"invalid TLD", "user@example.invalid", false}, 42 + {"test TLD", "user@mail.test", false}, 43 + } 44 + 45 + for _, tt := range tests { 46 + t.Run(tt.name, func(t *testing.T) { 47 + got := IsValidEmail(tt.email) 48 + if got != tt.want { 49 + t.Errorf("IsValidEmail(%q) = %v, want %v", tt.email, got, tt.want) 50 + } 51 + }) 52 + } 53 + }