| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- package service
- import (
- "encoding/json"
- "fmt"
- "regexp"
- "strings"
- "golang.org/x/net/publicsuffix"
- )
- var registrationEmailDomainPattern = regexp.MustCompile(
- `^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$`,
- )
- // RegistrationEmailSuffix extracts normalized suffix in "@domain" form.
- func RegistrationEmailSuffix(email string) string {
- _, domain, ok := splitEmailForPolicy(email)
- if !ok {
- return ""
- }
- return "@" + domain
- }
- // RegistrationEmailDomain 返回邮箱对应的可注册主域名,用于域名注册额度归一化。
- // 例如 abc.com 和 abcd.abc.com 都返回 abc.com;无法从公共后缀表归一化时保留原域名。
- func RegistrationEmailDomain(email string) string {
- _, domain, ok := splitEmailForPolicy(email)
- if !ok {
- return ""
- }
- return NormalizeRegistrationEmailDomain(domain)
- }
- // NormalizeRegistrationEmailDomain 将邮箱域名归一为可注册主域名。
- func NormalizeRegistrationEmailDomain(domain string) string {
- domain = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(domain, "@")))
- domain = strings.TrimRight(domain, ".")
- if domain == "" {
- return ""
- }
- registrable, err := publicsuffix.EffectiveTLDPlusOne(domain)
- if err != nil {
- return domain
- }
- return registrable
- }
- // IsRegistrationEmailSuffixAllowed checks whether an email is allowed by suffix whitelist.
- // Empty whitelist means allow all.
- func IsRegistrationEmailSuffixAllowed(email string, whitelist []string) bool {
- if len(whitelist) == 0 {
- return true
- }
- _, domain, ok := splitEmailForPolicy(email)
- if !ok {
- return false
- }
- suffix := "@" + domain
- for _, allowed := range whitelist {
- allowed = strings.ToLower(strings.TrimSpace(allowed))
- if strings.HasPrefix(allowed, "@") && suffix == allowed {
- return true
- }
- if strings.HasPrefix(allowed, "*.") && registrationEmailDomainMatchesWildcard(domain, allowed) {
- return true
- }
- }
- return false
- }
- // IsRegistrationEmailSuffixLimited 判断非空白名单是否对该邮箱域名启用单账户额度。
- func IsRegistrationEmailSuffixLimited(email string, whitelist []string) bool {
- return len(whitelist) > 0 && !IsRegistrationEmailSuffixAllowed(email, whitelist)
- }
- // NormalizeRegistrationEmailSuffixWhitelist normalizes and validates suffix whitelist items.
- func NormalizeRegistrationEmailSuffixWhitelist(raw []string) ([]string, error) {
- return normalizeRegistrationEmailSuffixWhitelist(raw, true)
- }
- // ParseRegistrationEmailSuffixWhitelist parses persisted JSON into normalized suffixes.
- // Invalid entries are ignored to keep old misconfigurations from breaking runtime reads.
- func ParseRegistrationEmailSuffixWhitelist(raw string) []string {
- raw = strings.TrimSpace(raw)
- if raw == "" {
- return []string{}
- }
- var items []string
- if err := json.Unmarshal([]byte(raw), &items); err != nil {
- return []string{}
- }
- normalized, _ := normalizeRegistrationEmailSuffixWhitelist(items, false)
- if len(normalized) == 0 {
- return []string{}
- }
- return normalized
- }
- func normalizeRegistrationEmailSuffixWhitelist(raw []string, strict bool) ([]string, error) {
- if len(raw) == 0 {
- return nil, nil
- }
- seen := make(map[string]struct{}, len(raw))
- out := make([]string, 0, len(raw))
- for _, item := range raw {
- normalized, err := normalizeRegistrationEmailSuffix(item)
- if err != nil {
- if strict {
- return nil, err
- }
- continue
- }
- if normalized == "" {
- continue
- }
- if _, ok := seen[normalized]; ok {
- continue
- }
- seen[normalized] = struct{}{}
- out = append(out, normalized)
- }
- if len(out) == 0 {
- return nil, nil
- }
- return out, nil
- }
- func normalizeRegistrationEmailSuffix(raw string) (string, error) {
- value := strings.ToLower(strings.TrimSpace(raw))
- if value == "" {
- return "", nil
- }
- if strings.HasPrefix(value, "*.") {
- domain := strings.TrimPrefix(value, "*.")
- if !isValidRegistrationEmailDomain(domain) {
- return "", fmt.Errorf("invalid email suffix: %q", raw)
- }
- return "*." + domain, nil
- }
- domain := value
- if strings.Contains(value, "@") {
- if !strings.HasPrefix(value, "@") || strings.Count(value, "@") != 1 {
- return "", fmt.Errorf("invalid email suffix: %q", raw)
- }
- domain = strings.TrimPrefix(value, "@")
- }
- if !isValidRegistrationEmailDomain(domain) {
- return "", fmt.Errorf("invalid email suffix: %q", raw)
- }
- return "@" + domain, nil
- }
- func isValidRegistrationEmailDomain(domain string) bool {
- return domain != "" &&
- !strings.Contains(domain, "@") &&
- registrationEmailDomainPattern.MatchString(domain)
- }
- func registrationEmailDomainMatchesWildcard(domain string, allowed string) bool {
- base := strings.TrimPrefix(allowed, "*.")
- if !isValidRegistrationEmailDomain(base) {
- return false
- }
- return domain == base || strings.HasSuffix(domain, "."+base)
- }
- func splitEmailForPolicy(raw string) (local string, domain string, ok bool) {
- email := strings.ToLower(strings.TrimSpace(raw))
- local, domain, found := strings.Cut(email, "@")
- if !found || local == "" || domain == "" || strings.Contains(domain, "@") {
- return "", "", false
- }
- domain = strings.TrimRight(domain, ".")
- if domain == "" {
- return "", "", false
- }
- return local, domain, true
- }
|