forked from goware/emailx
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathemailx.go
173 lines (147 loc) · 4.72 KB
/
emailx.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
package emailx
import (
"errors"
"fmt"
"net"
"regexp"
"strings"
)
var (
//ErrInvalidFormat returns when email's format is invalid
ErrInvalidFormat = errors.New("invalid format")
//ErrUnresolvableHost returns when validator couldn't resolve email's host
ErrUnresolvableHost = errors.New("unresolvable host")
//ErrBlockedIPRange returns when the hostname resolved, but the IP is in a blocked range
ErrBlockedIPRange = errors.New("blocked ip range")
//BlockedIPs contains IP ranges to be considered invalid.
//Initialized with the known private IP ranges
BlockedIPs []*net.IPNet
userRegexp = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$")
hostRegexp = regexp.MustCompile("^[^\\s]+\\.[^\\s]+$")
// As per RFC 5332 secion 3.2.3: https://tools.ietf.org/html/rfc5322#section-3.2.3
// Dots are not allowed in the beginning, end or in occurances of more than 1 in the email address
userDotRegexp = regexp.MustCompile("(^[.]{1})|([.]{1}$)|([.]{2,})")
)
var privateIPBlocks []*net.IPNet
// init prepares the IP default address ranges for IP blocking.
// Adapted from https://stackoverflow.com/questions/41240761/go-check-if-ip-address-is-in-private-network-space
func init() {
for _, cidr := range []string{
"127.0.0.0/8", // IPv4 loopback
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"169.254.0.0/16", // RFC3927 link-local
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fc00::/7", // IPv6 unique local addr
} {
_, block, err := net.ParseCIDR(cidr)
if err != nil {
panic(fmt.Errorf("parse error on %q: %v", cidr, err))
}
privateIPBlocks = append(privateIPBlocks, block)
}
BlockedIPs = append(BlockedIPs, privateIPBlocks...)
}
// Validate checks format of a given email and resolves its host name.
func Validate(email string) error {
if len(email) < 6 || len(email) > 254 {
return ErrInvalidFormat
}
at := strings.LastIndex(email, "@")
if at <= 0 || at > len(email)-3 {
return ErrInvalidFormat
}
user := email[:at]
host := email[at+1:]
if len(user) > 64 {
return ErrInvalidFormat
}
if userDotRegexp.MatchString(user) || !userRegexp.MatchString(user) || !hostRegexp.MatchString(host) {
return ErrInvalidFormat
}
// Look for MX records
mxes, err := net.LookupMX(host)
if err != nil || len(mxes) == 0 {
// No MX records available, or lookup failed.
// Fall back to A/AAAA records
resolvedIPs, err := net.LookupIP(host)
if err != nil || len(resolvedIPs) == 0 {
// Only fail if both MX and A records are missing - any of the
// two is enough for an email to be deliverable
return ErrUnresolvableHost
}
if IsAnyIPBlocked(resolvedIPs) {
return ErrBlockedIPRange
}
// Record resolved successfully, and is not in a blocked IP range
return nil
}
// MX records found, validate them
for _, mx := range mxes {
// Check that at least one MX entry is valid and not in a blocked IP range.
// net.LookupMX returns entries sorted by their preference, so we technically only validate the preferred server.
if resolvedIP, err := net.LookupIP(mx.Host); err == nil && len(resolvedIP) > 0 {
// MX hostname resolved successfully, ...
if IsAnyIPBlocked(resolvedIP) {
return ErrBlockedIPRange
}
// ... and is not in a blocked IP range
return nil
}
}
return ErrUnresolvableHost
}
// IsAnyIPBlocked returns true if any of the IP addresses in the given slice are loopback, unicast, multicast or in a blocked range.
// See also IsBlockedIP() and BlockedIPs.
func IsAnyIPBlocked(ips []net.IP) bool {
for _, ip := range ips {
if IsBlockedIP(ip) {
return true
}
}
return false
}
// IsBlockedIP returns true the given IP address is loopback, unicast, multicast or in a blocked range.
// See also IsAnyIPBlocked() and BlockedIPs.
func IsBlockedIP(ip net.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
for _, block := range BlockedIPs {
if block.Contains(ip) {
return true
}
}
return false
}
// ValidateFast checks format of a given email.
func ValidateFast(email string) error {
if len(email) < 6 || len(email) > 254 {
return ErrInvalidFormat
}
at := strings.LastIndex(email, "@")
if at <= 0 || at > len(email)-3 {
return ErrInvalidFormat
}
user := email[:at]
host := email[at+1:]
if len(user) > 64 {
return ErrInvalidFormat
}
if userDotRegexp.MatchString(user) || !userRegexp.MatchString(user) || !hostRegexp.MatchString(host) {
return ErrInvalidFormat
}
return nil
}
// Normalize normalizes email address.
func Normalize(email string) string {
// Trim whitespaces.
email = strings.TrimSpace(email)
// Trim extra dot in hostname.
email = strings.TrimRight(email, ".")
// Lowercase.
email = strings.ToLower(email)
return email
}