├── .travis.yml ├── LICENSE ├── README.md ├── domain.go ├── domain_test.go ├── email.go └── email_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | - tip 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Dmitry Chestnykh 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials 14 | provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # validator 2 | 3 | [![Build Status](https://travis-ci.org/dchest/validator.png)](https://travis-ci.org/dchest/validator) 4 | 5 | Go package validator validates and normalizes email addresses and domain names. 6 | 7 | ## Installation 8 | 9 | ``` 10 | $ go get github.com/dchest/validator 11 | ``` 12 | 13 | ## Documentation 14 | 15 | 16 | -------------------------------------------------------------------------------- /domain.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var domainRegexp = regexp.MustCompile(`^(?i)[a-z0-9-]+(\.[a-z0-9-]+)+\.?$`) 11 | 12 | // IsValidDomain returns true if the domain is valid. 13 | // 14 | // It uses a simple regular expression to check the domain validity. 15 | func IsValidDomain(domain string) bool { 16 | return domainRegexp.MatchString(domain) 17 | } 18 | 19 | var ErrInvalidDomain = errors.New("invalid domain") 20 | 21 | // ValidateDomainByResolvingIt queries DNS for the given domain name, 22 | // and returns nil if the the name resolves, or an error. 23 | func ValidateDomainByResolvingIt(domain string) error { 24 | if !IsValidDomain(domain) { 25 | return ErrInvalidDomain 26 | } 27 | addr, err := net.LookupHost(domain) 28 | if err != nil { 29 | return err 30 | } 31 | if len(addr) == 0 { 32 | return ErrInvalidDomain 33 | } 34 | return nil 35 | } 36 | 37 | // NormalizeDomain returns a normalized domain. 38 | // It returns an empty string if the domain is not valid. 39 | func NormalizeDomain(domain string) string { 40 | // Trim whitespace. 41 | domain = strings.TrimSpace(domain) 42 | // Check validity. 43 | if !IsValidDomain(domain) { 44 | return "" 45 | } 46 | // Remove trailing dot. 47 | domain = strings.TrimRight(domain, ".") 48 | // Convert to lower case. 49 | domain = strings.ToLower(domain) 50 | return domain 51 | } 52 | -------------------------------------------------------------------------------- /domain_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Dmitry Chestnykh. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package validator 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | // Validation tests. 12 | 13 | var validDomains = []string{ 14 | "www.codingrobots.com", 15 | "google.com", 16 | "sub.domain.example.com", 17 | "another.valid.com.", 18 | } 19 | 20 | var invalidDomains = []string{ 21 | "", 22 | "local", 23 | ".example.com", 24 | "президент.рф", // must be in IDNA encoded form 25 | "invalid..", 26 | } 27 | 28 | func TestIsValidDomain(t *testing.T) { 29 | for i, v := range validDomains { 30 | if !IsValidDomain(v) { 31 | t.Errorf("%d: didn't accept valid domain: %s", i, v) 32 | } 33 | } 34 | for i, v := range invalidDomains { 35 | if IsValidDomain(v) { 36 | t.Errorf("%d: accepted invalid domain: %s", i, v) 37 | } 38 | } 39 | } 40 | 41 | func TestValidateDomainByResolvingIt(t *testing.T) { 42 | err := ValidateDomainByResolvingIt("www.example.com") 43 | if err != nil { 44 | t.Errorf("%s", err) 45 | } 46 | err = ValidateDomainByResolvingIt("randomdomainnamethatdoesntexist") 47 | if err == nil { 48 | t.Errorf("invalid domain name validated") 49 | } 50 | } 51 | 52 | // Normalization tests. 53 | 54 | var sameDomains = []string{ 55 | "www.example.com", 56 | "www.EXAMPLE.COM", 57 | "www.example.com.", 58 | "WWW.exampLE.CoM", 59 | } 60 | 61 | var differentDomains = []string{ 62 | "www.example.com", 63 | "example.com", 64 | } 65 | 66 | func TestNormalizeDomain(t *testing.T) { 67 | for i, v0 := range sameDomains { 68 | for j, v1 := range sameDomains { 69 | if i == j { 70 | continue 71 | } 72 | nv0 := NormalizeDomain(v0) 73 | nv1 := NormalizeDomain(v1) 74 | if nv0 == "" { 75 | t.Errorf("%d: domain invalid: %q", i, v0) 76 | } 77 | if nv0 != nv1 { 78 | t.Errorf("%d-%d: normalized domains differ: %q and %q", i, j, nv0, nv1) 79 | } 80 | } 81 | } 82 | for i, v0 := range differentDomains { 83 | for j, v1 := range differentDomains { 84 | if i == j { 85 | continue 86 | } 87 | nv0 := NormalizeDomain(v0) 88 | nv1 := NormalizeDomain(v1) 89 | if nv0 == "" { 90 | t.Errorf("%d: domain invalid: %q", i, v0) 91 | } 92 | if nv0 == nv1 { 93 | t.Errorf("%d-%d: normalized domains are the same: %q and %q", i, j, nv0, nv1) 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Dmitry Chestnykh. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package validator validates and normalizes email addresses and domain names. 6 | package validator 7 | 8 | import ( 9 | "errors" 10 | "net" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | // Regular expression from WebCore's HTML5 email input: http://goo.gl/7SZbzj 16 | var emailRegexp = regexp.MustCompile("(?i)" + // case insensitive 17 | "^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+" + // local part 18 | "@" + 19 | "[a-z0-9-]+(\\.[a-z0-9-]+)*$") // domain part 20 | 21 | // IsValidEmail returns true if the given string is a valid email address. 22 | // 23 | // It uses a simple regular expression to check the address validity. 24 | func IsValidEmail(email string) bool { 25 | if len(email) > 254 { 26 | return false 27 | } 28 | return emailRegexp.MatchString(email) 29 | } 30 | 31 | var ErrInvalidEmail = errors.New("invalid email address") 32 | 33 | // fetchMXRecords fetches MX records for the domain of the given email. 34 | func fetchMXRecords(email string) ([]*net.MX, error) { 35 | if !IsValidEmail(email) { 36 | return nil, ErrInvalidEmail 37 | } 38 | // Extract domain. 39 | _, domain, ok := splitEmail(email) 40 | if !ok { 41 | return nil, ErrInvalidEmail 42 | } 43 | if !IsValidDomain(domain) { 44 | return nil, ErrInvalidEmail 45 | } 46 | mx, err := net.LookupMX(domain) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return mx, nil 51 | } 52 | 53 | // ValidateEmailByResolvingDomain validates email address by looking up MX 54 | // records on its domain. 55 | // 56 | // This function can return various DNS errors, which may be temporary 57 | // (e.g. caused by internet connection being offline) or permanent, 58 | // e.g. the host doesn't exist or has no MX records. 59 | // 60 | // The function returns nil if email is valid and its domain can 61 | // accept email messages (however this doesn't guarantee that a user 62 | // with such email address exists on the host). 63 | func ValidateEmailByResolvingDomain(email string) error { 64 | mx, err := fetchMXRecords(email) 65 | if err != nil { 66 | return err 67 | } 68 | if len(mx) == 0 { 69 | return ErrInvalidEmail 70 | } 71 | return nil 72 | } 73 | 74 | // splitEmail splits email address into local and domain parts. 75 | // The last returned value is false if splitting fails. 76 | func splitEmail(email string) (local string, domain string, ok bool) { 77 | parts := strings.Split(email, "@") 78 | if len(parts) < 2 { 79 | return 80 | } 81 | local = parts[0] 82 | domain = parts[1] 83 | // Check that the parts contain enough characters. 84 | if len(local) < 1 { 85 | return 86 | } 87 | if len(domain) < len("x.xx") { 88 | return 89 | } 90 | return local, domain, true 91 | } 92 | 93 | // NormalizeEmail returns a normalized email address. 94 | // It returns an empty string if the email is not valid. 95 | func NormalizeEmail(email string) string { 96 | // Trim whitespace. 97 | email = strings.TrimSpace(email) 98 | // Make sure it is valid. 99 | if !IsValidEmail(email) { 100 | return "" 101 | } 102 | // Split email into parts. 103 | local, domain, ok := splitEmail(email) 104 | if !ok { 105 | return "" 106 | } 107 | // Remove trailing dot from domain. 108 | domain = strings.TrimRight(domain, ".") 109 | // Convert domain to lower case. 110 | domain = strings.ToLower(domain) 111 | // Combine and return the result. 112 | return local + "@" + domain 113 | } 114 | -------------------------------------------------------------------------------- /email_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Dmitry Chestnykh. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package validator 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | // Validation tests. 12 | 13 | var validEmails = []string{ 14 | "a@example.com", 15 | "postmaster@example.com", 16 | "president@kremlin.gov.ru", 17 | "example@example.co.uk", 18 | } 19 | 20 | var invalidEmails = []string{ 21 | "", 22 | "example", 23 | "example.com", 24 | ".com", 25 | "адрес@пример.рф", 26 | " space_before@example.com", 27 | "space between@example.com", 28 | "\nnewlinebefore@example.com", 29 | "newline\nbetween@example.com", 30 | "test@example.com.", 31 | "asyouallcanseethisemailaddressexceedsthemaximumnumberofcharactersallowedtobeintheemailaddresswhichisnomorethatn254accordingtovariousrfcokaycanistopnowornotyetnoineedmorecharacterstoadd@i.really.cannot.thinkof.what.else.to.put.into.this.invalid.address.net", 32 | } 33 | 34 | func TestIsValidEmail(t *testing.T) { 35 | for i, v := range validEmails { 36 | if !IsValidEmail(v) { 37 | t.Errorf("%d: didn't accept valid email: %s", i, v) 38 | } 39 | } 40 | for i, v := range invalidEmails { 41 | if IsValidEmail(v) { 42 | t.Errorf("%d: accepted invalid email: %s", i, v) 43 | } 44 | } 45 | } 46 | 47 | func TestValidateEmailByResolvingDomain(t *testing.T) { 48 | err := ValidateEmailByResolvingDomain("abuse@gmail.com") 49 | if err != nil { 50 | t.Errorf("%s", err) 51 | } 52 | err = ValidateEmailByResolvingDomain("nomx@api.codingrobots.com") 53 | if err == nil { 54 | t.Errorf("invalid email address validated") 55 | } 56 | } 57 | 58 | // Normalization tests. 59 | 60 | var sameEmails = []string{ 61 | "test@example.com", 62 | "test@EXAMPLE.COM", 63 | "test@ExAmpLE.com", 64 | } 65 | 66 | var differentEmails = []string{ 67 | "test@example.com", 68 | "TEST@example.com", 69 | "president@whitehouse.gov", 70 | } 71 | 72 | func TestNormalizeEmail(t *testing.T) { 73 | for i, v0 := range sameEmails { 74 | for j, v1 := range sameEmails { 75 | if i == j { 76 | continue 77 | } 78 | nv0 := NormalizeEmail(v0) 79 | nv1 := NormalizeEmail(v1) 80 | if nv0 == "" { 81 | t.Errorf("%d: email invalid: %q", i, v0) 82 | } 83 | if nv0 != nv1 { 84 | t.Errorf("%d-%d: normalized emails differ: %q and %q", i, j, nv0, nv1) 85 | } 86 | } 87 | } 88 | for i, v0 := range differentEmails { 89 | for j, v1 := range differentEmails { 90 | if i == j { 91 | continue 92 | } 93 | nv0 := NormalizeEmail(v0) 94 | nv1 := NormalizeEmail(v1) 95 | if nv0 == "" { 96 | t.Errorf("%d: email invalid: %q", i, v0) 97 | } 98 | if nv0 == nv1 { 99 | t.Errorf("%d-%d: normalized emails are the same: %q and %q", i, j, nv0, nv1) 100 | } 101 | } 102 | } 103 | } 104 | --------------------------------------------------------------------------------