如何在Go中验证电子邮件地址

50

我查阅了StackOverflow,未找到任何回答如何在Go语言中验证电子邮件的问题。

经过一些研究,我发现并解决了我的需求。

我有这个正则表达式Go函数,它们能够很好地工作:

import (
    "fmt"
    "regexp"
)

func main() {
    fmt.Println(isEmailValid("test44@gmail.com")) // true
    fmt.Println(isEmailValid("test$@gmail.com")) // true -- expected "false" 
}


// isEmailValid checks if the email provided is valid by regex.
func isEmailValid(e string) bool {
    emailRegex := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[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])?)*$")
    return emailRegex.MatchString(e)
}

问题在于它接受了我不想要的特殊字符。我尝试使用其他语言的“regex”表达式,但是调试时会抛出错误"unknown escape"

有人能给我一个适用于GoLang的好的regex或任何快速解决方案(pkg)吗?


https://github.com/goware/emailx - leoOrion
https://github.com/badoux/checkmail - leoOrion
3
  1. 这种语言叫做Go。
  2. 无论用什么语言,正则表达式和电子邮件地址都不太搭配。
- Volker
7个回答

113
标准库已经内置了电子邮件解析和验证功能,只需使用:mail.ParseAddress()
一个简单的“有效性测试”:
func valid(email string) bool {
    _, err := mail.ParseAddress(email)
    return err == nil
}

测试一下:

for _, email := range []string{
    "good@exmaple.com",
    "bad-example",
} {
    fmt.Printf("%18s valid: %t\n", email, valid(email))
}

输出结果如下(在Go Playground上试一下):

  good@exmaple.com valid: true
       bad-example valid: false

注意:

net/mail 包实现并遵循 RFC 5322 规范(和 RFC 6532 的扩展)。这意味着,看似错误的电子邮件地址,例如 bad-example@t,会被该包接受并解析,因为它根据规范是有效的。 t 可能是一个有效的本地域名,它不一定是公共域。 net/mail 不检查地址的域部分是否为公共域,也不检查其是否为现有的、可达的公共域。


5
谢谢,它在大多数情况下都能正常工作。但是,我有一些无效电子邮件的问题: "email@example.com (Joe Smith)", "email@example"。 - Riyaz Khan
2
明白了,就像RFC 5322文档中所写的那样,它正在检查域名。这里是截图,那么为什么对于"email@example"地址会失败呢? - Riyaz Khan
4
exampleexample.com 一样是一个有效的域名。它不一定要指定一个公共域名,也可以是本地网络的本地域名。至于 (Joe Smith) 部分:它是一个注释,可以出现在邮件的任何位置,请参见 Wikipedia: Email address。相信我,net/mail 包可以比你的自定义解决方案更好、更快、更可靠地解析电子邮件地址。 - icza
2
我认为这不是一个有效的电子邮件 "bad-example@t" - tile
2
根据RFC 5322,这是有效的。t可能是一个有效的本地域名。如上面所述,该域可以是本地域,不一定是公共域。 - icza
显示剩余7条评论

15

上述@icza的方法很好,但是,如果我们在登录/注册表单验证中使用它,许多人将输入部分或不正确的电子邮件地址,这将在生产中创建一堆无效记录。此外,谁知道因此我们可能会遇到麻烦。

因此,我采用了正则表达式解决方案来验证标准电子邮件:

下面是代码:

func isEmailValid(e string) bool {
    emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
    return emailRegex.MatchString(e)
}

测试用例:

fmt.Println(isEmailValid("test44@gmail.com"))         // true 
fmt.Println(isEmailValid("bad-email"))               // false
fmt.Println(isEmailValid("test44$@gmail.com"))      // false
fmt.Println(isEmailValid("test-email.com"))        // false
fmt.Println(isEmailValid("test+email@test.com"))  // true

