parse.go raw
1 package smtp
2
3 import (
4 "fmt"
5 "strconv"
6 "strings"
7 "time"
8 )
9
10 // cutPrefixFold is a version of strings.CutPrefix which is case-insensitive.
11 func cutPrefixFold(s, prefix string) (string, bool) {
12 if len(s) < len(prefix) || !strings.EqualFold(s[:len(prefix)], prefix) {
13 return "", false
14 }
15 return s[len(prefix):], true
16 }
17
18 func parseCmd(line string) (cmd string, arg string, err error) {
19 line = strings.TrimRight(line, "\r\n")
20
21 l := len(line)
22 switch {
23 case strings.HasPrefix(strings.ToUpper(line), "STARTTLS"):
24 return "STARTTLS", "", nil
25 case l == 0:
26 return "", "", nil
27 case l < 4:
28 return "", "", fmt.Errorf("command too short: %q", line)
29 case l == 4:
30 return strings.ToUpper(line), "", nil
31 case l == 5:
32 // Too long to be only command, too short to have args
33 return "", "", fmt.Errorf("mangled command: %q", line)
34 }
35
36 // If we made it here, command is long enough to have args
37 if line[4] != ' ' {
38 // There wasn't a space after the command?
39 return "", "", fmt.Errorf("mangled command: %q", line)
40 }
41
42 return strings.ToUpper(line[0:4]), strings.TrimSpace(line[5:]), nil
43 }
44
45 // Takes the arguments proceeding a command and files them
46 // into a map[string]string after uppercasing each key. Sample arg
47 // string:
48 //
49 // " BODY=8BITMIME SIZE=1024 SMTPUTF8"
50 //
51 // The leading space is mandatory.
52 func parseArgs(s string) (map[string]string, error) {
53 argMap := map[string]string{}
54 for _, arg := range strings.Fields(s) {
55 m := strings.Split(arg, "=")
56 switch len(m) {
57 case 2:
58 argMap[strings.ToUpper(m[0])] = m[1]
59 case 1:
60 argMap[strings.ToUpper(m[0])] = ""
61 default:
62 return nil, fmt.Errorf("failed to parse arg string: %q", arg)
63 }
64 }
65 return argMap, nil
66 }
67
68 func parseHelloArgument(arg string) (string, error) {
69 domain := arg
70 if idx := strings.IndexRune(arg, ' '); idx >= 0 {
71 domain = arg[:idx]
72 }
73 if domain == "" {
74 return "", fmt.Errorf("invalid domain")
75 }
76 return domain, nil
77 }
78
79 // Parses the BY argument defined in RFC2852 section 4.
80 // Returns pointer to options or nil if invalid.
81 func parseDeliverByArgument(arg string) *DeliverByOptions {
82 secondsStr, modeStr, ok := strings.Cut(arg, ";")
83 if !ok {
84 return nil
85 }
86 modeStr, traceValue := strings.CutSuffix(modeStr, "T")
87 if modeStr != string(DeliverByNotify) && modeStr != string(DeliverByReturn) {
88 return nil
89 }
90 modeValue := DeliverByMode(modeStr)
91 secondsValue, err := strconv.Atoi(secondsStr)
92 if err != nil || (modeValue == DeliverByReturn && secondsValue < 1) {
93 return nil
94 }
95 return &DeliverByOptions{
96 Time: time.Duration(secondsValue) * time.Second,
97 Mode: modeValue,
98 Trace: traceValue,
99 }
100 }
101
102 // parser parses command arguments defined in RFC 5321 section 4.1.2.
103 type parser struct {
104 s string
105 }
106
107 func (p *parser) peekByte() (byte, bool) {
108 if len(p.s) == 0 {
109 return 0, false
110 }
111 return p.s[0], true
112 }
113
114 func (p *parser) readByte() (byte, bool) {
115 ch, ok := p.peekByte()
116 if ok {
117 p.s = p.s[1:]
118 }
119 return ch, ok
120 }
121
122 func (p *parser) acceptByte(ch byte) bool {
123 got, ok := p.peekByte()
124 if !ok || got != ch {
125 return false
126 }
127 p.readByte()
128 return true
129 }
130
131 func (p *parser) expectByte(ch byte) error {
132 if !p.acceptByte(ch) {
133 if len(p.s) == 0 {
134 return fmt.Errorf("expected '%v', got EOF", string(ch))
135 } else {
136 return fmt.Errorf("expected '%v', got '%v'", string(ch), string(p.s[0]))
137 }
138 }
139 return nil
140 }
141
142 func (p *parser) parseReversePath() (string, error) {
143 if strings.HasPrefix(p.s, "<>") {
144 p.s = strings.TrimPrefix(p.s, "<>")
145 return "", nil
146 }
147 return p.parsePath()
148 }
149
150 func (p *parser) parsePath() (string, error) {
151 hasBracket := p.acceptByte('<')
152 if p.acceptByte('@') {
153 i := strings.IndexByte(p.s, ':')
154 if i < 0 {
155 return "", fmt.Errorf("malformed a-d-l")
156 }
157 p.s = p.s[i+1:]
158 }
159 mbox, err := p.parseMailbox()
160 if err != nil {
161 return "", fmt.Errorf("in mailbox: %v", err)
162 }
163 if hasBracket {
164 if err := p.expectByte('>'); err != nil {
165 return "", err
166 }
167 }
168 return mbox, nil
169 }
170
171 func (p *parser) parseMailbox() (string, error) {
172 localPart, err := p.parseLocalPart()
173 if err != nil {
174 return "", fmt.Errorf("in local-part: %v", err)
175 } else if localPart == "" {
176 return "", fmt.Errorf("local-part is empty")
177 }
178
179 if err := p.expectByte('@'); err != nil {
180 return "", err
181 }
182
183 var sb strings.Builder
184 sb.WriteString(localPart)
185 sb.WriteByte('@')
186
187 for {
188 ch, ok := p.peekByte()
189 if !ok {
190 break
191 }
192 if ch == ' ' || ch == '\t' || ch == '>' {
193 break
194 }
195 p.readByte()
196 sb.WriteByte(ch)
197 }
198
199 if strings.HasSuffix(sb.String(), "@") {
200 return "", fmt.Errorf("domain is empty")
201 }
202
203 return sb.String(), nil
204 }
205
206 func (p *parser) parseLocalPart() (string, error) {
207 var sb strings.Builder
208
209 if p.acceptByte('"') { // quoted-string
210 for {
211 ch, ok := p.readByte()
212 switch ch {
213 case '\\':
214 ch, ok = p.readByte()
215 case '"':
216 return sb.String(), nil
217 }
218 if !ok {
219 return "", fmt.Errorf("malformed quoted-string")
220 }
221 sb.WriteByte(ch)
222 }
223 } else { // dot-string
224 for {
225 ch, ok := p.peekByte()
226 if !ok {
227 return sb.String(), nil
228 }
229 switch ch {
230 case '@':
231 return sb.String(), nil
232 case '(', ')', '<', '>', '[', ']', ':', ';', '\\', ',', '"', ' ', '\t':
233 return "", fmt.Errorf("malformed dot-string")
234 }
235 p.readByte()
236 sb.WriteByte(ch)
237 }
238 }
239 }
240