123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595 |
- package rfc5321
- // Parse RFC5321 productions, no regex
- import (
- "bytes"
- "errors"
- "net"
- "strconv"
- )
- const (
- // The maximum total length of a reverse-path or forward-path is 256
- LimitPath = 256
- // The maximum total length of a user name or other local-part is 64
- // however, here we double it, since a few major services don't respect that and go over
- LimitLocalPart = 64 * 2
- // //The maximum total length of a domain name or number is 255
- LimitDomain = 255
- // The minimum total number of recipients that must be buffered is 100
- LimitRecipients = 100
- )
- // Parse Email Addresses according to https://tools.ietf.org/html/rfc5321
- type Parser struct {
- NullPath bool
- LocalPart string
- Domain string
- ADL []string
- PathParams [][]string
- pos int
- ch byte
- buf []byte
- accept bytes.Buffer
- }
- func NewParser(buf []byte) *Parser {
- s := new(Parser)
- s.buf = buf
- s.pos = -1
- return s
- }
- func (s *Parser) Reset() {
- s.buf = s.buf[:0]
- if s.pos != -1 {
- s.pos = -1
- s.ADL = nil
- s.PathParams = nil
- s.NullPath = false
- s.LocalPart = ""
- s.Domain = ""
- s.accept.Reset()
- }
- }
- func (s *Parser) set(input []byte) {
- s.Reset()
- s.buf = input
- }
- func (s *Parser) next() byte {
- s.pos++
- if s.pos < len(s.buf) {
- s.ch = s.buf[s.pos]
- return s.ch
- }
- return 0
- }
- func (s *Parser) peek() byte {
- if s.pos+1 < len(s.buf) {
- return s.buf[s.pos+1]
- }
- return 0
- }
- func (s *Parser) reversePath() (err error) {
- if s.peek() == ' ' {
- s.next() // tolerate a space at the front
- }
- if i := bytes.Index(s.buf[s.pos+1:], []byte{'<', '>'}); i == 0 {
- s.NullPath = true
- return nil
- }
- if err = s.path(); err != nil {
- return err
- }
- return nil
- }
- func (s *Parser) forwardPath() (err error) {
- if s.peek() == ' ' {
- s.next() // tolerate a space at the front
- }
- if i := bytes.Index(bytes.ToLower(s.buf[s.pos+1:]), []byte(postmasterPath)); i == 0 {
- s.LocalPart = postmasterLocalPart
- return nil
- }
- if err = s.path(); err != nil {
- return err
- }
- return nil
- }
- //MailFrom accepts the following syntax: Reverse-path [SP Mail-parameters] CRLF
- func (s *Parser) MailFrom(input []byte) (err error) {
- s.set(input)
- if err := s.reversePath(); err != nil {
- return err
- }
- s.next()
- if p := s.next(); p == ' ' {
- // parse Rcpt-parameters
- // The optional <mail-parameters> are associated with negotiated SMTP
- // service extensions
- if tup, err := s.parameters(); err != nil {
- return errors.New("param parse error")
- } else if len(tup) > 0 {
- s.PathParams = tup
- }
- }
- return nil
- }
- const postmasterPath = "<postmaster>"
- const postmasterLocalPart = "Postmaster"
- //RcptTo accepts the following syntax: ( "<Postmaster@" Domain ">" / "<Postmaster>" /
- // Forward-path ) [SP Rcpt-parameters] CRLF
- func (s *Parser) RcptTo(input []byte) (err error) {
- s.set(input)
- if err := s.forwardPath(); err != nil {
- return err
- }
- s.next()
- if p := s.next(); p == ' ' {
- // parse Rcpt-parameters
- if tup, err := s.parameters(); err != nil {
- return errors.New("param parse error")
- } else if len(tup) > 0 {
- s.PathParams = tup
- }
- }
- return nil
- }
- // esmtp-param *(SP esmtp-param)
- func (s *Parser) parameters() ([][]string, error) {
- params := make([][]string, 0)
- for {
- if result, err := s.param(); err != nil {
- return params, err
- } else {
- params = append(params, result)
- }
- if p := s.next(); p != ' ' {
- return params, nil
- }
- }
- }
- func isESMTPValue(c byte) bool {
- if ('!' <= c && c <= '<') ||
- ('>' <= c && c <= '~') {
- return true
- }
- return false
- }
- // esmtp-param = esmtp-keyword ["=" esmtp-value]
- // esmtp-keyword = (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
- // esmtp-value = 1*(%d33-60 / %d62-126)
- func (s *Parser) param() (result []string, err error) {
- state := 0
- var key, value string
- defer func() {
- result = append(result, key, value)
- s.accept.Reset()
- }()
- for c := s.next(); ; c = s.next() {
- switch state {
- case 0:
- // first char must be let-dig
- if !isLetDig(c) {
- return result, errors.New("parse error")
- }
- // accept
- s.accept.WriteByte(c)
- state = 1
- case 1:
- // *(ALPHA / DIGIT / "-")
- if !isLetDig(c) {
- if c == '=' {
- key = s.accept.String()
- s.accept.Reset()
- state = 2
- continue
- } else if c == '-' {
- // cannot have - at the end of a keyword
- if p := s.peek(); !isLetDig(p) && p != '-' {
- return result, errors.New("parse error")
- }
- s.accept.WriteByte(c)
- continue
- }
- key = s.accept.String()
- return result, nil
- }
- s.accept.WriteByte(c)
- case 2:
- // start of value, must match at least 1
- if !isESMTPValue(c) {
- return result, errors.New("parse error")
- }
- s.accept.WriteByte(c)
- if !isESMTPValue(s.peek()) {
- value = s.accept.String()
- return result, nil
- }
- state = 3
- case 3:
- // 1*(%d33-60 / %d62-126)
- s.accept.WriteByte(c)
- if !isESMTPValue(s.peek()) {
- value = s.accept.String()
- return result, nil
- }
- }
- }
- }
- // "<" [ A-d-l ":" ] Mailbox ">"
- func (s *Parser) path() (err error) {
- if s.next() == '<' && s.peek() == '@' {
- if err = s.adl(); err == nil {
- s.next()
- if s.ch != ':' {
- return errors.New("syntax error")
- }
- }
- }
- if err = s.mailbox(); err != nil {
- return err
- }
- if p := s.peek(); p != '>' {
- return errors.New("missing closing >")
- }
- return nil
- }
- // At-domain *( "," At-domain )
- func (s *Parser) adl() error {
- for {
- if err := s.atDomain(); err != nil {
- return err
- }
- s.ADL = append(s.ADL, s.accept.String())
- s.accept.Reset()
- if s.peek() != ',' {
- break
- }
- s.next()
- }
- return nil
- }
- // At-domain = "@" Domain
- func (s *Parser) atDomain() error {
- if s.next() == '@' {
- s.accept.WriteByte('@')
- return s.domain()
- }
- return errors.New("syntax error")
- }
- // sub-domain *("." sub-domain)
- func (s *Parser) domain() error {
- for {
- if err := s.subdomain(); err != nil {
- return err
- }
- if p := s.peek(); p != '.' {
- if p != ':' && p != ',' && p != '>' && p != 0 {
- return errors.New("domain parse error")
- }
- break
- }
- s.accept.WriteByte(s.next())
- }
- return nil
- }
- // Let-dig [Ldh-str]
- func (s *Parser) subdomain() error {
- state := 0
- for c := s.next(); ; c = s.next() {
- switch state {
- case 0:
- p := s.peek()
- if isLetDig(c) {
- s.accept.WriteByte(c)
- if !isLetDig(p) && p != '-' {
- return nil
- }
- state = 1
- continue
- }
- return errors.New("parse err")
- case 1:
- p := s.peek()
- if isLetDig(c) || c == '-' {
- s.accept.WriteByte(c)
- }
- if !isLetDig(p) && p != '-' {
- if c == '-' {
- return errors.New("parse err")
- }
- return nil
- }
- }
- }
- }
- // Local-part "@" ( Domain / address-literal )
- func (s *Parser) mailbox() error {
- defer func() {
- if s.accept.Len() > 0 {
- s.Domain = s.accept.String()
- s.accept.Reset()
- }
- }()
- err := s.localPart()
- if err != nil {
- return err
- }
- if s.ch != '@' {
- return errors.New("@ expected as part of mailbox")
- }
- if p := s.peek(); p == '[' {
- return s.addressLiteral()
- } else {
- return s.domain()
- }
- }
- // "[" ( IPv4-address-literal /
- // IPv6-address-literal /
- // General-address-literal ) "]"
- func (s *Parser) addressLiteral() error {
- ch := s.next()
- if ch == '[' {
- p := s.peek()
- var err error
- if p == 'I' || p == 'i' {
- for i := 0; i < 5; i++ {
- s.next() // IPv6:
- }
- err = s.ipv6AddressLiteral()
- } else if p >= 48 && p <= 57 {
- err = s.ipv4AddressLiteral()
- }
- if err != nil {
- return err
- }
- if s.ch != ']' {
- return errors.New("] expected for address literal")
- }
- return nil
- }
- return nil
- }
- // Snum 3("." Snum)
- func (s *Parser) ipv4AddressLiteral() error {
- for i := 0; i < 4; i++ {
- if err := s.snum(); err != nil {
- return err
- }
- if s.ch != '.' {
- break
- }
- s.accept.WriteByte(s.ch)
- }
- return nil
- }
- // 1*3DIGIT
- // representing a decimal integer
- // value accept the range 0 through 255
- func (s *Parser) snum() error {
- state := 0
- var num bytes.Buffer
- for i := 4; i > 0; i-- {
- c := s.next()
- if state == 0 {
- if !(c >= 48 && c <= 57) {
- return errors.New("parse error")
- } else {
- num.WriteByte(s.ch)
- s.accept.WriteByte(s.ch)
- state = 1
- continue
- }
- }
- if state == 1 {
- if !(c >= 48 && c <= 57) {
- if v, err := strconv.Atoi(num.String()); err != nil {
- return err
- } else if v >= 0 && v <= 255 {
- return nil
- } else {
- return errors.New("invalid ipv4")
- }
- } else {
- num.WriteByte(s.ch)
- s.accept.WriteByte(s.ch)
- }
- }
- }
- return errors.New("too many digits")
- }
- //IPv6:" IPv6-addr
- func (s *Parser) ipv6AddressLiteral() error {
- var ip bytes.Buffer
- for c := s.next(); ; c = s.next() {
- if !(c >= 48 && c <= 57) &&
- !(c >= 65 && c <= 70) &&
- !(c >= 97 && c <= 102) &&
- c != ':' && c != '.' {
- ipstr := ip.String()
- if v := net.ParseIP(ipstr); v != nil {
- s.accept.WriteString(ipstr)
- return nil
- }
- return errors.New("invalid ipv6")
- } else {
- ip.WriteByte(c)
- }
- }
- }
- // Dot-string / Quoted-string
- func (s *Parser) localPart() error {
- defer func() {
- if s.accept.Len() > 0 {
- s.LocalPart = s.accept.String()
- s.accept.Reset()
- }
- }()
- p := s.peek()
- if p == '"' {
- return s.quotedString()
- } else {
- return s.dotString()
- }
- }
- // DQUOTE *QcontentSMTP DQUOTE
- func (s *Parser) quotedString() error {
- if s.next() == '"' {
- if err := s.QcontentSMTP(); err != nil {
- return err
- }
- if s.ch != '"' {
- return errors.New("quoted string not closed")
- } else {
- // accept the "
- s.next()
- }
- }
- return nil
- }
- // qtextSMTP / quoted-pairSMTP
- // quoted-pairSMTP = %d92 %d32-126
- // qtextSMTP = %d32-33 / %d35-91 / %d93-126
- func (s *Parser) QcontentSMTP() error {
- state := 0
- for {
- ch := s.next()
- switch state {
- case 0:
- if ch == '\\' {
- state = 1
- s.accept.WriteByte(ch)
- continue
- } else if ch == 32 || ch == 33 ||
- (ch >= 35 && ch <= 91) ||
- (ch >= 93 && ch <= 126) {
- s.accept.WriteByte(ch)
- continue
- }
- return nil
- case 1:
- // escaped character state
- if ch >= 32 && ch <= 126 {
- s.accept.WriteByte(ch)
- state = 0
- continue
- } else {
- return errors.New("non-printable character found")
- }
- }
- }
- }
- //Dot-string = Atom *("." Atom)
- func (s *Parser) dotString() error {
- for {
- if err := s.atom(); err != nil {
- return err
- }
- if s.ch != '.' {
- break
- }
- s.accept.WriteByte(s.ch)
- }
- return nil
- }
- // 1*atext
- func (s *Parser) atom() error {
- state := 0
- for {
- if state == 0 {
- if !s.isAtext(s.next()) {
- return errors.New("parse error")
- } else {
- s.accept.WriteByte(s.ch)
- state = 1
- continue
- }
- }
- if state == 1 {
- if !s.isAtext(s.next()) {
- return nil
- } else {
- s.accept.WriteByte(s.ch)
- }
- }
- }
- }
- /*
- Dot-string = Atom *("." Atom)
- Atom = 1*atext
- atext = ALPHA / DIGIT / ; Any character except controls,
- "!" / "#" / ; SP, and specials.
- "$" / "%" / ; Used for atoms
- "&" / "'" /
- "*" / "+" /
- "-" / "/" /
- "=" / "?" /
- "^" / "_" /
- "`" / "{" /
- "|" / "}" /
- "~"
- */
- func (s *Parser) isAtext(c byte) bool {
- if ('0' <= c && c <= '9') ||
- ('A' <= c && c <= 'z') ||
- c == '!' || c == '#' ||
- c == '$' || c == '%' ||
- c == '&' || c == '\'' ||
- c == '*' || c == '+' ||
- c == '-' || c == '/' ||
- c == '=' || c == '?' ||
- c == '^' || c == '_' ||
- c == '`' || c == '{' ||
- c == '|' || c == '}' ||
- c == '~' {
- return true
- }
- return false
- }
- func isLetDig(c byte) bool {
- if ('0' <= c && c <= '9') ||
- ('A' <= c && c <= 'z') {
- return true
- }
- return false
- }
|