1package smtpserver
2
3import (
4 "errors"
5 "fmt"
6 "net"
7 "strconv"
8 "strings"
9 "time"
10
11 "golang.org/x/text/unicode/norm"
12
13 "github.com/mjl-/mox/dns"
14 "github.com/mjl-/mox/mox-"
15 "github.com/mjl-/mox/smtp"
16)
17
18// Parser holds the original string and string with ascii a-z upper-cased for easy
19// case-insensitive parsing.
20type parser struct {
21 orig string
22 upper string
23 o int // Offset into orig/upper.
24 smtputf8 bool // Whether SMTPUTF8 extension is enabled, making IDNA domains and utf8 localparts valid.
25 conn *conn
26 utf8LocalpartCode int // If non-zero, error for utf-8 localpart when smtputf8 not enabled.
27}
28
29// toUpper upper cases bytes that are a-z. strings.ToUpper does too much. and
30// would replace invalid bytes with unicode replacement characters, which would
31// break our requirement that offsets into the original and upper case strings
32// point to the same character.
33func toUpper(s string) string {
34 r := []byte(s)
35 for i, c := range r {
36 if c >= 'a' && c <= 'z' {
37 r[i] = c - 0x20
38 }
39 }
40 return string(r)
41}
42
43func newParser(s string, smtputf8 bool, conn *conn) *parser {
44 return &parser{orig: s, upper: toUpper(s), smtputf8: smtputf8, conn: conn}
45}
46
47func (p *parser) xerrorf(format string, args ...any) {
48 // For submission, send the remaining unparsed line. Otherwise, only log it.
49 var err error
50 errmsg := "bad syntax: " + fmt.Sprintf(format, args...)
51 remaining := fmt.Sprintf(" (remaining %q)", p.orig[p.o:])
52 if p.conn.account != nil {
53 errmsg += remaining
54 err = errors.New(errmsg)
55 } else {
56 err = errors.New(errmsg + remaining)
57 }
58
59 // ../rfc/5321:2377
60 panic(smtpError{smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, errmsg, err, false, true})
61}
62
63func (p *parser) xutf8localparterrorf() {
64 code := p.utf8LocalpartCode
65 if code == 0 {
66 code = smtp.C550MailboxUnavail
67 }
68 // ../rfc/6531:466
69 xsmtpUserErrorf(code, smtp.SeMsg6NonASCIIAddrNotPermitted7, "non-ascii address not permitted without smtputf8")
70}
71
72func (p *parser) empty() bool {
73 return p.o == len(p.orig)
74}
75
76// note: use xend() for check for end of line with remaining white space, to be used by commands.
77func (p *parser) xempty() {
78 if p.o != len(p.orig) {
79 p.xerrorf("expected end of line")
80 }
81}
82
83// check we are at the end of a command.
84func (p *parser) xend() {
85 // For submission, we are strict.
86 if p.conn.submission {
87 p.xempty()
88 }
89 // Otherwise we allow trailing white space. ../rfc/5321:1758
90 rem := p.remainder()
91 for _, c := range rem {
92 if c != ' ' && c != '\t' {
93 p.xerrorf("trailing data, not white space: %q", rem)
94 }
95 }
96}
97
98func (p *parser) hasPrefix(s string) bool {
99 return strings.HasPrefix(p.upper[p.o:], s)
100}
101
102func (p *parser) take(s string) bool {
103 if p.hasPrefix(s) {
104 p.o += len(s)
105 return true
106 }
107 return false
108}
109
110func (p *parser) xtake(s string) {
111 if !p.take(s) {
112 p.xerrorf("expected %q", s)
113 }
114}
115
116func (p *parser) space() bool {
117 return p.take(" ")
118}
119
120func (p *parser) xspace() {
121 p.xtake(" ")
122}
123
124func (p *parser) xtaken(n int) string {
125 r := p.orig[p.o : p.o+n]
126 p.o += n
127 return r
128}
129
130func (p *parser) remainder() string {
131 r := p.orig[p.o:]
132 p.o = len(p.orig)
133 return r
134}
135
136func (p *parser) peekchar() rune {
137 for _, c := range p.upper[p.o:] {
138 return c
139 }
140 return -1
141}
142
143func (p *parser) xtakefn1(what string, fn func(c rune, i int) bool) string {
144 if p.empty() {
145 p.xerrorf("need at least one char for %s", what)
146 }
147 for i, c := range p.upper[p.o:] {
148 if !fn(c, i) {
149 if i == 0 {
150 p.xerrorf("expected at least one char for %s", what)
151 }
152 return p.xtaken(i)
153 }
154 }
155 return p.remainder()
156}
157
158func (p *parser) xtakefn1case(what string, fn func(c rune, i int) bool) string {
159 if p.empty() {
160 p.xerrorf("need at least one char for %s", what)
161 }
162 for i, c := range p.orig[p.o:] {
163 if !fn(c, i) {
164 if i == 0 {
165 p.xerrorf("expected at least one char for %s", what)
166 }
167 return p.xtaken(i)
168 }
169 }
170 return p.remainder()
171}
172
173func (p *parser) xtakefn(fn func(c rune, i int) bool) string {
174 for i, c := range p.upper[p.o:] {
175 if !fn(c, i) {
176 return p.xtaken(i)
177 }
178 }
179 return p.remainder()
180}
181
182// xrawReversePath returns the raw string between the <>'s. We cannot parse it
183// immediately, because if this is an IDNA (internationalization) address, we would
184// only see the SMTPUTF8 indicator after having parsed the reverse path here. So we
185// parse the raw data here, and validate it after having seen all parameters.
186// ../rfc/5321:2260
187func (p *parser) xrawReversePath() string {
188 p.xtake("<")
189 s := p.xtakefn(func(c rune, i int) bool {
190 return c != '>'
191 })
192 p.xtake(">")
193 return s
194}
195
196// xbareReversePath parses a reverse-path without <>, as returned by
197// xrawReversePath. It takes smtputf8 into account.
198// ../rfc/5321:2260
199func (p *parser) xbareReversePath() smtp.Path {
200 if p.empty() {
201 return smtp.Path{}
202 }
203 // ../rfc/6531:468
204 p.utf8LocalpartCode = smtp.C550MailboxUnavail
205 defer func() {
206 p.utf8LocalpartCode = 0
207 }()
208 return p.xbarePath()
209}
210
211func (p *parser) xforwardPath() smtp.Path {
212 // ../rfc/6531:466
213 p.utf8LocalpartCode = smtp.C553BadMailbox
214 defer func() {
215 p.utf8LocalpartCode = 0
216 }()
217 return p.xpath()
218}
219
220// ../rfc/5321:2264
221func (p *parser) xpath() smtp.Path {
222 o := p.o
223 p.xtake("<")
224 r := p.xbarePath()
225 p.xtake(">")
226 if p.o-o > 256 {
227 // ../rfc/5321:3495
228 p.xerrorf("path longer than 256 octets")
229 }
230 return r
231}
232
233func (p *parser) xbarePath() smtp.Path {
234 // We parse but ignore any source routing.
235 // ../rfc/5321:1081 ../rfc/5321:1430 ../rfc/5321:1925
236 if p.take("@") {
237 p.xdomain()
238 for p.take(",") {
239 p.xtake("@")
240 p.xdomain()
241 }
242 p.xtake(":")
243 }
244 return p.xmailbox()
245}
246
247// ../rfc/5321:2291
248func (p *parser) xdomain() dns.Domain {
249 var s strings.Builder
250 s.WriteString(p.xsubdomain())
251 for p.take(".") {
252 s.WriteString("." + p.xsubdomain())
253 }
254 d, err := dns.ParseDomain(s.String())
255 if err != nil {
256 p.xerrorf("parsing domain name %q: %s", s.String(), err)
257 }
258 if len(s.String()) > 255 {
259 // ../rfc/5321:3491
260 p.xerrorf("domain longer than 255 octets")
261 }
262 return d
263}
264
265// ../rfc/5321:2303 ../rfc/6531:411
266func (p *parser) xsubdomain() string {
267 return p.xtakefn1("subdomain", func(c rune, i int) bool {
268 return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || i > 0 && c == '-' || c > 0x7f && p.smtputf8
269 })
270}
271
272// ../rfc/5321:2314
273func (p *parser) xmailbox() smtp.Path {
274 localpart := p.xlocalpart()
275 p.xtake("@")
276 return smtp.Path{Localpart: localpart, IPDomain: p.xipdomain(false)}
277}
278
279// ../rfc/5321:2307
280func (p *parser) xldhstr() string {
281 s := p.xtakefn1("ldh-str", func(c rune, i int) bool {
282 return c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-'
283 })
284 if s == "-" {
285 p.xerrorf("empty ldh-str")
286 } else if strings.HasSuffix(s, "-") {
287 p.o--
288 s = s[:len(s)-1]
289 }
290 return s
291}
292
293// parse address-literal or domain.
294func (p *parser) xipdomain(isehlo bool) dns.IPDomain {
295 // ../rfc/5321:2309
296 // ../rfc/5321:2397
297 if p.take("[") {
298 c := p.peekchar()
299 var ipv6 bool
300 if !(c >= '0' && c <= '9') {
301 addrlit := p.xldhstr()
302 p.xtake(":")
303 if !strings.EqualFold(addrlit, "IPv6") {
304 p.xerrorf("unrecognized address literal %q", addrlit)
305 }
306 ipv6 = true
307 }
308 ipaddr := p.xtakefn1("address literal", func(c rune, i int) bool {
309 return c != ']'
310 })
311 p.take("]")
312 ip := net.ParseIP(ipaddr)
313 if ip == nil {
314 p.xerrorf("invalid ip in address: %q", ipaddr)
315 }
316 isv4 := ip.To4() != nil
317 isAllowedSloppyIPv6Submission := func() bool {
318 // Mail user agents that submit are relatively likely to use IPs in EHLO and forget
319 // that an IPv6 address needs to be tagged as such. We can forgive them. For
320 // SMTP servers we are strict.
321 return isehlo && p.conn.submission && !mox.Pedantic && ip.To16() != nil
322 }
323 if ipv6 && isv4 {
324 p.xerrorf("ip address is not ipv6")
325 } else if !ipv6 && !isv4 && !isAllowedSloppyIPv6Submission() {
326 if ip.To16() != nil {
327 p.xerrorf("ip address is ipv6, must use syntax [IPv6:...]")
328 } else {
329 p.xerrorf("ip address is not ipv4")
330 }
331 }
332 return dns.IPDomain{IP: ip}
333 }
334 return dns.IPDomain{Domain: p.xdomain()}
335}
336
337// todo: reduce duplication between implementations: ../smtp/address.go:/xlocalpart ../dkim/parser.go:/xlocalpart ../smtpserver/parse.go:/xlocalpart
338func (p *parser) xlocalpart() smtp.Localpart {
339 // ../rfc/5321:2316
340 var s string
341 if p.hasPrefix(`"`) {
342 s = p.xquotedString(true)
343 } else {
344 s = p.xatom(true)
345 for p.take(".") {
346 s += "." + p.xatom(true)
347 }
348 }
349 // In the wild, some services use large localparts for generated (bounce) addresses.
350 if mox.Pedantic && len(s) > 64 || len(s) > 128 {
351 // ../rfc/5321:3486
352 p.xerrorf("localpart longer than 64 octets")
353 }
354 return smtp.Localpart(norm.NFC.String(s))
355}
356
357// ../rfc/5321:2324
358func (p *parser) xquotedString(islocalpart bool) string {
359 p.xtake(`"`)
360 var s string
361 var esc bool
362 for {
363 c := p.xchar()
364 if esc {
365 if c >= ' ' && c < 0x7f {
366 s += string(c)
367 esc = false
368 continue
369 }
370 p.xerrorf("invalid localpart, bad escaped char %c", c)
371 }
372 if c == '\\' {
373 esc = true
374 continue
375 }
376 if c == '"' {
377 return s
378 }
379 // ../rfc/5321:2332 ../rfc/6531:419
380 if islocalpart && c > 0x7f && !p.smtputf8 {
381 p.xutf8localparterrorf()
382 }
383 if c >= ' ' && c < 0x7f && c != '\\' && c != '"' || (c > 0x7f && p.smtputf8) {
384 s += string(c)
385 continue
386 }
387 p.xerrorf("invalid localpart, invalid character %c", c)
388 }
389}
390
391func (p *parser) xchar() rune {
392 // We are careful to track invalid utf-8 properly.
393 if p.empty() {
394 p.xerrorf("need another character")
395 }
396 var r rune
397 var o int
398 for i, c := range p.orig[p.o:] {
399 if i > 0 {
400 o = i
401 break
402 }
403 r = c
404 }
405 if o == 0 {
406 p.o = len(p.orig)
407 } else {
408 p.o += o
409 }
410 return r
411}
412
413// ../rfc/5321:2320 ../rfc/6531:414
414func (p *parser) xatom(islocalpart bool) string {
415 return p.xtakefn1("atom", func(c rune, i int) bool {
416 switch c {
417 case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~':
418 return true
419 }
420 if islocalpart && c > 0x7f && !p.smtputf8 {
421 p.xutf8localparterrorf()
422 }
423 return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || (c > 0x7f && p.smtputf8)
424 })
425}
426
427// ../rfc/5321:2338
428func (p *parser) xstring() string {
429 if p.peekchar() == '"' {
430 return p.xquotedString(false)
431 }
432 return p.xatom(false)
433}
434
435// ../rfc/5321:2279
436func (p *parser) xparamKeyword() string {
437 return p.xtakefn1("parameter keyword", func(c rune, i int) bool {
438 return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || (i > 0 && c == '-')
439 })
440}
441
442// ../rfc/5321:2281 ../rfc/6531:422
443func (p *parser) xparamValue() string {
444 return p.xtakefn1("parameter value", func(c rune, i int) bool {
445 return c > ' ' && c < 0x7f && c != '=' || (c > 0x7f && p.smtputf8)
446 })
447}
448
449// for smtp parameters that take a numeric parameter with specified number of
450// digits, eg SIZE=... for MAIL FROM.
451func (p *parser) xnumber(maxDigits int, allowZero bool) int64 {
452 s := p.xtakefn1("number", func(c rune, i int) bool {
453 return (c >= '1' && c <= '9' || c == '0' && (i > 0 || allowZero)) && i < maxDigits
454 })
455 v, err := strconv.ParseInt(s, 10, 64)
456 if err != nil {
457 p.xerrorf("bad number %q: %s", s, err)
458 }
459 return v
460}
461
462// parse date-time in UTC form. ../rfc/4865:147 ../rfc/4865-eid2040
463func (p *parser) xdatetimeutc() (time.Time, string) {
464 // ../rfc/3339:422
465 xdash := func() string {
466 p.xtake("-")
467 return "-"
468 }
469 xcolon := func() string {
470 p.xtake(":")
471 return ":"
472 }
473 xdigits := func(n int) string {
474 s := p.xtakefn1("digits", func(c rune, i int) bool {
475 return c >= '0' && c <= '9' && i < n
476 })
477 if len(s) != n {
478 p.xerrorf("parsing date-time: got %d digits, need %d", len(s), n)
479 }
480 return s
481 }
482 s := xdigits(4) + xdash() + xdigits(2) + xdash() + xdigits(2)
483 if !p.hasPrefix("T") {
484 p.xerrorf("expected T for date-time separator")
485 }
486 s += p.xtaken(1) + xdigits(2) + xcolon() + xdigits(2) + xcolon() + xdigits(2)
487 layout := time.RFC3339
488 if p.take(".") {
489 layout = time.RFC3339Nano
490 s += "." + p.xtakefn1("digits", func(c rune, i int) bool {
491 return c >= '0' && c <= '9'
492 })
493 }
494 if !p.hasPrefix("Z") {
495 p.xerrorf("expected Z for date-time utc timezone")
496 }
497 s += p.xtaken(1)
498
499 t, err := time.Parse(layout, s)
500 if err != nil {
501 p.xerrorf("bad utc date-time %q: %s", s, err)
502 }
503 return t, s
504}
505
506// sasl mechanism, for AUTH command.
507// ../rfc/4422:436
508func (p *parser) xsaslMech() string {
509 return p.xtakefn1case("sasl-mech", func(c rune, i int) bool {
510 return i < 20 && (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_')
511 })
512}
513
514// ../rfc/4954:696 ../rfc/6533:259
515func (p *parser) xtext() string {
516 var r strings.Builder
517 for !p.empty() {
518 b := p.orig[p.o]
519 if b >= 0x21 && b < 0x7f && b != '+' && b != '=' && b != ' ' {
520 r.WriteString(string(b))
521 p.xtaken(1)
522 continue
523 }
524 if b != '+' {
525 break
526 }
527 p.xtaken(1)
528 x := p.xtaken(2)
529 for _, b := range x {
530 if b >= '0' && b <= '9' || b >= 'A' && b <= 'F' {
531 continue
532 }
533 p.xerrorf("parsing xtext: invalid hexadecimal %q", x)
534 }
535 const hex = "0123456789ABCDEF"
536 b = byte(strings.IndexByte(hex, x[0])<<4) | byte(strings.IndexByte(hex, x[1])<<0)
537 r.WriteString(string(rune(b)))
538 }
539 return r.String()
540}
541