9 "github.com/mjl-/mox/dns"
10 "github.com/mjl-/mox/moxvar"
11 "github.com/mjl-/mox/smtp"
16func (e parseErr) Error() string {
20var _ error = parseErr("")
24 o int // Offset into s.
25 tracked string // All data consumed, except when "drop" is true. To be set by caller when parsing the value for "b=".
27 smtputf8 bool // If set, allow characters > 0x7f.
30func (p *parser) xerrorf(format string, args ...any) {
31 msg := fmt.Sprintf(format, args...)
33 msg = fmt.Sprintf("%s (leftover %q)", msg, p.s[p.o:])
38func (p *parser) track(s string) {
44func (p *parser) hasPrefix(s string) bool {
45 return strings.HasPrefix(p.s[p.o:], s)
48func (p *parser) xtaken(n int) string {
55func (p *parser) xtakefn(ignoreFWS bool, fn func(c rune, i int) bool) string {
57 for i, c := range p.s[p.o:] {
60 case ' ', '\t', '\r', '\n':
68 p.xtaken(len(p.s) - p.o)
72func (p *parser) empty() bool {
73 return p.o >= len(p.s)
76func (p *parser) xnonempty() {
78 p.xerrorf("expected at least 1 more char")
82func (p *parser) xtakefn1(ignoreFWS bool, fn func(c rune, i int) bool) string {
85 for i, c := range p.s[p.o:] {
88 case ' ', '\t', '\r', '\n':
92 p.xerrorf("expected at least 1 char")
99 return p.xtaken(len(p.s) - p.o)
102func (p *parser) wsp() {
103 p.xtakefn(false, func(c rune, i int) bool {
104 return c == ' ' || c == '\t'
108func (p *parser) fws() {
110 if p.hasPrefix("\r\n ") || p.hasPrefix("\r\n\t") {
116// peekfws returns whether remaining text starts with s, optionally prefix with fws.
117func (p *parser) peekfws(s string) bool {
125func (p *parser) xtake(s string) string {
126 if !strings.HasPrefix(p.s[p.o:], s) {
127 p.xerrorf("expected %q", s)
129 return p.xtaken(len(s))
132func (p *parser) take(s string) bool {
133 if strings.HasPrefix(p.s[p.o:], s) {
142func (p *parser) xtagName() string {
143 return p.xtakefn1(false, func(c rune, i int) bool {
144 return isalpha(c) || i > 0 && (isdigit(c) || c == '_')
148func (p *parser) xalgorithm() (string, string) {
150 xtagx := func(c rune, i int) bool {
151 return isalpha(c) || i > 0 && isdigit(c)
153 algk := p.xtakefn1(false, xtagx)
155 algv := p.xtakefn1(false, xtagx)
159// fws in value is ignored. empty/no base64 characters is valid.
162func (p *parser) xbase64() []byte {
164 p.xtakefn(false, func(c rune, i int) bool {
165 if isalphadigit(c) || c == '+' || c == '/' || c == '=' {
169 if c == ' ' || c == '\t' {
173 if strings.HasPrefix(rem, "\r\n ") || strings.HasPrefix(rem, "\r\n\t") {
176 if (strings.HasPrefix(rem, "\n ") || strings.HasPrefix(rem, "\n\t")) && p.o+i-1 > 0 && p.s[p.o+i-1] == '\r' {
181 buf, err := base64.StdEncoding.DecodeString(s)
183 p.xerrorf("decoding base64: %v", err)
188// parses canonicalization in original case.
189func (p *parser) xcanonical() string {
191 s := p.xhyphenatedWord()
193 return s + "/" + p.xhyphenatedWord()
198func (p *parser) xdomainselector(isselector bool) dns.Domain {
199 subdomain := func(c rune, i int) bool {
201 // dkim selectors with underscores happen in the wild, accept them when not in
203 return isalphadigit(c) || (i > 0 && (c == '-' || isselector && !moxvar.Pedantic && c == '_') && p.o+1 < len(p.s))
205 s := p.xtakefn1(false, subdomain)
206 for p.hasPrefix(".") {
207 s += p.xtake(".") + p.xtakefn1(false, subdomain)
210 // Not to be interpreted as IDNA.
211 return dns.Domain{ASCII: strings.ToLower(s)}
213 d, err := dns.ParseDomain(s)
215 p.xerrorf("parsing domain %q: %s", s, err)
220func (p *parser) xdomain() dns.Domain {
221 return p.xdomainselector(false)
224func (p *parser) xselector() dns.Domain {
225 return p.xdomainselector(true)
228func (p *parser) xhdrName(ignoreFWS bool) string {
231 // BNF for hdr-name (field-name) allows ";", but DKIM disallows unencoded semicolons.
../rfc/6376:643
233 return p.xtakefn1(ignoreFWS, func(c rune, i int) bool {
234 return c > ' ' && c < 0x7f && c != ':' && c != ';'
238func (p *parser) xsignedHeaderFields() []string {
240 l := []string{p.xhdrName(false)}
245 l = append(l, p.xhdrName(false))
250func (p *parser) xauid() Identity {
252 // Localpart is optional.
254 return Identity{Domain: p.xdomain()}
259 return Identity{&lp, dom}
262// todo: reduce duplication between implementations: ../smtp/address.go:/xlocalpart ../dkim/parser.go:/xlocalpart ../smtpserver/parse.go:/xlocalpart
263func (p *parser) xlocalpart() smtp.Localpart {
267 if p.hasPrefix(`"`) {
268 s = p.xquotedString()
275 // In the wild, some services use large localparts for generated (bounce) addresses.
276 if moxvar.Pedantic && len(s) > 64 || len(s) > 128 {
278 p.xerrorf("localpart longer than 64 octets")
280 return smtp.Localpart(s)
283func (p *parser) xquotedString() string {
290 if c >= ' ' && c < 0x7f {
295 p.xerrorf("invalid localpart, bad escaped char %c", c)
304 if c >= ' ' && c < 0x7f && c != '\\' && c != '"' || (c > 0x7f && p.smtputf8) {
308 p.xerrorf("invalid localpart, invalid character %c", c)
312func (p *parser) xchar() rune {
313 // We are careful to track invalid utf-8 properly.
315 p.xerrorf("need another character")
319 for i, c := range p.s[p.o:] {
330 p.track(p.s[p.o : p.o+o])
336func (p *parser) xatom() string {
337 return p.xtakefn1(false, func(c rune, i int) bool {
339 case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~':
342 return isalphadigit(c) || (c > 0x7f && p.smtputf8)
346func (p *parser) xbodyLength() int64 {
351func (p *parser) xnumber(maxdigits int) int64 {
353 for i, c := range p.s[p.o:] {
354 if c >= '0' && c <= '9' {
361 p.xerrorf("expected digits")
364 p.xerrorf("too many digits")
366 v, err := strconv.ParseInt(p.xtaken(o+1), 10, 64)
368 p.xerrorf("parsing digits: %s", err)
373func (p *parser) xqueryMethods() []string {
375 l := []string{p.xqtagmethod()}
379 l = append(l, p.xqtagmethod())
384func (p *parser) xqtagmethod() string {
386 s := p.xhyphenatedWord()
387 // ABNF production "x-sig-q-tag-args" should probably just have been
388 // "hyphenated-word". As qp-hdr-value, it will consume ":". A similar problem does
389 // not occur for "z" because it is also "|"-delimited. We work around the potential
390 // issue by parsing "dns/txt" explicitly.
392 if strings.EqualFold(s, "dns") && len(rem) >= len("/txt") && strings.EqualFold(rem[:len("/txt")], "/txt") {
394 } else if p.take("/") {
395 s += "/" + p.xqp(true, true, false)
400func isalpha(c rune) bool {
401 return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
404func isdigit(c rune) bool {
405 return c >= '0' && c <= '9'
408func isalphadigit(c rune) bool {
409 return isalpha(c) || isdigit(c)
413func (p *parser) xhyphenatedWord() string {
414 return p.xtakefn1(false, func(c rune, i int) bool {
415 return isalpha(c) || i > 0 && isdigit(c) || i > 0 && c == '-' && p.o+i+1 < len(p.s) && isalphadigit(rune(p.s[p.o+i+1]))
420func (p *parser) xqphdrvalue(ignoreFWS bool) string {
421 return p.xqp(true, false, ignoreFWS)
424func (p *parser) xqpSection() string {
425 return p.xqp(false, false, false)
428// dkim-quoted-printable (pipeEncoded true) or qp-section.
430// It is described in terms of (lots of) modifications to MIME quoted-printable,
431// but it may be simpler to just ignore that reference.
433// ignoreFWS is required for "z=", which can have FWS anywhere.
434func (p *parser) xqp(pipeEncoded, colonEncoded, ignoreFWS bool) string {
437 hex := func(c byte) rune {
438 if c >= '0' && c <= '9' {
441 return rune(10 + c - 'A')
447 if pipeEncoded && p.hasPrefix("|") {
450 if colonEncoded && p.hasPrefix(":") {
454 h := p.xtakefn(ignoreFWS, func(c rune, i int) bool {
455 return i < 2 && (c >= '0' && c <= '9' || c >= 'A' && c <= 'Z')
458 p.xerrorf("expected qp-hdr-value")
460 c := (hex(h[0]) << 4) | hex(h[1])
464 x := p.xtakefn(ignoreFWS, func(c rune, i int) bool {
465 return c > ' ' && c < 0x7f && c != ';' && c != '=' && !(pipeEncoded && c == '|')
475func (p *parser) xtimestamp() int64 {
480func (p *parser) xcopiedHeaderFields() []string {
482 l := []string{p.xztagcopy()}
483 for p.hasPrefix("|") {
486 l = append(l, p.xztagcopy())
491func (p *parser) xztagcopy() string {
492 // ABNF does not mention FWS (unlike for other fields), but FWS is allowed everywhere in the value...
494 f := p.xhdrName(true)
497 v := p.xqphdrvalue(true)