Просмотр исходного кода

More strict MAIL and RCPT command parsing. Addresses issue #120 (#129)

* More strict MAIL and RCPT command parsing. Addresses issue #120

* optimize command parsing: use a bytes slice instead of allocating strings or copying buffers

With the following loosening to allow for more email to get through:

- tolerate space before opening <
- local part may be 128 characters, rather than the 64 limit
Flashmob 6 лет назад
Родитель
Сommit
a910a48e28
12 измененных файлов с 1358 добавлено и 171 удалено
  1. 2 2
      api_test.go
  2. 1 1
      backends/p_redis_test.go
  3. 38 2
      client.go
  4. 3 3
      cmd/guerrillad/serve_test.go
  5. 17 13
      glide.lock
  6. 8 0
      mail/envelope.go
  7. 595 0
      mail/rfc5321/parse.go
  8. 583 0
      mail/rfc5321/parse_test.go
  9. 81 77
      server.go
  10. 2 2
      server_test.go
  11. 28 25
      tests/guerrilla_test.go
  12. 0 46
      util.go

+ 2 - 2
api_test.go

@@ -423,7 +423,7 @@ func talkToServer(address string) {
 	str, err = in.ReadString('\n')
 	fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
 	str, err = in.ReadString('\n')
-	fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+	fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 	str, err = in.ReadString('\n')
 	fmt.Fprint(conn, "DATA\r\n")
 	str, err = in.ReadString('\n')
@@ -549,7 +549,7 @@ func TestSkipAllowsHost(t *testing.T) {
 	}
 	in := bufio.NewReader(conn)
 	fmt.Fprint(conn, "HELO test\r\n")
-	fmt.Fprint(conn, "RCPT TO: [email protected]\r\n")
+	fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 	in.ReadString('\n')
 	in.ReadString('\n')
 	str, _ := in.ReadString('\n')

+ 1 - 1
backends/p_redis_test.go

@@ -12,7 +12,7 @@ import (
 func TestRedisGeneric(t *testing.T) {
 
 	e := mail.NewEnvelope("127.0.0.1", 1)
-	e.RcptTo = append(e.RcptTo, mail.Address{"test", "grr.la"})
+	e.RcptTo = append(e.RcptTo, mail.Address{User: "test", Host: "grr.la"})
 
 	l, _ := log.GetLogger("./test_redis.log", "debug")
 	g, err := New(BackendConfig{

+ 38 - 2
client.go

@@ -4,9 +4,12 @@ import (
 	"bufio"
 	"bytes"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
+	"github.com/flashmob/go-guerrilla/mail/rfc5321"
+	"github.com/flashmob/go-guerrilla/response"
 	"net"
 	"net/textproto"
 	"sync"
@@ -49,6 +52,7 @@ type client struct {
 	// guards access to conn
 	connGuard sync.Mutex
 	log       log.Logger
+	parser    rfc5321.Parser
 }
 
 // NewClient allocates a new client.
@@ -120,8 +124,7 @@ func (c *client) resetTransaction() {
 // A transaction starts after a MAIL command gets issued by the client.
 // Call resetTransaction to end the transaction
 func (c *client) isInTransaction() bool {
-	isMailFromEmpty := c.MailFrom == (mail.Address{})
-	if isMailFromEmpty {
+	if len(c.MailFrom.User) == 0 && !c.MailFrom.NullPath {
 		return false
 	}
 	return true
@@ -201,3 +204,36 @@ func getRemoteAddr(conn net.Conn) string {
 		return conn.RemoteAddr().Network()
 	}
 }
+
+type pathParser func([]byte) error
+
+func (c *client) parsePath(in []byte, p pathParser) (mail.Address, error) {
+	address := mail.Address{}
+	var err error
+	if len(in) > rfc5321.LimitPath {
+		return address, errors.New(response.Canned.FailPathTooLong.String())
+	}
+	if err = p(in); err != nil {
+		return address, errors.New(response.Canned.FailInvalidAddress.String())
+	} else if c.parser.NullPath {
+		// bounce has empty from address
+		address = mail.Address{}
+	} else if len(c.parser.LocalPart) > rfc5321.LimitLocalPart {
+		err = errors.New(response.Canned.FailLocalPartTooLong.String())
+	} else if len(c.parser.Domain) > rfc5321.LimitDomain {
+		err = errors.New(response.Canned.FailDomainTooLong.String())
+	} else {
+		address = mail.Address{
+			User:       c.parser.LocalPart,
+			Host:       c.parser.Domain,
+			ADL:        c.parser.ADL,
+			PathParams: c.parser.PathParams,
+			NullPath:   c.parser.NullPath,
+		}
+	}
+	return address, err
+}
+
+func (s *server) rcptTo(in []byte) (address mail.Address, err error) {
+	return address, err
+}

+ 3 - 3
cmd/guerrillad/serve_test.go

@@ -697,7 +697,7 @@ func TestDebug(t *testing.T) {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 			} else {
-				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
+				if result, err = test.Command(conn, buffin, "RCPT TO:<[email protected]>"); err == nil {
 					expect := "250 2.1.5 OK"
 					if strings.Index(result, expect) != 0 {
 						t.Error("Expected:", expect, "but got:", result)
@@ -744,7 +744,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 			} else {
-				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
+				if result, err = test.Command(conn, buffin, "RCPT TO:<[email protected]>"); err == nil {
 					expect := "454 4.1.1 Error: Relay access denied: grr.la"
 					if strings.Index(result, expect) != 0 {
 						t.Error("Expected:", expect, "but got:", result)
@@ -777,7 +777,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 			} else {
-				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
+				if result, err = test.Command(conn, buffin, "RCPT TO:<[email protected]>"); err == nil {
 					expect := "250 2.1.5 OK"
 					if strings.Index(result, expect) != 0 {
 						t.Error("Expected:", expect, "but got:", result)

+ 17 - 13
glide.lock

@@ -1,33 +1,37 @@
-hash: f125882976090918727d8badc2f6dbbb8565a91081159ee65d39f667eabc81fe
-updated: 2018-06-11T02:31:42.387414528+10:00
+hash: 9d467015838b5163cc5f1ebb92fa68fd4721445b2dbf236a03d002c92b4ba52a
+updated: 2018-12-24T15:20:01.207901397Z
 imports:
 - name: github.com/asaskevich/EventBus
   version: 68a521d7cbbb7a859c2608b06342f384b3bd5f5a
+- name: github.com/garyburd/redigo
+  version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
+  subpackages:
+  - internal
+  - redis
 - name: github.com/go-sql-driver/mysql
-  version: d523deb1b23d913de5bdada721a6071e71283618
+  version: 72cd26f257d44c1114970e19afddcd812016007e
 - name: github.com/gomodule/redigo
-  version: 9c11da706d9b7902c6da69c592f75637793fe121
+  version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
   subpackages:
-  - internal
   - redis
 - name: github.com/inconshreveable/mousetrap
   version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
 - name: github.com/rakyll/statik
-  version: fd36b3595eb2ec8da4b8153b107f7ea08504899d
+  version: 79258177a57a85a8ab2eca7ce0936aad80307f4e
   subpackages:
   - fs
 - name: github.com/sirupsen/logrus
-  version: c155da19408a8799da419ed3eeb0cb5db0ad5dbc
+  version: 3e01752db0189b9157070a0e1668a620f9a85da2
 - name: github.com/spf13/cobra
-  version: b62566898a99f2db9c68ed0026aa0a052e59678d
+  version: d2d81d9a96e23f0255397222bb0b4e3165e492dc
 - name: github.com/spf13/pflag
-  version: 25f8b5b07aece3207895bf19f7ab517eb3b22a40
+  version: 24fa6976df40757dce6aea913e7b81ade90530e1
 - name: golang.org/x/crypto
-  version: 8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9
+  version: 505ab145d0a99da450461ae2c1a9f6cd10d1f447
   subpackages:
   - ssh/terminal
 - name: golang.org/x/net
-  version: 1e491301e022f8f977054da4c2d852decd59571f
+  version: 927f97764cc334a6575f4b7a1584a147864d5723
   subpackages:
   - html
   - html/atom
@@ -38,7 +42,7 @@ imports:
   - unix
   - windows
 - name: golang.org/x/text
-  version: 5c1cf69b5978e5a34c5f9ba09a83e56acc4b7877
+  version: 17bcc049122f272a32787ba38073ee47433023e9
   subpackages:
   - encoding
   - encoding/charmap
@@ -58,7 +62,7 @@ imports:
   - runes
   - transform
 - name: google.golang.org/appengine
-  version: b1f26356af11148e710935ed1ac8a7f5702c7612
+  version: e9657d882bb81064595ca3b56cbe2546bbabf7b1
   subpackages:
   - cloudsql
 - name: gopkg.in/iconv.v1

+ 8 - 0
mail/envelope.go

@@ -31,8 +31,16 @@ const maxHeaderChunk = 1 + (3 << 10) // 3KB
 
 // Address encodes an email address of the form `<user@host>`
 type Address struct {
+	// User is local part
 	User string
+	// Host is the domain
 	Host string
+	// ADL is at-domain list if matched
+	ADL []string
+	// PathParams contains any ESTMP parameters that were matched
+	PathParams [][]string
+	// NullPath is true if <> was received
+	NullPath bool
 }
 
 func (ep *Address) String() string {

+ 595 - 0
mail/rfc5321/parse.go

@@ -0,0 +1,595 @@
+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
+}

+ 583 - 0
mail/rfc5321/parse_test.go

@@ -0,0 +1,583 @@
+package rfc5321
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestParseParam(t *testing.T) {
+	s := NewParser([]byte("SIZE=2000"))
+	params, err := s.param()
+	if strings.Compare(params[0], "SIZE") != 0 {
+		t.Error("SIZE ecpected")
+	}
+	if strings.Compare(params[1], "2000") != 0 {
+		t.Error("2000 ecpected")
+	}
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("SI--ZE=2000 BODY=8BITMIME"))
+	tup, err := s.parameters()
+	if strings.Compare(tup[0][0], "SI--ZE") != 0 {
+		t.Error("SI--ZE ecpected")
+	}
+	if strings.Compare(tup[0][1], "2000") != 0 {
+		t.Error("2000 ecpected")
+	}
+	if strings.Compare(tup[1][0], "BODY") != 0 {
+		t.Error("BODY expected", err)
+	}
+	if strings.Compare(tup[1][1], "8BITMIME") != 0 {
+		t.Error("8BITMIME expected", err)
+	}
+
+	s = NewParser([]byte("SI--ZE-=2000 BODY=8BITMIME")) // illegal - after ZE
+	tup, err = s.parameters()
+	if err == nil {
+		t.Error("error was expected ")
+	}
+}
+
+func TestParseRcptTo(t *testing.T) {
+	var s Parser
+	err := s.RcptTo([]byte("<Postmaster>"))
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	err = s.RcptTo([]byte("<[email protected]>"))
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+	if s.LocalPart != "Postmaster" {
+		t.Error("s.LocalPart should be: Postmaster")
+	}
+
+	err = s.RcptTo([]byte("<[email protected]> NOTIFY=SUCCESS,FAILURE"))
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	//
+}
+
+func TestParseForwardPath(t *testing.T) {
+	s := NewParser([]byte("<@a,@b:user@[227.0.0.1>")) // missing ]
+	err := s.forwardPath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	s = NewParser([]byte("<@a,@b:user@[527.0.0.1>")) // ip out of range
+	err = s.forwardPath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	// with a 'size' estmp param
+	s = NewParser([]byte("<[email protected]> NOTIFY=FAILURE ORCPT=rfc822;[email protected]"))
+	err = s.forwardPath()
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	// tolerate a space at the front
+	s = NewParser([]byte(" <[email protected]>"))
+	err = s.forwardPath()
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	// tolerate a space at the front, invalid
+	s = NewParser([]byte(" <"))
+	err = s.forwardPath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	// tolerate a space at the front, invalid
+	s = NewParser([]byte(" "))
+	err = s.forwardPath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	// empty
+	s = NewParser([]byte(""))
+	err = s.forwardPath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+}
+
+func TestParseReversePath(t *testing.T) {
+
+	s := NewParser([]byte("<@a,@b:user@d>"))
+	err := s.reversePath()
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("<@a,@b:user@d> param=some-value")) // includes a mail parameter
+	err = s.reversePath()
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("<@a,@b:user@[227.0.0.1]>"))
+	err = s.reversePath()
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("<>"))
+	err = s.reversePath()
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte(""))
+	err = s.reversePath()
+	if err == nil {
+		t.Error("error  expected ", err)
+	}
+
+	s = NewParser([]byte("[email protected]"))
+	err = s.reversePath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	s = NewParser([]byte("<@ghg;$7@65"))
+	err = s.reversePath()
+	if err == nil {
+		t.Error("error  expected ", err)
+	}
+
+	// tolerate a space at the front
+	s = NewParser([]byte(" <>"))
+	err = s.reversePath()
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	// tolerate a space at the front, invalid
+	s = NewParser([]byte(" <"))
+	err = s.reversePath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	// tolerate a space at the front, invalid
+	s = NewParser([]byte(" "))
+	err = s.reversePath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	// empty
+	s = NewParser([]byte(" "))
+	err = s.reversePath()
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+}
+
+func TestParseIpv6Address(t *testing.T) {
+	s := NewParser([]byte("2001:0000:3238:DFE1:0063:0000:0000:FEFB"))
+	err := s.ipv6AddressLiteral()
+	if s.accept.String() != "2001:0000:3238:DFE1:0063:0000:0000:FEFB" {
+		t.Error("expected 2001:0000:3238:DFE1:0063:0000:0000:FEFB, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+	s = NewParser([]byte("2001:3238:DFE1:6323:FEFB:2536:1.2.3.2"))
+	err = s.ipv6AddressLiteral()
+	if s.accept.String() != "2001:3238:DFE1:6323:FEFB:2536:1.2.3.2" {
+		t.Error("expected 2001:3238:DFE1:6323:FEFB:2536:1.2.3.2, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("2001:0000:3238:DFE1:63:0000:0000:FEFB"))
+	err = s.ipv6AddressLiteral()
+	if s.accept.String() != "2001:0000:3238:DFE1:63:0000:0000:FEFB" {
+		t.Error("expected 2001:0000:3238:DFE1:63:0000:0000:FEFB, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("2001:0000:3238:DFE1:63::FEFB"))
+	err = s.ipv6AddressLiteral()
+	if s.accept.String() != "2001:0000:3238:DFE1:63::FEFB" {
+		t.Error("expected 2001:0000:3238:DFE1:63::FEFB, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("2001:0:3238:DFE1:63::FEFB"))
+	err = s.ipv6AddressLiteral()
+	if s.accept.String() != "2001:0:3238:DFE1:63::FEFB" {
+		t.Error("expected 2001:0:3238:DFE1:63::FEFB, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("g001:0:3238:DFE1:63::FEFB"))
+	err = s.ipv6AddressLiteral()
+	if s.accept.String() != "" {
+		t.Error("expected \"\", got:", s.accept.String())
+	}
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+	s = NewParser([]byte("g001:0:3238:DFE1::63::FEFB"))
+	err = s.ipv6AddressLiteral()
+	if s.accept.String() != "" {
+		t.Error("expected \"\", got:", s.accept.String())
+	}
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+}
+
+func TestParseIpv4Address(t *testing.T) {
+	s := NewParser([]byte("0.0.0.255"))
+	err := s.ipv4AddressLiteral()
+	if s.accept.String() != "0.0.0.255" {
+		t.Error("expected 0.0.0.255, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
+	s = NewParser([]byte("0.0.0.256"))
+	err = s.ipv4AddressLiteral()
+	if s.accept.String() != "0.0.0.256" {
+		t.Error("expected 0.0.0.256, got:", s.accept.String())
+	}
+	if err == nil {
+		t.Error("error expected ", err)
+	}
+
+}
+
+func TestParseMailBoxBad(t *testing.T) {
+
+	// must be quoted
+	s := NewParser([]byte("Abc\\@[email protected]"))
+	err := s.mailbox()
+
+	if err == nil {
+		t.Error("error expected")
+	}
+
+	// must be quoted
+	s = NewParser([]byte("Fred\\ [email protected]"))
+	err = s.mailbox()
+
+	if err == nil {
+		t.Error("error expected")
+	}
+}
+
+func TestParseMailbox(t *testing.T) {
+
+	s := NewParser([]byte("jsmith@[IPv6:2001:db8::1]"))
+	err := s.mailbox()
+	if s.Domain != "2001:db8::1" {
+		t.Error("expected domain:2001:db8::1, got:", s.Domain)
+	}
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+	s = NewParser([]byte("\"qu\\{oted\"@test.com"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+	s = NewParser([]byte("\"qu\\{oted\"@[127.0.0.1]"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+	s = NewParser([]byte("jsmith@[IPv6:2001:db8::1]"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+	s = NewParser([]byte("Joe.\\[email protected]"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("\"Abc@def\"@example.com"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("\"Fred Bloggs\"@example.com"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("customer/[email protected]"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("[email protected]"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("!def!xyz%[email protected]"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("[email protected]"))
+	err = s.mailbox()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+}
+
+func TestParseLocalPart(t *testing.T) {
+	s := NewParser([]byte("\"qu\\{oted\""))
+	err := s.localPart()
+	if s.LocalPart != "qu\\{oted" {
+		t.Error("expected qu\\{oted, got:", s.LocalPart)
+	}
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("dot.string"))
+	err = s.localPart()
+	if s.LocalPart != "dot.string" {
+		t.Error("expected dot.string, got:", s.LocalPart)
+	}
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("dot.st!ring"))
+	err = s.localPart()
+	if s.LocalPart != "dot.st!ring" {
+		t.Error("expected dot.st!ring, got:", s.LocalPart)
+	}
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("dot..st!ring")) // fail
+	err = s.localPart()
+
+	if err == nil {
+		t.Error("error expected ")
+	}
+}
+
+func TestParseQuotedString(t *testing.T) {
+	s := NewParser([]byte("\"qu\\ oted\""))
+	err := s.quotedString()
+	if s.accept.String() != "qu\\ oted" {
+		t.Error("Expected qu\\ oted, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+	s = NewParser([]byte("\"@\""))
+	err = s.quotedString()
+	if s.accept.String() != "@" {
+		t.Error("Expected @, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ")
+	}
+}
+
+func TestParseDotString(t *testing.T) {
+
+	s := NewParser([]byte("Joe..\\\\Blow"))
+	err := s.dotString()
+	if err == nil {
+		t.Error("error expected ")
+	}
+
+	s = NewParser([]byte("Joe.\\\\Blow"))
+	err = s.dotString()
+	if s.accept.String() != "Joe.\\\\Blow" {
+		t.Error("Expected Joe.\\\\Blow, got:", s.accept.String())
+	}
+	if err != nil {
+		t.Error("error not expected ")
+	}
+}
+
+func TestParsePath(t *testing.T) {
+	s := NewParser([]byte("<foo>")) // requires @
+	err := s.path()
+	if err == nil {
+		t.Error("error expected ")
+	}
+	s = NewParser([]byte("<@example.com,@test.com:[email protected]>"))
+	err = s.path()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("<@example.com>")) // no mailbox
+	err = s.path()
+	if err == nil {
+		t.Error("error expected ")
+	}
+
+	s = NewParser([]byte("<[email protected]	1")) // no closing >
+	err = s.path()
+	if err == nil {
+		t.Error("error expected ")
+	}
+}
+
+func TestParseADL(t *testing.T) {
+	s := NewParser([]byte("@example.com,@test.com"))
+	err := s.adl()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+}
+
+func TestParseAtDomain(t *testing.T) {
+	s := NewParser([]byte("@example.com"))
+	err := s.atDomain()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+}
+
+func TestParseDomain(t *testing.T) {
+
+	s := NewParser([]byte("a"))
+	err := s.domain()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+	s = NewParser([]byte("a.com.gov"))
+	err = s.domain()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+
+	s = NewParser([]byte("wrong-.com"))
+	err = s.domain()
+	if err == nil {
+		t.Error("error was expected ")
+	}
+	s = NewParser([]byte("wrong."))
+	err = s.domain()
+	if err == nil {
+		t.Error("error was expected ")
+	}
+}
+
+func TestParseSubDomain(t *testing.T) {
+
+	s := NewParser([]byte("a"))
+	err := s.subdomain()
+	if err != nil {
+		t.Error("error not expected ")
+	}
+	s = NewParser([]byte("-a"))
+	err = s.subdomain()
+	if err == nil {
+		t.Error("error was expected ")
+	}
+	s = NewParser([]byte("a--"))
+	err = s.subdomain()
+	if err == nil {
+		t.Error("error was expected ")
+	}
+	s = NewParser([]byte("a--"))
+	err = s.subdomain()
+	if err == nil {
+		t.Error("error was expected ")
+	}
+	s = NewParser([]byte("a--b"))
+	err = s.subdomain()
+	if err != nil {
+		t.Error("error was not expected ")
+	}
+
+	// although a---b looks like an illegal subdomain, it is rfc5321 grammar spec
+	s = NewParser([]byte("a---b"))
+	err = s.subdomain()
+	if err != nil {
+		t.Error("error was not expected ")
+	}
+
+	s = NewParser([]byte("abc"))
+	err = s.subdomain()
+	if err != nil {
+		t.Error("error was not expected ")
+	}
+
+	s = NewParser([]byte("a-b-c"))
+	err = s.subdomain()
+	if err != nil {
+		t.Error("error was not expected ")
+	}
+
+}
+func TestParse(t *testing.T) {
+
+	s := NewParser([]byte("<"))
+	err := s.reversePath()
+	if err == nil {
+		t.Error("< expected parse error")
+	}
+
+	// the @ needs to be quoted
+	s = NewParser([]byte("<@[email protected]>"))
+	err = s.reversePath()
+	if err == nil {
+		t.Error("expected parse error", err)
+	}
+
+	s = NewParser([]byte("<\"@m.conm\"@test.com>"))
+	err = s.reversePath()
+	if err != nil {
+		t.Error("not expected parse error", err)
+	}
+
+	s = NewParser([]byte("<[email protected]>"))
+	err = s.reversePath()
+	if err != nil {
+		t.Error("not expected parse error")
+	}
+
+	s = NewParser([]byte("<@test:[email protected]>"))
+	err = s.reversePath()
+	if err != nil {
+		t.Error("not expected parse error")
+	}
+	s = NewParser([]byte("<@test,@test2:[email protected]>"))
+	err = s.reversePath()
+	if err != nil {
+		t.Error("not expected parse error")
+	}
+
+}

+ 81 - 77
server.go

@@ -1,23 +1,25 @@
 package guerrilla
 
 import (
+	"bytes"
 	"crypto/rand"
 	"crypto/tls"
+	"crypto/x509"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"net"
+	"path/filepath"
 	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
 
-	"crypto/x509"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
+	"github.com/flashmob/go-guerrilla/mail/rfc5321"
 	"github.com/flashmob/go-guerrilla/response"
-	"io/ioutil"
-	"path/filepath"
 )
 
 const (
@@ -25,14 +27,6 @@ const (
 	CommandLineMaxLength = 1024
 	// Number of allowed unrecognized commands before we terminate the connection
 	MaxUnrecognizedCommands = 5
-	// The maximum total length of a reverse-path or forward-path is 256
-	RFC2821LimitPath = 256
-	// The maximum total length of a user name or other local-part is 64
-	RFC2832LimitLocalPart = 64
-	//The maximum total length of a domain name or number is 255
-	RFC2821LimitDomain = 255
-	// The minimum total number of recipients that must be buffered is 100
-	RFC2821LimitRecipients = 100
 )
 
 const (
@@ -55,7 +49,7 @@ type server struct {
 	clientPool      *Pool
 	wg              sync.WaitGroup // for waiting to shutdown
 	listener        net.Listener
-	closedListener  chan (bool)
+	closedListener  chan bool
 	hosts           allowedHosts // stores map[string]bool for faster lookup
 	state           int
 	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
@@ -71,6 +65,27 @@ type allowedHosts struct {
 	sync.Mutex                 // guard access to the map
 }
 
+type command []byte
+
+var (
+	cmdHELO     command = []byte("HELO")
+	cmdEHLO     command = []byte("EHLO")
+	cmdHELP     command = []byte("HELP")
+	cmdXCLIENT  command = []byte("XCLIENT")
+	cmdMAIL     command = []byte("MAIL FROM:")
+	cmdRCPT     command = []byte("RCPT TO:")
+	cmdRSET     command = []byte("RSET")
+	cmdVRFY     command = []byte("VRFY")
+	cmdNOOP     command = []byte("NOOP")
+	cmdQUIT     command = []byte("QUIT")
+	cmdDATA     command = []byte("DATA")
+	cmdSTARTTLS command = []byte("STARTTLS")
+)
+
+func (c command) match(in []byte) bool {
+	return bytes.Index(in, []byte(c)) == 0
+}
+
 // Creates and returns a new ready-to-run Server from a configuration
 func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, error) {
 	server := &server{
@@ -303,27 +318,22 @@ func (server *server) allowsHost(host string) bool {
 	return false
 }
 
-// Reads from the client until a terminating sequence is encountered,
+const commandSuffix = "\r\n"
+
+// Reads from the client until a \n terminator is encountered,
 // or until a timeout occurs.
-func (server *server) readCommand(client *client, maxSize int64) (string, error) {
-	var input, reply string
+func (server *server) readCommand(client *client) ([]byte, error) {
+	//var input string
 	var err error
+	var bs []byte
 	// In command state, stop reading at line breaks
-	suffix := "\r\n"
-	for {
-		client.setTimeout(server.timeout.Load().(time.Duration))
-		reply, err = client.bufin.ReadString('\n')
-		input = input + reply
-		if err != nil {
-			break
-		}
-		if strings.HasSuffix(input, suffix) {
-			// discard the suffix and stop reading
-			input = input[0 : len(input)-len(suffix)]
-			break
-		}
+	bs, err = client.bufin.ReadSlice('\n')
+	if err != nil {
+		return bs, err
+	} else if bytes.HasSuffix(bs, []byte(commandSuffix)) {
+		return bs[:len(bs)-2], err
 	}
-	return input, err
+	return bs[:len(bs)-1], err
 }
 
 // flushResponse a response to the client. Flushes the client.bufout buffer to the connection
@@ -384,7 +394,7 @@ func (server *server) handleClient(client *client) {
 			client.state = ClientCmd
 		case ClientCmd:
 			client.bufin.setLimit(CommandLineMaxLength)
-			input, err := server.readCommand(client, sc.MaxSize)
+			input, err := server.readCommand(client)
 			server.log().Debugf("Client sent: %s", input)
 			if err == io.EOF {
 				server.log().WithError(err).Warnf("Client closed the connection: %s", client.RemoteIP)
@@ -406,20 +416,19 @@ func (server *server) handleClient(client *client) {
 				continue
 			}
 
-			input = strings.Trim(input, " \r\n")
 			cmdLen := len(input)
 			if cmdLen > CommandVerbMaxLength {
 				cmdLen = CommandVerbMaxLength
 			}
-			cmd := strings.ToUpper(input[:cmdLen])
+			cmd := bytes.ToUpper(input[:cmdLen])
 			switch {
-			case strings.Index(cmd, "HELO") == 0:
-				client.Helo = strings.Trim(input[4:], " ")
+			case cmdHELO.match(cmd):
+				client.Helo = string(bytes.Trim(input[4:], " "))
 				client.resetTransaction()
 				client.sendResponse(helo)
 
-			case strings.Index(cmd, "EHLO") == 0:
-				client.Helo = strings.Trim(input[4:], " ")
+			case cmdEHLO.match(cmd):
+				client.Helo = string(bytes.Trim(input[4:], " "))
 				client.resetTransaction()
 				client.sendResponse(ehlo,
 					messageSize,
@@ -428,88 +437,83 @@ func (server *server) handleClient(client *client) {
 					advertiseEnhancedStatusCodes,
 					help)
 
-			case strings.Index(cmd, "HELP") == 0:
+			case cmdHELP.match(cmd):
 				quote := response.GetQuote()
 				client.sendResponse("214-OK\r\n", quote)
 
-			case sc.XClientOn && strings.Index(cmd, "XCLIENT ") == 0:
-				if toks := strings.Split(input[8:], " "); len(toks) > 0 {
+			case sc.XClientOn && cmdXCLIENT.match(cmd):
+				if toks := bytes.Split(input[8:], []byte{' '}); len(toks) > 0 {
 					for i := range toks {
-						if vals := strings.Split(toks[i], "="); len(vals) == 2 {
-							if vals[1] == "[UNAVAILABLE]" {
+						if vals := bytes.Split(toks[i], []byte{'='}); len(vals) == 2 {
+							if bytes.Compare(vals[1], []byte("[UNAVAILABLE]")) == 0 {
 								// skip
 								continue
 							}
-							if vals[0] == "ADDR" {
-								client.RemoteIP = vals[1]
+							if bytes.Compare(vals[0], []byte("ADDR")) == 0 {
+								client.RemoteIP = string(vals[1])
 							}
-							if vals[0] == "HELO" {
-								client.Helo = vals[1]
+							if bytes.Compare(vals[0], []byte("HELO")) == 0 {
+								client.Helo = string(vals[1])
 							}
 						}
 					}
 				}
 				client.sendResponse(response.Canned.SuccessMailCmd)
-			case strings.Index(cmd, "MAIL FROM:") == 0:
+			case cmdMAIL.match(cmd):
 				if client.isInTransaction() {
 					client.sendResponse(response.Canned.FailNestedMailCmd)
 					break
 				}
-				addr := input[10:]
-				if !(strings.Index(addr, "<>") == 0) &&
-					!(strings.Index(addr, " <>") == 0) {
-					// Not Bounce, extract mail.
-					if from, err := extractEmail(addr); err != nil {
-						client.sendResponse(err)
-						break
-					} else {
-						client.MailFrom = from
-					}
-
-				} else {
+				client.MailFrom, err = client.parsePath([]byte(input[10:]), client.parser.MailFrom)
+				if err != nil {
+					server.log().WithError(err).Error("MAIL parse error", "["+string(input[10:])+"]")
+					client.sendResponse(err)
+					break
+				} else if client.parser.NullPath {
 					// bounce has empty from address
 					client.MailFrom = mail.Address{}
 				}
 				client.sendResponse(response.Canned.SuccessMailCmd)
 
-			case strings.Index(cmd, "RCPT TO:") == 0:
-				if len(client.RcptTo) > RFC2821LimitRecipients {
+			case cmdRCPT.match(cmd):
+				if len(client.RcptTo) > rfc5321.LimitRecipients {
 					client.sendResponse(response.Canned.ErrorTooManyRecipients)
 					break
 				}
-				to, err := extractEmail(input[8:])
+				to, err := client.parsePath([]byte(input[8:]), client.parser.RcptTo)
 				if err != nil {
+					server.log().WithError(err).Error("RCPT parse error", "["+string(input[8:])+"]")
 					client.sendResponse(err.Error())
+					break
+				}
+				if !server.allowsHost(to.Host) {
+					client.sendResponse(response.Canned.ErrorRelayDenied, " ", to.Host)
 				} else {
-					if !server.allowsHost(to.Host) {
-						client.sendResponse(response.Canned.ErrorRelayDenied, " ", to.Host)
+					client.PushRcpt(to)
+					rcptError := server.backend().ValidateRcpt(client.Envelope)
+					if rcptError != nil {
+						client.PopRcpt()
+						client.sendResponse(response.Canned.FailRcptCmd, " ", rcptError.Error())
 					} else {
-						client.PushRcpt(to)
-						rcptError := server.backend().ValidateRcpt(client.Envelope)
-						if rcptError != nil {
-							client.PopRcpt()
-							client.sendResponse(response.Canned.FailRcptCmd, " ", rcptError.Error())
-						} else {
-							client.sendResponse(response.Canned.SuccessRcptCmd)
-						}
+						client.sendResponse(response.Canned.SuccessRcptCmd)
 					}
 				}
 
-			case strings.Index(cmd, "RSET") == 0:
+			case cmdRSET.match(cmd):
 				client.resetTransaction()
 				client.sendResponse(response.Canned.SuccessResetCmd)
 
-			case strings.Index(cmd, "VRFY") == 0:
+			case cmdVRFY.match(cmd):
 				client.sendResponse(response.Canned.SuccessVerifyCmd)
 
-			case strings.Index(cmd, "NOOP") == 0:
+			case cmdNOOP.match(cmd):
 				client.sendResponse(response.Canned.SuccessNoopCmd)
 
-			case strings.Index(cmd, "QUIT") == 0:
+			case cmdQUIT.match(cmd):
 				client.sendResponse(response.Canned.SuccessQuitCmd)
 				client.kill()
 
-			case strings.Index(cmd, "DATA") == 0:
+			case cmdDATA.match(cmd):
 				if len(client.RcptTo) == 0 {
 					client.sendResponse(response.Canned.FailNoRecipientsDataCmd)
 					break
@@ -517,7 +521,7 @@ func (server *server) handleClient(client *client) {
 				client.sendResponse(response.Canned.SuccessDataCmd)
 				client.state = ClientData
 
-			case sc.TLS.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
+			case sc.TLS.StartTLSOn && cmdSTARTTLS.match(cmd):
 
 				client.sendResponse(response.Canned.SuccessStartTLSCmd)
 				client.state = ClientStartTLS

+ 2 - 2
server_test.go

@@ -337,7 +337,7 @@ func TestGatewayTimeout(t *testing.T) {
 		for i := 0; i < 2; i++ {
 			fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
 			str, err = in.ReadString('\n')
-			fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+			fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 			str, err = in.ReadString('\n')
 			fmt.Fprint(conn, "DATA\r\n")
 			str, err = in.ReadString('\n')
@@ -398,7 +398,7 @@ func TestGatewayPanic(t *testing.T) {
 		for i := 0; i < 2; i++ {
 			fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
 			str, err = in.ReadString('\n')
-			fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+			fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 			str, err = in.ReadString('\n')
 			fmt.Fprint(conn, "DATA\r\n")
 			str, err = in.ReadString('\n')

+ 28 - 25
tests/guerrilla_test.go

@@ -16,6 +16,7 @@ package test
 
 import (
 	"encoding/json"
+	"github.com/flashmob/go-guerrilla/mail/rfc5321"
 	"testing"
 
 	"time"
@@ -341,12 +342,12 @@ func TestRFC2821LimitRecipients(t *testing.T) {
 
 			for i := 0; i < 101; i++ {
 				//fmt.Println(fmt.Sprintf("RCPT TO:test%[email protected]", i))
-				if _, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:test%[email protected]", i)); err != nil {
+				if _, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<test%[email protected]>", i)); err != nil {
 					t.Error("RCPT TO", err.Error())
 					break
 				}
 			}
-			response, err := Command(conn, bufin, "RCPT TO:[email protected]")
+			response, err := Command(conn, bufin, "RCPT TO:<[email protected]>")
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -390,7 +391,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 65)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", rfc5321.LimitLocalPart+1)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -400,7 +401,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 			}
 			// what about if it's exactly 64?
 			// repeat > 64 characters in local part
-			response, err = Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 64)))
+			response, err = Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", rfc5321.LimitLocalPart-1)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -444,7 +445,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 256 characters in local part
-			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 257-7)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", 257-7)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -454,7 +455,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 			}
 			// what about if it's exactly 256?
 			response, err = Command(conn, bufin,
-				fmt.Sprintf("RCPT TO:%s@%s.la", strings.Repeat("a", 64), strings.Repeat("b", 257-5-64)))
+				fmt.Sprintf("RCPT TO:<%s@%s.la>", strings.Repeat("a", 64), strings.Repeat("b", 186)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -494,7 +495,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:a@%s.l", strings.Repeat("a", 255-2)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<a@%s.l>", strings.Repeat("a", 255-2)))
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -504,7 +505,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 			}
 			// what about if it's exactly 255?
 			response, err = Command(conn, bufin,
-				fmt.Sprintf("RCPT TO:a@%s.la", strings.Repeat("b", 255-4)))
+				fmt.Sprintf("RCPT TO:<a@%s.la>", strings.Repeat("b", 255-6)))
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -544,7 +545,7 @@ func TestMailFromCmd(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// Basic valid address
-			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -703,15 +704,17 @@ func TestMailFromCmd(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 
-			// SMTPUTF8 not implemented for now, currently still accepted
-			response, err = Command(conn, bufin, "MAIL FROM:<anö[email protected]>")
-			if err != nil {
-				t.Error("command failed", err.Error())
-			}
-			expected = "250 2.1.0 OK"
-			if strings.Index(response, expected) != 0 {
-				t.Error("Server did not respond with", expected, ", it said:"+response)
-			}
+			/*
+				// todo SMTPUTF8 not implemented for now,
+				response, err = Command(conn, bufin, "MAIL FROM:<anö[email protected]>")
+				if err != nil {
+					t.Error("command failed", err.Error())
+				}
+				expected = "250 2.1.0 OK"
+				if strings.Index(response, expected) != 0 {
+					t.Error("Server did not respond with", expected, ", it said:"+response)
+				}
+			*/
 
 			// Reset
 			response, err = Command(conn, bufin, "RSET")
@@ -868,11 +871,11 @@ func TestNestedMailCmd(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
-			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -884,7 +887,7 @@ func TestNestedMailCmd(t *testing.T) {
 			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
-			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -902,7 +905,7 @@ func TestNestedMailCmd(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 
-			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -992,7 +995,7 @@ func TestDataMaxLength(t *testing.T) {
 				t.Error("command failed", err.Error())
 			}
 			//fmt.Println(response)
-			response, err = Command(conn, bufin, "RCPT TO:[email protected]")
+			response, err = Command(conn, bufin, "RCPT TO:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -1079,12 +1082,12 @@ func TestDataCommand(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 
-			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
 			//fmt.Println(response)
-			response, err = Command(conn, bufin, "RCPT TO:[email protected]")
+			response, err = Command(conn, bufin, "RCPT TO:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}

+ 0 - 46
util.go

@@ -1,46 +0,0 @@
-package guerrilla
-
-import (
-	"errors"
-	"regexp"
-	"strings"
-
-	"github.com/flashmob/go-guerrilla/mail"
-	"github.com/flashmob/go-guerrilla/response"
-)
-
-var extractEmailRegex, _ = regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk!
-
-func extractEmail(str string) (mail.Address, error) {
-	email := mail.Address{}
-	var err error
-	if len(str) > RFC2821LimitPath {
-		return email, errors.New(response.Canned.FailPathTooLong.String())
-	}
-	if matched := extractEmailRegex.FindStringSubmatch(str); len(matched) > 2 {
-		email.User = matched[1]
-		email.Host = validHost(matched[2])
-	} else if res := strings.Split(str, "@"); len(res) > 1 {
-		email.User = strings.TrimSpace(res[0])
-		email.Host = validHost(res[1])
-	}
-	err = nil
-	if email.User == "" || email.Host == "" {
-		err = errors.New(response.Canned.FailInvalidAddress.String())
-	} else if len(email.User) > RFC2832LimitLocalPart {
-		err = errors.New(response.Canned.FailLocalPartTooLong.String())
-	} else if len(email.Host) > RFC2821LimitDomain {
-		err = errors.New(response.Canned.FailDomainTooLong.String())
-	}
-	return email, err
-}
-
-var validhostRegex, _ = regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
-
-func validHost(host string) string {
-	host = strings.Trim(host, " ")
-	if validhostRegex.MatchString(host) {
-		return host
-	}
-	return ""
-}