3
即使是 Stack Overflow 上最差的答案也承认 $ 是可接受的。https://dev59.com/93I-5IYBdhLWcg3wBjjk#2049510 https://play.golang.org/p/iI2GeEmHRzE - user4466350
2
相信我,mail.ParseAddress() 做得比你更好。它可以接受本地域名,但告诉我,你确定 this-surely-doesnt-exist-just-showing-this-will-pass@nowhereland.cumgood@mydomain 更好作为“有效”的电子邮件地址吗? 无论如何,您都应该实现并要求进行电子邮件验证。否则,仅验证电子邮件地址是否语法正确是几乎毫无意义的。 - icza
@icza为什么会在mail.ParseAddress()中传递你的错误示例电子邮件this-surely-doesnt-exist-just-showing-this-will-pass@nowhereland.cum?生产环境谁关心本地域名,你是否可以在任何电子邮件提供者服务中注册类似于本地域名的电子邮件地址? - Riyaz Khan
@RiyazKhan 是的,你不能在 gmail.com 使用本地域名,因为 gmail.com 是公共域名。但是如果一家公司为其员工提供电子邮件服务,那么你可以使用(或者你只能使用)本地域名。 - icza
这个程序缺少非ASCII字符,而这些字符在域名部分和顶级域名是有效的。此外,还有长度大于4个字符的ASCII顶级域名(https://en.wikipedia.org/wiki/List_of_Internet_top-level_domains)。即使你有一个有效的电子邮件地址,你仍然需要检查它是否真正属于你的用户。因此,你必须要求进行电子邮件验证。 - ineiti

2

我发现正则表达式很难读懂,所以我更喜欢易读的代码。例如:

// Accepts at least the x@y.zz pattern.
func isEmailAddress(v string) bool {
    if v == "" {
        return false
    }
    if containsWhitespace(v) {
        return false
    }

    iAt := strings.IndexByte(v, '@')
    if iAt == -1 {
        return false
    }

    localPart := v[:iAt]
    if localPart == "" {
        return false
    }

    domain := v[iAt+1:]
    if domain == "" {
        return false
    }

    iDot := strings.IndexByte(domain, '.')
    if iDot == -1 || iDot == 0 || iDot == len(domain)-1 {
        return false
    }

    if strings.Index(domain, "..") != -1 {
        return false
    }

    iTLD := strings.LastIndexByte(domain, '.')
    return 2 <= len([]rune(domain[iTLD+1:]))
}

func containsWhitespace(v string) bool {
    for _, r := range v {
        if unicode.IsSpace(r) {
            return true
        }
    }
    return false
}

1
你可以创建一个基于标准库mail的函数IsEmail
func IsEmail(email string) bool {
  emailAddress, err := mail.ParseAddress(email)
  return err == nil && emailAddress.Address == email
}

测试用例:

func main() {
  fmt.Println("'hello@world.com'", IsEmail("hello@world.com")) // true
  fmt.Println("'asd <hello@world.com>'", IsEmail("asd <hello@world.com>")) // false
  fmt.Println("'a@b'", IsEmail("a@b")) // true
}

先生,您真是太棒了!您想出了一种方法,确保带有“name”的输入不会通过emailAddress.Address == email。您真是应该得到一块饼干的! - undefined
foo@bar被视为正确的电子邮件地址。 - undefined

1

这个外部包govalidator对我来说完美地完成了工作。

import (
  "fmt"
  "github.com/asaskevich/govalidator"
)

func main() {
    d := govalidator.IsEmail("helloworld@") 
    fmt.Println(d) // false
}

0

方法实现示例:

var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[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])?)*$")

这是在我创建结构体的同一个包中。

type EmailInputRegistration struct {
    Email           string
}

然后处理错误:

func (in EmailInputRegistration) Validate() error {
    if !emailRegexp.MatchString(in.Email) {
        return fmt.Errorf("%w: email invalid", ErrValidation)
    }
    //any other exception handling...

    return nil
}


理想情况下,应该重构此EmailInputRegistration以包括所有注册所需的数据,例如电子邮件、用户、密码等。

0
isValid 函数用于验证和规范化电子邮件地址。如果电子邮件包含非ASCII字符,它会将其转换为Punycode。然后,该函数会检查电子邮件是否符合有效电子邮件地址的规定,从而支持任何语言。
然后,可以使用idna.ToUnicode函数将其转换回原始语言。这种可逆性确保国际化域名和电子邮件地址可以以标准化的形式存储和处理,同时以可读和可理解的格式呈现给用户。
我认为这是在该网站上找到的用于电子邮件检查的最佳选择,毫无疑问。
package main

import (
    "fmt"
    "strings"

    "golang.org/x/net/idna"
)

