Skip to content

Commit

Permalink
Email checker is added. Fixes #28 (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
cinar committed Jun 19, 2023
1 parent 4b34bf5 commit 6653f73
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ This package currently provides the following checkers:
- [ascii](doc/checkers/ascii.md) checks if the given string consists of only ASCII characters.
- [cidr](doc/checkers/cidr.md) checker checks if the value is a valid CIDR notation IP address and prefix length.
- [digits](doc/checkers/digits.md) checks if the given string consists of only digit characters.
- [email](doc/checkers/email.md) checks if the given string is an email address.
- [fqdn](doc/checkers/fqdn.md) checks if the given string is a fully qualified domain name.
- [ip](doc/checkers/ip.md) checks if the given value is an IP address.
- [ipv4](doc/checkers/ipv4.md) checks if the given value is an IPv4 address.
Expand Down
1 change: 1 addition & 0 deletions checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var makers = map[string]MakeFunc{
CheckerAscii: makeAscii,
CheckerCidr: makeCidr,
CheckerDigits: makeDigits,
CheckerEmail: makeEmail,
CheckerFqdn: makeFqdn,
CheckerIp: makeIp,
CheckerIpV4: makeIpV4,
Expand Down
28 changes: 28 additions & 0 deletions doc/checkers/email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Email Checker

The ```email``` checker checks if the given string is an email address. If the given string is not a valid email address, the checker will return the ```NOT_EMAIL``` result. Here is an example:

```golang
type User struct {
Email string `checkers:"email"`
}

user := &User{
Email: "[email protected]",
}

_, valid := Check(user)
if !valid {
// Send the mistakes back to the user
}
```

In your custom checkers, you can call the ```ascii``` checker function ```IsAscii``` to validate the user input. Here is an example:

```golang
result := IsEmail("[email protected]")

if result != ResultValid {
// Send the mistakes back to the user
}
```
114 changes: 114 additions & 0 deletions email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package checker

import (
"reflect"
"regexp"
"strings"
)

// CheckerEmail is the name of the checker.
const CheckerEmail = "email"

// ResultNotFqdn indicates that the given string is not a valid email.
const ResultNotEmail = "NOT_EMAIL"

// ipV6Prefix is the IPv6 prefix for the domain.
const ipV6Prefix = "[IPv6:"

// notQuotedChars is the valid not quoted characters.
var notQuotedChars = regexp.MustCompile("[a-zA-Z0-9!#$%&'*+-/=?^_`{|}~]")

// IsEmail checks if the given string is an email address.
func IsEmail(email string) Result {
atIndex := strings.LastIndex(email, "@")
if atIndex == -1 || atIndex == len(email)-1 {
return ResultNotEmail
}

domain := email[atIndex+1:]
if isValidEmailDomain(domain) != ResultValid {
return ResultNotEmail
}

user := email[:atIndex]

// Cannot be empty user
if len(user) == 0 || len(user) > 64 {
return ResultNotEmail
}

// Cannot start or end with dot
if user[0] == '.' || user[len(user)-1] == '.' {
return ResultNotEmail
}

quoted := false
start := true
prev := ' '

for _, c := range user {
// Cannot have a double dot unless quoted
if !quoted && c == '.' && prev == '.' {
return ResultNotEmail
}

if start {
start = false

if c == '"' {
quoted = true
prev = c
continue
}
}

if !quoted {
if c == '.' {
start = true
} else if !notQuotedChars.MatchString(string(c)) {
return ResultNotEmail
}
} else {
if c == '"' && prev != '\\' {
quoted = false
}
}

prev = c
}

return ResultValid
}

// makeEmail makes a checker function for the email checker.
func makeEmail(_ string) CheckFunc {
return checkEmail
}

// checkEmail checks if the given string is an email address.
func checkEmail(value, _ reflect.Value) Result {
if value.Kind() != reflect.String {
panic("string expected")
}

return IsEmail(value.String())
}

// isValidEmailDomain checks if the email domain is a IPv4 or IPv6 address, or a FQDN.
func isValidEmailDomain(domain string) Result {
if len(domain) > 255 {
return ResultNotEmail
}

if domain[0] == '[' {
if strings.HasPrefix(domain, ipV6Prefix) {
// postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]
return IsIpV6(domain[len(ipV6Prefix) : len(domain)-1])
}

// postmaster@[123.123.123.123]
return IsIpV4(domain[1 : len(domain)-1])
}

return IsFqdn(domain)
}
83 changes: 83 additions & 0 deletions email_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package checker

import "testing"

func TestCheckEmailNonString(t *testing.T) {
defer FailIfNoPanic(t)

type User struct {
Email int `checkers:"email"`
}

user := &User{}

Check(user)
}

func TestCheckEmailValid(t *testing.T) {
type User struct {
Email string `checkers:"email"`
}

user := &User{
Email: "[email protected]",
}

_, valid := Check(user)
if !valid {
t.Fail()
}
}

func TestIsEmailValid(t *testing.T) {
validEmails := []string{
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"test/[email protected]",
"[email protected]",
"\" \"@example.org",
"\"john..doe\"@example.org",
"[email protected]",
"\"very.(),:;<>[]\\\".VERY.\\\"very@\\\\ \\\"very\\\".unusual\"@strange.example.com",
"user%[email protected]",
"[email protected]",
"postmaster@[123.123.123.123]",
"postmaster@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
}

for _, email := range validEmails {
if IsEmail(email) != ResultValid {
t.Fatal(email)
}
}
}

func TestIsEmailInvalid(t *testing.T) {
validEmails := []string{
"Abc.example.com",
"A@b@[email protected]",
"a\"b(c)d,e:f;g<h>i[j\\k][email protected]",
"just\"not\"[email protected]",
"this is\"not\\[email protected]",
"this\\ still\\\"not\\\\[email protected]",
"1234567890123456789012345678901234567890123456789012345678901234+x@example.com",
"i_like_underscore@but_its_not_allowed_in_this_part.example.com",
"QA[icon]CHOCOLATE[icon]@test.com",
"[email protected]",
"[email protected]",
"[email protected]",
"user@domaincannotbemorethan255charactersdomaincannotbemorethan255charactersdomaincannotbemorethan255charactersdomaincannotbemorethan255charactersdomaincannotbemorethan255charactersdomaincannotbemorethan255charactersdomaincannotbemorethan255charactersdomaincannotbemorethan255characters.com",
}

for _, email := range validEmails {
if IsEmail(email) == ResultValid {
t.Fatal(email)
}
}
}
4 changes: 2 additions & 2 deletions fqdn.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
// CheckerFqdn is the name of the checker.
const CheckerFqdn = "fqdn"

// ResultNotFqdn indicates that the given string contains non-ASCII characters.
// ResultNotFqdn indicates that the given string is not a valid FQDN.
const ResultNotFqdn = "NOT_FQDN"

// Valid characters excluding full-width characters.
Expand Down Expand Up @@ -56,7 +56,7 @@ func IsFqdn(domain string) Result {
return ResultValid
}

// makeFqdn makes a checker function for the ascii checker.
// makeFqdn makes a checker function for the fqdn checker.
func makeFqdn(_ string) CheckFunc {
return checkFqdn
}
Expand Down

0 comments on commit 6653f73

Please sign in to comment.