/
address.go
154 lines (138 loc) · 4.27 KB
/
address.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
package mailutil
import (
"errors"
"fmt"
"net/mail"
"net/url"
"sort"
"strings"
)
// Decodes the Name (not used at the moment).
// mail.ParseAddress and mail.ParseAddressList yield errors on encoded input, so we should this
var RobustAddressParser = mail.AddressParser{
WordDecoder: TryMimeDecoder,
}
var ErrInvalidAddress = errors.New("invalid email address")
type Addr struct {
Display string // RFC 5322 display-name
Local string // RFC 5322 local-part, only a subset of ASCII is allowed
Domain string // RFC 5322 domain
}
// compares Local and Domain case-insensitively
func (a *Addr) Equals(other *Addr) bool {
return strings.EqualFold(a.Local, other.Local) && strings.EqualFold(a.Domain, other.Domain)
}
// RFC 5322
// addr-spec = local-part "@" domain
//
// Because the local-part might be quoted, we let golang do the work
func (a Addr) RFC5322AddrSpec() string {
s := (&mail.Address{Address: a.Local + "@" + a.Domain}).String()
return s[1 : len(s)-1] // strip first and last char, which is '<' and '>'
}
// RFC 5322
// name-addr = [display-name] angle-addr
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
//
// mail.Address.String(): "If the address's name contains non-ASCII characters the name will be rendered according to RFC 2047."
func (a *Addr) RFC5322NameAddr() string {
return (&mail.Address{
Name: a.Display,
Address: a.Local + "@" + a.Domain,
}).String()
}
// RFC 6068
// The 'mailto' URI Scheme
func (a *Addr) RFC6068URI(query string) string {
u := url.URL{
Scheme: "mailto",
Opaque: a.RFC5322AddrSpec(),
RawQuery: query,
}
return "<" + u.String() + ">" // "URIs are enclosed in '<' and '>'"
}
// Returns a.Display if it exists, else a.Local.
//
// rspamd has the rule "SPOOF_DISPLAY_NAME" which yields a huge penalty if the "From" field looks like "foo@example.net via List<list@example.com>" [1].
// To be on the safe side, we crop the result at the first "@", if any.
//
// [1] https://github.com/rspamd/rspamd/blob/master/rules/misc.lua#L517
func (a *Addr) DisplayOrLocal() string {
var result string
if a.Display != "" {
result = a.Display
} else {
result = a.Local
}
return strings.SplitN(result, "@", 2)[0]
}
func (a *Addr) String() string {
return a.RFC5322AddrSpec()
}
func NewAddr(address *mail.Address) (*Addr, error) {
atPos := strings.LastIndex(address.Address, "@") // local-part may contain "@", so we split at the last one
if atPos == -1 {
return nil, ErrInvalidAddress
}
a := &Addr{}
a.Display = address.Name
a.Local = strings.ToLower(address.Address[0:atPos])
a.Domain = strings.ToLower(address.Address[atPos+1:])
return a, nil
}
// parses an address like "Alice <alice@example.org>", "<alice@example.org>" or "alice@example.org"
// returns the canonicalized address
//
// It is recommended to canonicalize or parse all input data (from form post data, url parameters, SMTP commands, header fields).
func ParseAddress(rfc5322Address string) (*Addr, error) {
parsed, err := RobustAddressParser.Parse(rfc5322Address)
if err != nil {
return nil, ErrInvalidAddress
}
return NewAddr(parsed)
}
// ParseAddresses expects one RFC 5322 address-list per line. It does lax parsing and is intended for user input.
func ParseAddresses(rawAddresses string, limit int) (addrs []*Addr, errs []error) {
for _, line := range strings.Split(rawAddresses, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parsedAddresses, err := RobustAddressParser.ParseList(line)
if err != nil {
errs = append(errs, fmt.Errorf(`error parsing line "%s": %s`, line, err))
continue
}
for _, p := range parsedAddresses {
address, err := NewAddr(p)
if err != nil {
errs = append(errs, fmt.Errorf(`error parsing address "%s": %s`, p, err))
continue
}
addrs = append(addrs, address)
}
if len(addrs) > limit {
errs = append(errs, fmt.Errorf("please enter not more than %d addresses", limit))
return
}
}
return
}
func RFC5322AddrSpecs(addrs []*Addr) []string {
var specs = make([]string, len(addrs))
for i := range addrs {
specs[i] = addrs[i].RFC5322AddrSpec()
}
sort.Strings(specs)
// make sorted slice unique
var nextIndex = 0
for i, spec := range specs {
if i > 0 && specs[i-1] == specs[i] {
continue
}
specs[nextIndex] = spec
nextIndex++
}
specs = specs[:nextIndex]
return specs
}