func isValid(email string) interface{} {
    // Check the overall length of the email address
    if len(email) > 320 {
        return fmt.Errorf("email length exceeds 320 characters")
    }

    // Transform the local part to lowercase for case-insensitive unique storage
    parts := strings.Split(strings.ToLower(email), "@")
    if len(parts) != 2 {
        return fmt.Errorf("email must contain a single '@' character")
    }
    localPart, domainPart := parts[0], parts[1]

    // Check for empty local or domain parts
    if len(localPart) == 0 || len(domainPart) == 0 {
        return fmt.Errorf("local or domain part cannot be empty in the email")
    }

    // Check for consecutive special characters in the local part
    prevChar := rune(0)
    for _, char := range localPart {
        if strings.ContainsRune("!#$%&'*+-/=?^_`{|}~.", char) {
            if char == prevChar && char != '-' {
                return fmt.Errorf("consecutive special characters '%c' are not allowed in the local part", char)
            }
        }
        prevChar = char
    }

    // Check for spaces
    if strings.ContainsAny(email, " ") {
        return fmt.Errorf("spaces are not allowed in the email")
    }

    // Check the length of the local part and the domain part
    if len(localPart) > 64 || len(domainPart) > 255 {
        return fmt.Errorf("local part or domain part length exceeds the limit in the email")
    }

    // Convert international domain to ASCII (Punycode) only if needed
    asciiDomain, err := idna.ToASCII(domainPart)
    if err != nil {
        return fmt.Errorf("failed to convert domain to ASCII: %s", err)
    }

    // Convert international local part to ASCII (Punycode) only if needed
    asciiLocal, err := idna.ToASCII(localPart)
    if err != nil {
        return fmt.Errorf("failed to convert local part to ASCII: %s", err)
    }

    // Check that the domain labels do not start or end with special characters and TLD is alphabetic
    domainLabels := strings.Split(asciiDomain, ".")
    for i, label := range domainLabels {
        // Check first and last character of each label
        if !strings.ContainsAny("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", string(label[0])) ||
            !strings.ContainsAny("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", string(label[len(label)-1])) {
            return fmt.Errorf("domain labels must not start or end with special characters in the email")
        }

        // Check label length
        if len(label) > 63 {
            return fmt.Errorf("domain label length exceeds the limit in the email")
        }

        // Validate that the TLD is alphabetic
        if i == len(domainLabels)-1 && !strings.HasPrefix(label, "xn--") {
            decodedTLD, err := idna.ToUnicode(label)
            if err != nil {
                return fmt.Errorf("failed to decode TLD: %s", err)
            }
            isAlpha := true
            for _, ch := range decodedTLD {
                if (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') {
                    isAlpha = false
                    break
                }
            }
            if !isAlpha {
                return fmt.Errorf("TLD must be alphabetic in the email")
            }
        }
    }

    // Return the fully converted email
    return fmt.Sprintf("%s@%s", asciiLocal, asciiDomain)
}

func main() {
    // Standard ASCII email address (Valid)
    fmt.Println(isValid("test@example.com")) // Should return "test@example.com"

    // Unicode in local part (Valid)
    fmt.Println(isValid("tést@example.com")) // Should return Punycode-converted email

    // Punycode-encoded domain (IDNA) (Valid)
    fmt.Println(isValid("test@xn--fsqu00a.xn--0zwm56d")) // Should return "test@xn--fsqu00a.xn--0zwm56d"

    // Unicode in domain part (Valid)
    fmt.Println(isValid("test@例子.测试")) // Should return Punycode-converted domain

    // Unicode in both local and domain parts (Valid)
    fmt.Println(isValid("tést@例子.测试")) // Should return fully Punycode-converted email

    // Cyrillic script in local part (Valid)
    fmt.Println(isValid("тест@пример.ру")) // Should return fully Punycode-converted email

    // Arabic script in local part (Valid)
    fmt.Println(isValid("اختبار@مثال.اختبار")) // Should return fully Punycode-converted email

    // Hebrew script in local part (Valid)
    fmt.Println(isValid("בדיקה@דוגמה.בדיקה")) // Should return fully Punycode-converted email

    // Punycode-encoded local part and domain (Valid)
    fmt.Println(isValid("xn--e1aybc@xn--80akhbyknj4f.xn--p1ai")) // Should return "xn--e1aybc@xn--80akhbyknj4f.xn--p1ai"

    // Various other languages (All should be Valid)
    fmt.Println(isValid("測試@例子.測試"))   // Should return fully Punycode-converted email (Chinese)
    fmt.Println(isValid("テスト@例.テスト"))  // Should return fully Punycode-converted email (Japanese)
    fmt.Println(isValid("테스트@예시.테스트")) // Should return fully Punycode-converted email (Korean)

    // Email with consecutive special characters in local part (Invalid)
    fmt.Println(isValid("test..test@example.com")) // Should return an error

    // Email with special characters at the beginning or end of the local part (Invalid)
    fmt.Println(isValid(".test@example.com")) // Should return an error
    fmt.Println(isValid("test.@example.com")) // Should return an error

    // Email with space (Invalid)
    fmt.Println(isValid("test test@example.com")) // Should return an error

    // Email with no local part (Invalid)
    fmt.Println(isValid("@example.com")) // Should return an error

    // Email with no domain part (Invalid)
    fmt.Println(isValid("test@")) // Should return an error

    // Email with empty string (Invalid)
    fmt.Println(isValid("")) // Should return an error

    // Email exceeding 320 character limit (Invalid)
    fmt.Println(isValid(strings.Repeat("a", 320) + "@example.com")) // Should return an error
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接