-
Notifications
You must be signed in to change notification settings - Fork 3
/
disposable.go
192 lines (153 loc) · 4.97 KB
/
disposable.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// Copyright 2020-22 PJ Engineering and Business Solutions Pty. Ltd. All rights reserved.
package disposable
import (
"errors"
"golang.org/x/net/idna"
"strings"
"unicode"
)
// ErrInvalidEmail is returned if the email address is invalid.
var ErrInvalidEmail = errors.New("invalid email")
// ParsedEmail returns a parsed email address.
//
// An email address is made up of 3 components: <local-part>@<domain>.
// The local-part is case-sensitive according to the specs, but most
// (if not all) reputable email services will treat it as case-insensitive.
// The domain is case-insensitive.
type ParsedEmail struct {
// Email represents the input email (after white-space has been trimmed).
Email string
// Preferred represents the local-part in the way the user seems to prefer it.
// For example if the local-part is case-insensitive, the user may prefer their
// email address all upper-case even if it does not matter.
Preferred string
// Normalized represents the local-part normalized such that it can be
// compared for uniqueness.
//
// For gmail, since john.smith@gmail.com, johnsmith@gmail.com, and JohnSmith@gmail.com
// are all equivalent, the normalized local-part is 'johnsmith'.
Normalized string
// Extra represents extra information that is domain specific.
//
// Example: gmail ignores all characters after the first '+' in the local-part.
//
// adam+junk@gmail.com => adam@gmail.com (Extra: junk)
Extra string
// Disposable is true if the email address is detected to be from
// a disposable email service.
//
// See: https://github.com/martenson/disposable-email-domains
Disposable bool
// Domain represents the component after the '@' character.
// It is lower-cased since it's case-insensitive.
Domain string
// LocalPart represents the component before the '@' character.
LocalPart string
}
// ParseEmail parses a given email address. Set caseSensitive to true if you want the local-part
// to be considered case-sensitive. The default value is false. Basic email validation is performed but
// it is not comprehensively checked.
//
// See https://github.com/badoux/checkmail for a more robust validation solution.
//
// See also https://davidcel.is/posts/stop-validating-email-addresses-with-regex.
//
func ParseEmail(email string, caseSensitive ...bool) (ParsedEmail, error) {
// Perform basic validation
email = strings.TrimSpace(email)
if email == "" {
return ParsedEmail{}, ErrInvalidEmail
}
if strings.Contains(email, " ") {
return ParsedEmail{Email: email}, ErrInvalidEmail
}
var cs bool
if len(caseSensitive) > 0 {
cs = caseSensitive[0]
}
splits := strings.Split(email, "@")
if len(splits) != 2 {
return ParsedEmail{Email: email}, ErrInvalidEmail
}
domain := toLower(splits[1])
localPart := splits[0]
domain, err := idna.ToASCII(domain)
if err != nil {
return ParsedEmail{Email: email}, ErrInvalidEmail
}
if !ValidateDomain(domain) {
return ParsedEmail{Email: email}, ErrInvalidEmail
}
p := ParsedEmail{
Email: email,
Domain: domain,
LocalPart: localPart,
}
// Normalize local part
p.Normalized, p.Preferred, p.Extra = normalize(localPart, domain, cs)
// Check if domain is disposable
_, p.Disposable = DisposableList[domain]
return p, nil
}
func normalize(localPart, domain string, caseSensitive bool) (ret string, pref string, sufx string) {
pref = localPart
switch domain {
case "gmail.com":
// remove suffix from localPart
splits := strings.SplitN(localPart, "+", 2)
if len(splits) == 2 {
localPart, sufx = splits[0], splits[1]
pref = localPart
}
// remove the periods
localPart = strings.ReplaceAll(localPart, ".", "")
}
// lower-case the local part
if caseSensitive {
ret = localPart
return
}
ret = toLower(localPart)
return
}
func toLower(s string) (ret string) {
for _, r := range s {
ret += string(unicode.ToLower(r))
}
return
}
// ValidateDomain returns true if the domain component of an email address is valid.
// domain must be already lower-case and white-space trimmed. This function only performs a basic check and is not
// authoritative. For domains containing unicode characters, you must perform punycode conversion beforehand.
// See: https://godoc.org/golang.org/x/net/idna#ToASCII
func ValidateDomain(domain string) bool {
if domain == "" {
return false
}
// Check if first or last character is . or dash
if strings.HasPrefix(domain, ".") || strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, ".") || strings.HasSuffix(domain, "-") {
return false
}
// Check if only a-z, 0-9, -, . and _ are found.
for _, r := range domain {
switch r {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
case '-', '.', '_':
case ' ':
return false
default:
if unicode.IsSpace(r) {
return false
} else if 'a' <= r && r <= 'z' {
} else {
return false
}
}
}
// Check number of characters after final dot is at least 2
splits := strings.Split(domain, ".")
if len(splits) > 1 && len(splits[len(splits)-1]) < 2 {
return false
}
return true
}