Browse Source

Merge branch 'master' into stream

flashmob 5 years ago
parent
commit
9a24d7a2b7

+ 1 - 2
.travis.yml

@@ -1,10 +1,9 @@
 language: go
 sudo: false
 go:
-  - 1.9
-  - 1.10.x
   - 1.11.x
   - 1.12.x
+  - 1.13.x
   - master
 
 cache:

+ 1 - 1
README.md

@@ -270,7 +270,7 @@ Using Nginx as a proxy
 
 For such purposes as load balancing, terminating TLS early,
  or supporting SSL versions not supported by Go (highly not recommended if you
- want to use older SSL versions), 
+ want to use older TLS/SSL versions), 
  it is possible to [use NGINX as a proxy](https://github.com/flashmob/go-guerrilla/wiki/Using-Nginx-as-a-proxy).
 
 

+ 10 - 2
backends/p_guerrilla_db_redis.go

@@ -435,11 +435,19 @@ func GuerrillaDbRedis() Decorator {
 					e.Subject,
 					ts)
 				e.QueuedId = hash
+
 				// Add extra headers
+				protocol := "SMTP"
+				if e.ESMTP {
+					protocol = "E" + protocol
+				}
+				if e.TLS {
+					protocol = protocol + "S"
+				}
 				var addHead string
 				addHead += "Delivered-To: " + to + "\r\n"
-				addHead += "Received: from " + e.Helo + " (" + e.Helo + "  [" + e.RemoteIP + "])\r\n"
-				addHead += "	by " + e.RcptTo[0].Host + " with SMTP id " + hash + "@" + e.RcptTo[0].Host + ";\r\n"
+				addHead += "Received: from " + e.RemoteIP + " ([" + e.RemoteIP + "])\r\n"
+				addHead += "	by " + e.RcptTo[0].Host + " with " + protocol + " id " + hash + "@" + e.RcptTo[0].Host + ";\r\n"
 				addHead += "	" + time.Now().Format(time.RFC1123Z) + "\r\n"
 
 				// data will be compressed when printed, with addHead added to beginning

+ 9 - 2
backends/p_header.go

@@ -54,11 +54,18 @@ func Header() Decorator {
 				if len(e.Hashes) > 0 {
 					hash = e.Hashes[0]
 				}
+				protocol := "SMTP"
+				if e.ESMTP {
+					protocol = "E" + protocol
+				}
+				if e.TLS {
+					protocol = protocol + "S"
+				}
 				var addHead string
 				addHead += "Delivered-To: " + to + "\n"
-				addHead += "Received: from " + e.Helo + " (" + e.Helo + "  [" + e.RemoteIP + "])\n"
+				addHead += "Received: from " + e.RemoteIP + " ([" + e.RemoteIP + "])\n"
 				if len(e.RcptTo) > 0 {
-					addHead += "	by " + e.RcptTo[0].Host + " with SMTP id " + hash + "@" + e.RcptTo[0].Host + ";\n"
+					addHead += "	by " + e.RcptTo[0].Host + " with " + protocol + " id " + hash + "@" + e.RcptTo[0].Host + ";\n"
 				}
 				addHead += "	" + time.Now().Format(time.RFC1123Z) + "\n"
 				// save the result

+ 1 - 1
backends/p_redis.go

@@ -107,7 +107,7 @@ func Redis() Decorator {
 					if doErr != nil {
 						Log().WithError(doErr).Warn("Error while SETEX to redis")
 						result := NewResult(response.Canned.FailBackendTransaction)
-						return result, redisErr
+						return result, doErr
 					}
 					e.Values["redis"] = "redis" // the next processor will know to look in redis for the message data
 				} else {

+ 2 - 0
client.go

@@ -229,6 +229,8 @@ func (c *client) parsePath(in []byte, p pathParser) (mail.Address, error) {
 			ADL:        c.parser.ADL,
 			PathParams: c.parser.PathParams,
 			NullPath:   c.parser.NullPath,
+			Quoted:     c.parser.LocalPartQuotes,
+			IP:         c.parser.IP,
 		}
 	}
 	return address, err

+ 10 - 10
cmd/guerrillad/serve_test.go

@@ -684,7 +684,7 @@ func TestServerAddEvent(t *testing.T) {
 	if conn, buffin, err := test.Connect(newServer, 20); err != nil {
 		t.Error("Could not connect to new server", newServer.ListenInterface, err)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
@@ -766,7 +766,7 @@ func TestServerStartEvent(t *testing.T) {
 	if conn, buffin, err := test.Connect(newConf.Servers[1], 20); err != nil {
 		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 enable.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
@@ -845,7 +845,7 @@ func TestServerStopEvent(t *testing.T) {
 	if conn, buffin, err := test.Connect(newConf.Servers[1], 20); err != nil {
 		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 enable.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
@@ -958,7 +958,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 	if conn, buffin, err := test.Connect(conf.Servers[1], 20); err != nil {
 		t.Error("Could not connect to new server", conf.Servers[1].ListenInterface, err)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 secure.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
@@ -997,7 +997,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 	if conn, buffin, err := test.Connect(conf.Servers[1], 20); err != nil {
 		t.Error("Could not connect to new server", conf.Servers[1].ListenInterface, err)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 secure.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
@@ -1068,7 +1068,7 @@ func TestTLSConfigEvent(t *testing.T) {
 		if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 			t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 		} else {
-			if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 				expect := "250 mail.test.com Hello"
 				if strings.Index(result, expect) != 0 {
 					t.Error("Expected", expect, "but got", result)
@@ -1254,7 +1254,7 @@ func TestBadTLSReload(t *testing.T) {
 	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
@@ -1294,7 +1294,7 @@ func TestBadTLSReload(t *testing.T) {
 	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
@@ -1373,7 +1373,7 @@ func TestSetTimeoutEvent(t *testing.T) {
 	} else {
 		waitTimeout.Add(1)
 		go func() {
-			if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 				expect := "250 mail.test.com Hello"
 				if strings.Index(result, expect) != 0 {
 					t.Error("Expected", expect, "but got", result)
@@ -1442,7 +1442,7 @@ func TestDebugLevelChange(t *testing.T) {
 	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
-		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+		if result, err := test.Command(conn, buffin, "HELO example.com"); err == nil {
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)

+ 11 - 6
config.go

@@ -42,6 +42,8 @@ type ServerConfig struct {
 	LogFile string `json:"log_file,omitempty"`
 	// Hostname will be used in the server's reply to HELO/EHLO. If TLS enabled
 	// make sure that the Hostname matches the cert. Defaults to os.Hostname()
+	// Hostname will also be used to fill the 'Host' property when the "RCPT TO" address is
+	// addressed to just <postmaster>
 	Hostname string `json:"host_name"`
 	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	ListenInterface string `json:"listen_interface"`
@@ -96,9 +98,13 @@ type ServerTLSConfig struct {
 // https://golang.org/pkg/crypto/tls/#pkg-constants
 // Ciphers introduced before Go 1.7 are listed here,
 // ciphers since Go 1.8, see tls_go1.8.go
+// ....... since Go 1.13, see tls_go1.13.go
 var TLSCiphers = map[string]uint16{
 
-	// // Note: Generally avoid using CBC unless for compatibility
+	// Note: Generally avoid using CBC unless for compatibility
+	// The following ciphersuites are not configurable for TLS 1.3
+	// see tls_go1.13.go for a list of ciphersuites always used in TLS 1.3
+
 	"TLS_RSA_WITH_3DES_EDE_CBC_SHA":        tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
 	"TLS_RSA_WITH_AES_128_CBC_SHA":         tls.TLS_RSA_WITH_AES_128_CBC_SHA,
 	"TLS_RSA_WITH_AES_256_CBC_SHA":         tls.TLS_RSA_WITH_AES_256_CBC_SHA,
@@ -118,13 +124,12 @@ var TLSCiphers = map[string]uint16{
 	"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384":   tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
 	"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
 
-	// Include to prevent downgrade attacks
-	"TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
+	// see tls_go1.13 for new TLS 1.3 ciphersuites
+	// Note that TLS 1.3 ciphersuites are not configurable
 }
 
 // https://golang.org/pkg/crypto/tls/#pkg-constants
 var TLSProtocols = map[string]uint16{
-	"ssl3.0": tls.VersionSSL30,
 	"tls1.0": tls.VersionTLS10,
 	"tls1.1": tls.VersionTLS11,
 	"tls1.2": tls.VersionTLS12,
@@ -172,7 +177,7 @@ func (c *AppConfig) Load(jsonBytes []byte) error {
 		}
 	}
 
-	// read the timestamps for the ssl keys, to determine if they need to be reloaded
+	// read the timestamps for the TLS keys, to determine if they need to be reloaded
 	for i := 0; i < len(c.Servers); i++ {
 		if err := c.Servers[i].loadTlsKeyTimestamps(); err != nil {
 			return err
@@ -402,7 +407,7 @@ func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla)
 	}
 }
 
-// Loads in timestamps for the ssl keys
+// Loads in timestamps for the TLS keys
 func (sc *ServerConfig) loadTlsKeyTimestamps() error {
 	var statErr = func(iface string, err error) error {
 		return fmt.Errorf(

+ 4 - 4
goguerrilla.conf.sample

@@ -31,8 +31,8 @@
                 "tls_always_on":false,
                 "private_key_file":"/path/to/pem/file/test.com.key",
                 "public_key_file":"/path/to/pem/file/test.com.crt",
-                "protocols" : ["ssl3.0", "tls1.2"],
-                "ciphers" : ["TLS_FALLBACK_SCSV", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"],
+                "protocols" : ["tls1.0", "tls1.2"],
+                "ciphers" : ["TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"],
                 "curves" : ["P256", "P384", "P521", "X25519"],
                 "client_auth_type" : "NoClientCert"
             }
@@ -50,8 +50,8 @@
                 "public_key_file":"/path/to/pem/file/test.com.crt",
                  "start_tls_on":false,
                  "tls_always_on":true,
-                 "protocols" : ["ssl3.0", "tls1.2"],
-                 "ciphers" : ["TLS_FALLBACK_SCSV", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"],
+                 "protocols" : ["tls1.0", "tls1.2"],
+                 "ciphers" : ["TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"],
                  "curves" : ["P256", "P384", "P521", "X25519"],
                  "client_auth_type" : "NoClientCert"
             }

+ 1 - 1
guerrilla.go

@@ -320,7 +320,7 @@ func (g *guerrilla) subscribeEvents() {
 	// TLS changes
 	events[EventConfigServerTLSConfig] = serverEvent(func(sc *ServerConfig) {
 		if server, err := g.findServer(sc.ListenInterface); err == nil {
-			if err := server.configureSSL(); err == nil {
+			if err := server.configureTLS(); err == nil {
 				g.mainlog().Infof("Server [%s] new TLS configuration loaded", sc.ListenInterface)
 			} else {
 				g.mainlog().WithError(err).Errorf("Server [%s] failed to load the new TLS configuration", sc.ListenInterface)

+ 65 - 0
mail/encoding/encoding_test.go

@@ -15,5 +15,70 @@ func TestEncodingMimeHeaderDecode(t *testing.T) {
 	str = mail.MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
 	if strings.Index(str, "André Pirard") != 0 {
 		t.Error("expecting André Pirard, got:", str)
+
+	}
+
+	str = mail.MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?=\tPirard <[email protected]>")
+	if strings.Index(str, "André\tPirard") != 0 {
+		t.Error("expecting André Pirard, got:", str)
+
+	}
+
+}
+
+// TestEncodingMimeHeaderDecodeEnding tests when the encoded word is at the end
+func TestEncodingMimeHeaderDecodeEnding(t *testing.T) {
+
+	// plaintext at the beginning
+	str := mail.MimeHeaderDecode("What about this one? =?ISO-8859-1?Q?Andr=E9?=")
+	if str != "What about this one? André" {
+		t.Error("expecting: What about this one? André, but got:", str)
+
+	}
+
+	// not plaintext at beginning
+	str = mail.MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= What about this one? =?ISO-8859-1?Q?Andr=E9?=")
+	if str != "André What about this one? André" {
+		t.Error("expecting: André What about this one? André, but got:", str)
+
+	}
+	// plaintext at beginning corruped
+	str = mail.MimeHeaderDecode("=?ISO-8859-1?B?Andr=E9?= What about this one? =?ISO-8859-1?Q?Andr=E9?=")
+	if strings.Index(str, "=?ISO-8859-1?B?Andr=E9?= What about this one? André") != 0 {
+		t.Error("expecting:=?ISO-8859-1?B?Andr=E9?= What about this one? André, but got:", str)
+
+	}
+}
+
+// TestEncodingMimeHeaderDecodeBad tests the case of a malformed encoding
+func TestEncodingMimeHeaderDecodeBad(t *testing.T) {
+	// bad base64 encoding, it should return the string unencoded
+	str := mail.MimeHeaderDecode("=?ISO-8859-1?B?Andr=E9?=\tPirard <[email protected]>")
+	if strings.Index(str, "=?ISO-8859-1?B?Andr=E9?=\tPirard <[email protected]>") != 0 {
+		t.Error("expecting =?ISO-8859-1?B?Andr=E9?=\tPirard <[email protected]>, got:", str)
+
+	}
+
+}
+
+func TestEncodingMimeHeaderDecodeNoSpace(t *testing.T) {
+	// there is no space
+	str := mail.MimeHeaderDecode("A =?ISO-8859-1?Q?Andr=E9?=WORLD IN YOUR POCKET")
+	if str != "A AndréWORLD IN YOUR POCKET" {
+		// in this case, if it's QP and ?= is found at the end then we can assume no space?
+		t.Error("Did not get [A AndréWORLD IN YOUR POCKET]")
+	}
+}
+
+func TestEncodingMimeHeaderDecodeMulti(t *testing.T) {
+
+	str := mail.MimeHeaderDecode("=?iso-2022-jp?B?GyRCIVpLXEZ8Om89fCFbPEIkT0lUOk5NUSROJU0lPyROSn0bKEI=?= =?iso-2022-jp?B?GyRCJCxCPyQkJEckORsoQg==?=")
+	if strings.Index(str, "【本日削除】実は不採用のネタの方が多いです") != 0 {
+		t.Error("expecting 【本日削除】実は不採用のネタの方が多いです, got:", str)
+	}
+
+	str = mail.MimeHeaderDecode("=?iso-2022-jp?B?GyRCIVpLXEZ8Om89fCFbPEIkT0lUOk5NUSROJU0lPyROSn0bKEI=?= \t =?iso-2022-jp?B?GyRCJCxCPyQkJEckORsoQg==?=")
+	if strings.Index(str, "【本日削除】実は不採用のネタの方が多いです") != 0 {
+		t.Error("expecting 【本日削除】実は不採用のネタの方が多いです, got:", str)
 	}
 }

+ 206 - 47
mail/envelope.go

@@ -5,14 +5,15 @@ import (
 	"crypto/md5"
 	"errors"
 	"fmt"
-	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"io"
 	"mime"
-	"net/mail"
+	"net"
 	"net/textproto"
 	"strings"
 	"sync"
 	"time"
+
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 )
 
 // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
@@ -39,37 +40,83 @@ type Address struct {
 	PathParams []smtp.PathParam
 	// NullPath is true if <> was received
 	NullPath bool
+	// Quoted indicates if the local-part needs quotes
+	Quoted bool
+	// IP stores the IP Address, if the Host is an IP
+	IP net.IP
+	// DisplayName is a label before the address (RFC5322)
+	DisplayName string
+	// DisplayNameQuoted is true when DisplayName was quoted
+	DisplayNameQuoted bool
 }
 
-func (ep *Address) String() string {
-	return fmt.Sprintf("%s@%s", ep.User, ep.Host)
+func (a *Address) String() string {
+	var local string
+	if a.IsEmpty() {
+		return ""
+	}
+	if a.User == "postmaster" && a.Host == "" {
+		return "postmaster"
+	}
+	if a.Quoted {
+		var sb bytes.Buffer
+		sb.WriteByte('"')
+		for i := 0; i < len(a.User); i++ {
+			if a.User[i] == '\\' || a.User[i] == '"' {
+				// escape
+				sb.WriteByte('\\')
+			}
+			sb.WriteByte(a.User[i])
+		}
+		sb.WriteByte('"')
+		local = sb.String()
+	} else {
+		local = a.User
+	}
+	if a.Host != "" {
+		if a.IP != nil {
+			return fmt.Sprintf("%s@[%s]", local, a.Host)
+		}
+		return fmt.Sprintf("%s@%s", local, a.Host)
+	}
+	return local
 }
 
-func (ep *Address) IsEmpty() bool {
-	return ep.User == "" && ep.Host == ""
+func (a *Address) IsEmpty() bool {
+	return a.User == "" && a.Host == ""
 }
 
+func (a *Address) IsPostmaster() bool {
+	if a.User == "postmaster" {
+		return true
+	}
+	return false
+}
 var ap = mail.AddressParser{}
 var apLock sync.Mutex // guards mail.AddressParser
 
 // NewAddress takes a string of an RFC 5322 address of the
 // form "Gogh Fir <[email protected]>" or "[email protected]".
-func NewAddress(str string) (Address, error) {
+func NewAddress(str string) (*Address, error) {
 	apLock.Lock()
 	defer apLock.Unlock()
-	a, err := ap.Parse(str)
+	l, err := ap.Address([]byte(str))
 	if err != nil {
-		return Address{}, err
+		return nil, err
 	}
-	pos := strings.Index(a.Address, "@")
-	if pos > 0 {
-		return Address{
-				User: a.Address[0:pos],
-				Host: a.Address[pos+1:],
-			},
-			nil
+	if len(l.List) == 0 {
+		return nil, errors.New("no email address matched")
 	}
-	return Address{}, errors.New("invalid address")
+	a := new(Address)
+	addr := &l.List[0]
+	a.User = addr.LocalPart
+	a.Quoted = addr.LocalPartQuoted
+	a.Host = addr.Domain
+	a.IP = addr.IP
+	a.DisplayName = addr.DisplayName
+	a.DisplayNameQuoted = addr.DisplayNameQuoted
+	a.NullPath = addr.NullPath
+	return a, nil
 }
 
 // Envelope of Email represents a single SMTP message.
@@ -100,6 +147,8 @@ type Envelope struct {
 	QueuedId string
 	// TransportType indicates whenever 8BITMIME extension has been signaled
 	TransportType smtp.TransportType
+	// ESMTP: true if EHLO was used
+	ESMTP bool
 	// When locked, it means that the envelope is being processed by the backend
 	sync.Mutex
 }
@@ -180,6 +229,7 @@ func (e *Envelope) Reseed(remoteIP string, clientID uint64) {
 	e.QueuedId = queuedID(clientID)
 	e.Helo = ""
 	e.TLS = false
+	e.ESMTP = false
 }
 
 // PushRcpt adds a recipient email address to the envelope
@@ -194,57 +244,166 @@ func (e *Envelope) PopRcpt() Address {
 	return ret
 }
 
+const (
+	statePlainText = iota
+	stateStartEncodedWord
+	stateEncodedWord
+	stateEncoding
+	stateCharset
+	statePayload
+	statePayloadEnd
+)
+
 // MimeHeaderDecode converts 7 bit encoded mime header strings to UTF-8
 func MimeHeaderDecode(str string) string {
-	state := 0
-	var buf bytes.Buffer
-	var out []byte
+	// optimized to only create an output buffer if there's need to
+	// the `out` buffer is only made if an encoded word was decoded without error
+	// `out` is made with the capacity of len(str)
+	// a simple state machine is used to detect the start & end of encoded word and plain-text
+	state := statePlainText
+	var (
+		out        []byte
+		wordStart  int  // start of an encoded word
+		wordLen    int  // end of an encoded
+		ptextStart = -1 // start of plan-text
+		ptextLen   int  // end of plain-text
+	)
 	for i := 0; i < len(str); i++ {
 		switch state {
-		case 0:
+		case statePlainText:
+			if ptextStart == -1 {
+				ptextStart = i
+			}
 			if str[i] == '=' {
-				buf.WriteByte(str[i])
-				state = 1
+				state = stateStartEncodedWord
+				wordStart = i
+				wordLen = 1
 			} else {
-				out = append(out, str[i])
+				ptextLen++
 			}
-		case 1:
+		case stateStartEncodedWord:
 			if str[i] == '?' {
-				buf.WriteByte(str[i])
-				state = 2
+				wordLen++
+				state = stateCharset
 			} else {
-				out = append(out, str[i])
-				buf.Reset()
-				state = 0
+				wordLen = 0
+				state = statePlainText
+				ptextLen++
+			}
+		case stateCharset:
+			if str[i] == '?' {
+				wordLen++
+				state = stateEncoding
+			} else if str[i] >= 'a' && str[i] <= 'z' ||
+				str[i] >= 'A' && str[i] <= 'Z' ||
+				str[i] >= '0' && str[i] <= '9' || str[i] == '-' {
+				wordLen++
+			} else {
+				// error
+				state = statePlainText
+				ptextLen += wordLen
+				wordLen = 0
+			}
+		case stateEncoding:
+			if str[i] == '?' {
+				wordLen++
+				state = statePayload
+			} else if str[i] == 'Q' || str[i] == 'q' || str[i] == 'b' || str[i] == 'B' {
+				wordLen++
+			} else {
+				// abort
+				state = statePlainText
+				ptextLen += wordLen
+				wordLen = 0
 			}
 
-		case 2:
-			if str[i] == ' ' {
-				d, err := Dec.Decode(buf.String())
-				if err == nil {
-					out = append(out, []byte(d)...)
+		case statePayload:
+			if str[i] == '?' {
+				wordLen++
+				state = statePayloadEnd
+			} else {
+				wordLen++
+			}
+
+		case statePayloadEnd:
+			if str[i] == '=' {
+				wordLen++
+				var err error
+				out, err = decodeWordAppend(ptextLen, out, str, ptextStart, wordStart, wordLen)
+				if err != nil && out == nil {
+					// special case: there was an error with decoding and `out` wasn't created
+					// we can assume the encoded word as plaintext
+					ptextLen += wordLen //+ 1 // add 1 for the space/tab
+					wordLen = 0
+					wordStart = 0
+					state = statePlainText
+					continue
+				}
+				if skip := hasEncodedWordAhead(str, i+1); skip != -1 {
+					i = skip
 				} else {
-					out = append(out, buf.Bytes()...)
+					out = makeAppend(out, len(str), []byte{})
 				}
-				out = append(out, ' ')
-				buf.Reset()
-				state = 0
+				ptextStart = -1
+				ptextLen = 0
+				wordLen = 0
+				wordStart = 0
+				state = statePlainText
 			} else {
-				buf.WriteByte(str[i])
+				// abort
+				state = statePlainText
+				ptextLen += wordLen
+				wordLen = 0
 			}
+
 		}
 	}
-	if buf.Len() > 0 {
-		d, err := Dec.Decode(buf.String())
-		if err == nil {
-			out = append(out, []byte(d)...)
-		} else {
-			out = append(out, buf.Bytes()...)
-		}
+
+	if out != nil && ptextLen > 0 {
+		out = makeAppend(out, len(str), []byte(str[ptextStart:ptextStart+ptextLen]))
+		ptextLen = 0
+	}
+
+	if out == nil {
+		// best case: there was nothing to encode
+		return str
 	}
 	return string(out)
 }
 
+func decodeWordAppend(ptextLen int, out []byte, str string, ptextStart int, wordStart int, wordLen int) ([]byte, error) {
+	if ptextLen > 0 {
+		out = makeAppend(out, len(str), []byte(str[ptextStart:ptextStart+ptextLen]))
+	}
+	d, err := Dec.Decode(str[wordStart : wordLen+wordStart])
+	if err == nil {
+		out = makeAppend(out, len(str), []byte(d))
+	} else if out != nil {
+		out = makeAppend(out, len(str), []byte(str[wordStart:wordLen+wordStart]))
+	}
+	return out, err
+}
+
+func makeAppend(out []byte, size int, in []byte) []byte {
+	if out == nil {
+		out = make([]byte, 0, size)
+	}
+	out = append(out, in...)
+	return out
+}
+
+func hasEncodedWordAhead(str string, i int) int {
+	for ; i+2 < len(str); i++ {
+		if str[i] != ' ' && str[i] != '\t' {
+			return -1
+		}
+		if str[i+1] == '=' && str[i+2] == '?' {
+			return i
+		}
+	}
+	return -1
+}
+
 // Envelopes have their own pool
 
 type Pool struct {

+ 76 - 3
mail/envelope_test.go

@@ -21,19 +21,46 @@ func TestMimeHeaderDecode(t *testing.T) {
 	*/
 
 	str := MimeHeaderDecode("=?utf-8?B?55So5oi34oCcRXBpZGVtaW9sb2d5IGluIG51cnNpbmcgYW5kIGg=?=  =?utf-8?B?ZWFsdGggY2FyZSBlQm9vayByZWFkL2F1ZGlvIGlkOm8=?=  =?utf-8?B?cTNqZWVr4oCd5Zyo572R56uZ4oCcU1BZ5Lit5paH5a6Y5pa5572R56uZ4oCd?=  =?utf-8?B?55qE5biQ5Y+36K+m5oOF?=")
-	if i := strings.Index(str, "用户“Epidemiology in nursing and h  ealth care eBook read/audio id:o  q3jeek”在网站“SPY中文官方网站”  的帐号详情"); i != 0 {
-		t.Error("expecting 用户“Epidemiology in nursing and h  ealth care eBook read/audio id:o  q3jeek”在网站“SPY中文官方网站”  的帐号详情, got:", str)
+	if i := strings.Index(str, "用户“Epidemiology in nursing and health care eBook read/audio id:oq3jeek”在网站“SPY中文官方网站”的帐号详情"); i != 0 {
+		t.Error("\nexpecting \n用户“Epidemiology in nursing and h ealth care eBook read/audio id:oq3jeek”在网站“SPY中文官方网站”的帐号详情\n got:\n", str)
 	}
 	str = MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
 	if strings.Index(str, "André Pirard") != 0 {
 		t.Error("expecting André Pirard, got:", str)
 	}
 }
+
+// TestMimeHeaderDecodeNone tests strings without any encoded words
+func TestMimeHeaderDecodeNone(t *testing.T) {
+	// in the best case, there will be nothing to decode
+	str := MimeHeaderDecode("Andre Pirard <[email protected]>")
+	if strings.Index(str, "Andre Pirard") != 0 {
+		t.Error("expecting Andre Pirard, got:", str)
+	}
+
+}
+
+func TestAddressPostmaster(t *testing.T) {
+	addr := &Address{User: "postmaster"}
+	str := addr.String()
+	if str != "postmaster" {
+		t.Error("it was not postmaster,", str)
+	}
+}
+
+func TestAddressNull(t *testing.T) {
+	addr := &Address{NullPath: true}
+	str := addr.String()
+	if str != "" {
+		t.Error("it was not empty", str)
+	}
+}
+
 func TestNewAddress(t *testing.T) {
 
 	addr, err := NewAddress("<hoop>")
 	if err == nil {
-		t.Error("there should be an error:", addr)
+		t.Error("there should be an error:", err)
 	}
 
 	addr, err = NewAddress(`Gogh Fir <[email protected]>`)
@@ -41,6 +68,34 @@ func TestNewAddress(t *testing.T) {
 		t.Error("there should be no error:", addr.Host, err)
 	}
 }
+
+func TestQuotedAddress(t *testing.T) {
+
+	str := `<"  yo-- man wazz'''up? surprise \surprise, this is [email protected] "@example.com>`
+	//str = `<"post\master">`
+	addr, err := NewAddress(str)
+	if err != nil {
+		t.Error("there should be no error:", err)
+	}
+
+	str = addr.String()
+	// in this case, string should remove the unnecessary escape
+	if strings.Contains(str, "\\surprise") {
+		t.Error("there should be no \\surprise:", err)
+	}
+
+}
+
+func TestAddressWithIP(t *testing.T) {
+	str := `<"  yo-- man wazz'''up? surprise \surprise, this is [email protected] "@[64.233.160.71]>`
+	addr, err := NewAddress(str)
+	if err != nil {
+		t.Error("there should be no error:", err)
+	} else if addr.IP == nil {
+		t.Error("expecting the address host to be an IP")
+	}
+}
+
 func TestEnvelope(t *testing.T) {
 	e := NewEnvelope("127.0.0.1", 22)
 
@@ -87,3 +142,21 @@ func TestEnvelope(t *testing.T) {
 	}
 
 }
+
+func TestEncodedWordAhead(t *testing.T) {
+	str := "=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>"
+	if hasEncodedWordAhead(str, 24) != -1 {
+		t.Error("expecting no encoded word ahead")
+	}
+
+	str = "=?ISO-8859-1?Q?Andr=E9?= ="
+	if hasEncodedWordAhead(str, 24) != -1 {
+		t.Error("expecting no encoded word ahead")
+	}
+
+	str = "=?ISO-8859-1?Q?Andr=E9?= =?ISO-8859-1?Q?Andr=E9?="
+	if hasEncodedWordAhead(str, 24) == -1 {
+		t.Error("expecting an encoded word ahead")
+	}
+
+}

+ 257 - 0
mail/smtp/address.go

@@ -0,0 +1,257 @@
+package rfc5321
+
+import (
+	"errors"
+	"net"
+)
+
+// Parse productions according to ABNF in RFC5322
+type RFC5322 struct {
+	AddressList
+	Parser
+	addr SingleAddress
+}
+
+type AddressList struct {
+	List  []SingleAddress
+	Group string
+}
+
+type SingleAddress struct {
+	DisplayName       string
+	DisplayNameQuoted bool
+	LocalPart         string
+	LocalPartQuoted   bool
+	Domain            string
+	IP                net.IP
+	NullPath          bool
+}
+
+var (
+	errNotAtom               = errors.New("not atom")
+	errExpectingAngleAddress = errors.New("not angle address")
+	errNotAWord              = errors.New("not a word")
+	errExpectingColon        = errors.New("expecting : ")
+	errExpectingSemicolon    = errors.New("expecting ; ")
+	errExpectingAngleClose   = errors.New("expecting >")
+	errExpectingAngleOpen    = errors.New("< expected")
+	errQuotedUnclosed        = errors.New("quoted string not closed")
+)
+
+// Address parses the "address" production specified in RFC5322
+// address         =   mailbox / group
+func (s *RFC5322) Address(input []byte) (AddressList, error) {
+	s.set(input)
+	s.next()
+	s.List = nil
+	s.addr = SingleAddress{}
+	if err := s.mailbox(); err != nil {
+		if s.ch == ':' {
+			if groupErr := s.group(); groupErr != nil {
+				return s.AddressList, groupErr
+			} else {
+				err = nil
+			}
+		}
+		return s.AddressList, err
+
+	}
+	return s.AddressList, nil
+}
+
+// group  =  display-name ":" [group-List] ";" [CFWS]
+func (s *RFC5322) group() error {
+	if s.addr.DisplayName == "" {
+		if err := s.displayName(); err != nil {
+			return err
+		}
+	} else {
+		s.Group = s.addr.DisplayName
+		s.addr.DisplayName = ""
+	}
+	if s.ch != ':' {
+		return errExpectingColon
+	}
+	s.next()
+	_ = s.groupList()
+	s.skipSpace()
+	if s.ch != ';' {
+		return errExpectingSemicolon
+	}
+	return nil
+}
+
+// mailbox  =   name-addr / addr-spec
+func (s *RFC5322) mailbox() error {
+	pos := s.pos // save the position
+	if err := s.nameAddr(); err != nil {
+		if err == errExpectingAngleAddress && s.ch != ':' { // ':' means it's a group
+			// we'll attempt to parse as an email address without angle brackets
+			s.addr.DisplayName = ""
+			s.addr.DisplayNameQuoted = false
+			s.pos = pos - 1 //- 1 // rewind to the saved position
+			if s.pos > -1 {
+				s.ch = s.buf[s.pos]
+			}
+			if err = s.Parser.mailbox(); err != nil {
+				return err
+			}
+			s.addAddress()
+		} else {
+			return err
+		}
+	}
+	return nil
+}
+
+// addAddress ads the current address to the List
+func (s *RFC5322) addAddress() {
+	s.addr.LocalPart = s.LocalPart
+	s.addr.LocalPartQuoted = s.LocalPartQuotes
+	s.addr.Domain = s.Domain
+	s.addr.IP = s.IP
+	s.List = append(s.List, s.addr)
+	s.addr = SingleAddress{}
+}
+
+// nameAddr consumes the name-addr production.
+// name-addr =  [display-name] angle-addr
+func (s *RFC5322) nameAddr() error {
+	_ = s.displayName()
+	if s.ch == '<' {
+		if err := s.angleAddr(); err != nil {
+			return err
+		}
+		s.next()
+		if s.ch != '>' {
+			return errExpectingAngleClose
+		}
+		s.addAddress()
+		return nil
+	} else {
+		return errExpectingAngleAddress
+	}
+
+}
+
+// angleAddr consumes the angle-addr production
+// angle-addr      =   [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
+func (s *RFC5322) angleAddr() error {
+	s.skipSpace()
+	if s.ch != '<' {
+		return errExpectingAngleOpen
+	}
+	// addr-spec       =   local-part "@" domain
+	if err := s.Parser.mailbox(); err != nil {
+		return err
+	}
+	s.skipSpace()
+	return nil
+}
+
+// displayName consumes the display-name production:
+// display-name    =   phrase
+// phrase          =   1*word / obs-phrase
+func (s *RFC5322) displayName() error {
+	defer func() {
+		if s.accept.Len() > 0 {
+			s.addr.DisplayName = s.accept.String()
+			s.accept.Reset()
+		}
+	}()
+	// phrase
+	if err := s.word(); err != nil {
+		return err
+	}
+	for {
+		err := s.word()
+		if err != nil {
+			return nil
+		}
+	}
+}
+
+// quotedString consumes a quoted-string production
+func (s *RFC5322) quotedString() error {
+	if s.ch == '"' {
+		if err := s.Parser.QcontentSMTP(); err != nil {
+			return err
+		}
+		if s.ch != '"' {
+			return errQuotedUnclosed
+		} else {
+			// accept the "
+			s.next()
+		}
+	}
+	return nil
+}
+
+// word = atom / quoted-string
+func (s *RFC5322) word() error {
+	if s.ch == '"' {
+		s.addr.DisplayNameQuoted = true
+		return s.quotedString()
+	} else if s.isAtext(s.ch) || s.ch == ' ' || s.ch == '\t' {
+		return s.atom()
+	}
+	return errNotAWord
+}
+
+// atom = [CFWS] 1*atext [CFWS]
+func (s *RFC5322) atom() error {
+	s.skipSpace()
+	if !s.isAtext(s.ch) {
+		return errNotAtom
+	}
+	for {
+		if s.isAtext(s.ch) {
+			s.accept.WriteByte(s.ch)
+			s.next()
+		} else {
+			skipped := s.skipSpace()
+			if !s.isAtext(s.ch) {
+				return nil
+			}
+			if skipped > 0 {
+				s.accept.WriteByte(' ')
+			}
+			s.accept.WriteByte(s.ch)
+			s.next()
+		}
+	}
+}
+
+// groupList consumes the "group-List" production:
+// group-List      =   mailbox-List / CFWS / obs-group-List
+func (s *RFC5322) groupList() error {
+	// mailbox-list    =   (mailbox *("," mailbox))
+	if err := s.mailbox(); err != nil {
+		return err
+	}
+	s.next()
+	for {
+		s.skipSpace()
+		if s.ch != ',' {
+			return nil
+		}
+		s.next()
+		s.skipSpace()
+		if err := s.mailbox(); err != nil {
+			return err
+		}
+		s.next()
+	}
+}
+
+// skipSpace skips vertical space by calling next(), returning the count of spaces skipped
+func (s *RFC5322) skipSpace() int {
+	var skipped int
+	for {
+		if s.ch != ' ' && s.ch != 9 {
+			return skipped
+		}
+		s.next()
+		skipped++
+	}
+}

+ 84 - 0
mail/smtp/address_test.go

@@ -0,0 +1,84 @@
+package rfc5321
+
+import (
+	"testing"
+)
+
+func TestParseRFC5322(t *testing.T) {
+	var s RFC5322
+	if _, err := s.Address([]byte("\"Mike Jones\" <[email protected]>")); err != nil {
+		t.Error(err)
+	}
+	// parse a simple address
+	if a, err := s.Address([]byte("[email protected]")); err != nil {
+		t.Error(err)
+	} else {
+		if len(a.List) != 1 {
+			t.Error("expecting 1 address")
+		} else {
+			// display name should be empty
+		}
+	}
+}
+
+func TestParseRFC5322Decoder(t *testing.T) {
+	var s RFC5322
+	if _, err := s.Address([]byte("=?ISO-8859-1?Q?Andr=E9?= =?ISO-8859-1?Q?Andr=E9?= <[email protected]>")); err != nil {
+		t.Error(err)
+	}
+}
+
+func TestParseRFC5322IP(t *testing.T) {
+	var s RFC5322
+	// this is an incorrect IPv6 address
+	if _, err := s.Address([]byte("\"Mike Jones\" <\"testing 123\"@[IPv6:IPv6:2001:db8::1]>")); err == nil {
+		t.Error("Expecting error, because Ip address was wrong")
+	}
+	// this one is correct, with quoted display name and quoted local-part
+	if a, err := s.Address([]byte("\"Mike Jones\" <\"testing 123\"@[IPv6:2001:db8::1]>")); err != nil {
+		t.Error(err)
+	} else {
+		if len(a.List) != 1 {
+			t.Error("expecting 1 address, but got", len(a.List))
+		} else {
+			if a.List[0].DisplayNameQuoted == false {
+				t.Error(".List[0].DisplayNameQuoted is false, expecting true")
+			}
+			if a.List[0].LocalPartQuoted == false {
+				t.Error(".List[0].LocalPartQuotes is false, expecting true")
+			}
+			if a.List[0].IP == nil {
+				t.Error("a.List[0].IP should not be nil")
+			}
+			if a.List[0].Domain != "2001:db8::1" {
+				t.Error("a.List[0].Domain should be, but got", a.List[0].Domain)
+			}
+		}
+	}
+}
+
+func TestParseRFC5322Group(t *testing.T) {
+	// A Group:Ed Jones <[email protected]>,[email protected],John <[email protected]>;
+	var s RFC5322
+	if a, err := s.Address([]byte("A Group:Ed Jones <[email protected]>,[email protected],John <[email protected]> , \"te \\\" st\"<[email protected]> ;")); err != nil {
+		t.Error(err)
+	} else {
+		if a.Group != "A Group" {
+			t.Error("expecting a.Group to be \"A Group\" but got:", a.Group)
+		}
+		if len(a.List) != 4 {
+			t.Error("expecting 4 addresses, but got", len(a.List))
+		} else {
+			if a.List[0].DisplayName != "Ed Jones" {
+				t.Error("expecting a.List[0].DisplayName 'Ed Jones' but got:", a.List[0].DisplayName)
+			}
+			if a.List[0].LocalPart != "c" {
+				t.Error("expecting a.List[0].LocalPart 'c' but got:", a.List[0].LocalPart)
+			}
+			if a.List[0].Domain != "a.test" {
+				t.Error("expecting a.List[0].Domain 'a.test' but got:", a.List[0].Domain)
+			}
+		}
+
+	}
+}

+ 86 - 18
mail/smtp/parse.go

@@ -21,7 +21,6 @@ const (
 	// The minimum total number of recipients that must be buffered is 100
 	LimitRecipients = 100
 )
-
 type PathParam []string
 
 type TransportType int
@@ -50,11 +49,16 @@ func (p PathParam) Transport() TransportType {
 	return TransportTypeInvalid
 }
 
+var atExpected = errors.New("@ expected as part of mailbox")
+
+
 // Parse Email Addresses according to https://tools.ietf.org/html/rfc5321
 type Parser struct {
 	NullPath  bool
 	LocalPart string
+	LocalPartQuotes bool   // does the local part need quotes?
 	Domain    string
+	IP              net.IP
 
 	ADL        []string
 	PathParams []PathParam
@@ -83,6 +87,8 @@ func (s *Parser) Reset() {
 		s.LocalPart = ""
 		s.Domain = ""
 		s.accept.Reset()
+		s.LocalPartQuotes = false
+		s.IP = nil
 	}
 }
 
@@ -125,14 +131,15 @@ 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 {
+	if err = s.path(); err != nil && err != atExpected {
 		return err
 	}
-	return nil
+	// special case for forwardPath only - can just be addressed to postmaster
+	if i := strings.Index(strings.ToLower(s.LocalPart), postmasterLocalPart); i == 0 {
+		s.LocalPart = postmasterLocalPart
+		return nil // atExpected will be ignored, postmaster doesn't need @
+	}
+	return err // it may return atExpected
 }
 
 //MailFrom accepts the following syntax: Reverse-path [SP Mail-parameters] CRLF
@@ -155,8 +162,7 @@ func (s *Parser) MailFrom(input []byte) (err error) {
 	return nil
 }
 
-const postmasterPath = "<postmaster>"
-const postmasterLocalPart = "Postmaster"
+const postmasterLocalPart = "postmaster"
 
 //RcptTo accepts the following syntax: ( "<Postmaster@" Domain ">" / "<Postmaster>" /
 //                  Forward-path ) [SP Rcpt-parameters] CRLF
@@ -340,7 +346,7 @@ func (s *Parser) subdomain() error {
 				state = 1
 				continue
 			}
-			return errors.New("parse err")
+			return errors.New("subdomain parse err")
 		case 1:
 			p := s.peek()
 			if isLetDig(c) || c == '-' {
@@ -348,7 +354,7 @@ func (s *Parser) subdomain() error {
 			}
 			if !isLetDig(p) && p != '-' {
 				if c == '-' {
-					return errors.New("parse err")
+					return errors.New("subdomain parse err")
 				}
 				return nil
 			}
@@ -369,7 +375,7 @@ func (s *Parser) mailbox() error {
 		return err
 	}
 	if s.ch != '@' {
-		return errors.New("@ expected as part of mailbox")
+		return atExpected
 	}
 	if p := s.peek(); p == '[' {
 		return s.addressLiteral()
@@ -416,6 +422,11 @@ func (s *Parser) ipv4AddressLiteral() error {
 		}
 		s.accept.WriteByte(s.ch)
 	}
+	ip := net.ParseIP(s.accept.String())
+	if ip == nil {
+		return errors.New("invalid ip")
+	}
+	s.IP = ip
 	return nil
 }
 
@@ -429,7 +440,7 @@ func (s *Parser) snum() error {
 		c := s.next()
 		if state == 0 {
 			if !(c >= 48 && c <= 57) {
-				return errors.New("parse error")
+				return errors.New("snum parse error")
 			} else {
 				num.WriteByte(s.ch)
 				s.accept.WriteByte(s.ch)
@@ -465,7 +476,8 @@ func (s *Parser) ipv6AddressLiteral() error {
 			c != ':' && c != '.' {
 			ipstr := ip.String()
 			if v := net.ParseIP(ipstr); v != nil {
-				s.accept.WriteString(ipstr)
+				s.accept.WriteString(v.String())
+				s.IP = v
 				return nil
 			}
 			return errors.New("invalid ipv6")
@@ -514,15 +526,19 @@ func (s *Parser) QcontentSMTP() error {
 	state := 0
 	for {
 		ch := s.next()
+
 		switch state {
 		case 0:
 			if ch == '\\' {
 				state = 1
-				s.accept.WriteByte(ch)
+				//	s.accept.WriteByte(ch)
 				continue
 			} else if ch == 32 || ch == 33 ||
 				(ch >= 35 && ch <= 91) ||
 				(ch >= 93 && ch <= 126) {
+				if s.LocalPartQuotes == false && !s.isAtext(ch) {
+					s.LocalPartQuotes = true
+				}
 				s.accept.WriteByte(ch)
 				continue
 			}
@@ -530,6 +546,9 @@ func (s *Parser) QcontentSMTP() error {
 		case 1:
 			// escaped character state
 			if ch >= 32 && ch <= 126 {
+				if s.LocalPartQuotes == false && !s.isAtext(ch) {
+					s.LocalPartQuotes = true
+				}
 				s.accept.WriteByte(ch)
 				state = 0
 				continue
@@ -560,7 +579,7 @@ func (s *Parser) atom() error {
 	for {
 		if state == 0 {
 			if !s.isAtext(s.next()) {
-				return errors.New("parse error")
+				return errors.New("atom parse error")
 			} else {
 				s.accept.WriteByte(s.ch)
 				state = 1
@@ -599,7 +618,8 @@ atext           =       ALPHA / DIGIT / ; Any character except controls,
 
 func (s *Parser) isAtext(c byte) bool {
 	if ('0' <= c && c <= '9') ||
-		('A' <= c && c <= 'z') ||
+		('a' <= c && c <= 'z') ||
+		('A' <= c && c <= 'Z') ||
 		c == '!' || c == '#' ||
 		c == '$' || c == '%' ||
 		c == '&' || c == '\'' ||
@@ -617,8 +637,56 @@ func (s *Parser) isAtext(c byte) bool {
 
 func isLetDig(c byte) bool {
 	if ('0' <= c && c <= '9') ||
-		('A' <= c && c <= 'z') {
+		('A' <= c && c <= 'Z') ||
+		('a' <= c && c <= 'z') {
 		return true
 	}
 	return false
 }
+
+//ehlo = "EHLO" SP ( Domain / address-literal ) CRLF
+// Note: "HELO" is ignored here
+func (s *Parser) Ehlo(input []byte) (domain string, ip net.IP, err error) {
+	s.set(input)
+	s.next()
+	if s.ch == ' ' {
+		if p := s.peek(); p == '[' {
+			err = s.addressLiteral()
+			if err == nil {
+				domain = s.accept.String()
+				ip = net.ParseIP(domain)
+				if ip == nil {
+					err = errors.New("invalid ip")
+				}
+				return
+			}
+		} else {
+			err = s.domain()
+			if err == nil {
+				domain = s.accept.String()
+			}
+			return
+		}
+	} else {
+		err = errors.New("ehlo parse error")
+	}
+	return domain, ip, err
+
+}
+
+// helo  = "HELO" SP Domain CRLF
+// Note: "HELO" is ignored here, so is the CRLF at the end
+func (s *Parser) Helo(input []byte) (domain string, err error) {
+	s.set(input)
+	s.next()
+	if s.ch == ' ' {
+		err = s.domain()
+		if err == nil {
+			domain = s.accept.String()
+		}
+		return
+	} else {
+		err = errors.New("helo parse error")
+	}
+	return
+}

+ 90 - 24
mail/smtp/parse_test.go

@@ -51,8 +51,8 @@ func TestParseRcptTo(t *testing.T) {
 	if err != nil {
 		t.Error("error not expected ", err)
 	}
-	if s.LocalPart != "Postmaster" {
-		t.Error("s.LocalPart should be: Postmaster")
+	if s.LocalPart != "postmaster" {
+		t.Error("s.LocalPart should be: postmaster")
 	}
 
 	err = s.RcptTo([]byte("<[email protected]> NOTIFY=SUCCESS,FAILURE"))
@@ -60,7 +60,11 @@ func TestParseRcptTo(t *testing.T) {
 		t.Error("error not expected ", err)
 	}
 
-	//
+	err = s.RcptTo([]byte("<\"Postmaster\">"))
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+
 }
 
 func TestParseForwardPath(t *testing.T) {
@@ -189,16 +193,16 @@ func TestParseReversePath(t *testing.T) {
 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 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("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 s.accept.String() != "2001:3238:dfe1:6323:fefb:2536:102:302" {
+		t.Error("expected 2001:3238:dfe1:6323:fefb:2536:102:302, got:", s.accept.String())
 	}
 	if err != nil {
 		t.Error("error not expected ", err)
@@ -206,8 +210,8 @@ func TestParseIpv6Address(t *testing.T) {
 
 	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 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)
@@ -215,8 +219,8 @@ func TestParseIpv6Address(t *testing.T) {
 
 	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 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)
@@ -224,8 +228,8 @@ func TestParseIpv6Address(t *testing.T) {
 
 	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 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)
@@ -295,7 +299,7 @@ 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)
+		t.Error("expected domain: 2001:db8::1, got:", s.Domain)
 	}
 	if err != nil {
 		t.Error("error not expected ")
@@ -321,8 +325,8 @@ func TestParseMailbox(t *testing.T) {
 
 	s = NewParser([]byte("Joe.\\[email protected]"))
 	err = s.mailbox()
-	if err != nil {
-		t.Error("error not expected ")
+	if err == nil {
+		t.Error("error expected ")
 	}
 	s = NewParser([]byte("\"Abc@def\"@example.com"))
 	err = s.mailbox()
@@ -360,9 +364,12 @@ func TestParseMailbox(t *testing.T) {
 func TestParseLocalPart(t *testing.T) {
 	s := NewParser([]byte("\"qu\\{oted\""))
 	err := s.localPart()
-	if s.LocalPart != "qu\\{oted" {
+	if s.LocalPart != "qu{oted" {
 		t.Error("expected qu\\{oted, got:", s.LocalPart)
 	}
+	if s.LocalPartQuotes == true {
+		t.Error("local part does not need to be quoted")
+	}
 	if err != nil {
 		t.Error("error not expected ")
 	}
@@ -393,8 +400,8 @@ func TestParseLocalPart(t *testing.T) {
 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 s.accept.String() != "qu oted" {
+		t.Error("Expected qu oted, got:", s.accept.String())
 	}
 	if err != nil {
 		t.Error("error not expected ")
@@ -412,16 +419,16 @@ func TestParseQuotedString(t *testing.T) {
 
 func TestParseDotString(t *testing.T) {
 
-	s := NewParser([]byte("Joe..\\\\Blow"))
+	s := NewParser([]byte("Joe..Blow"))
 	err := s.dotString()
 	if err == nil {
 		t.Error("error expected ")
 	}
 
-	s = NewParser([]byte("Joe.\\\\Blow"))
+	s = NewParser([]byte("Joe.Blow"))
 	err = s.dotString()
-	if s.accept.String() != "Joe.\\\\Blow" {
-		t.Error("Expected Joe.\\\\Blow, got:", s.accept.String())
+	if s.accept.String() != "Joe.Blow" {
+		t.Error("Expected Joe.Blow, got:", s.accept.String())
 	}
 	if err != nil {
 		t.Error("error not expected ")
@@ -476,6 +483,12 @@ func TestParseDomain(t *testing.T) {
 		t.Error("error not expected ")
 	}
 
+	s = NewParser([]byte("a^m.com"))
+	err = s.domain()
+	if err == nil {
+		t.Error("error was expected ")
+	}
+
 	s = NewParser([]byte("a.com.gov"))
 	err = s.domain()
 	if err != nil {
@@ -542,6 +555,16 @@ func TestParseSubDomain(t *testing.T) {
 	}
 
 }
+
+func TestPostmasterQuoted(t *testing.T) {
+
+	var s Parser
+	err := s.RcptTo([]byte("<\"Po\\stmas\\ter\">"))
+	if err != nil {
+		t.Error("error not expected ", err)
+	}
+}
+
 func TestParse(t *testing.T) {
 
 	s := NewParser([]byte("<"))
@@ -582,6 +605,49 @@ func TestParse(t *testing.T) {
 
 }
 
+func TestEhlo(t *testing.T) {
+	var s Parser
+	domain, ip, err := s.Ehlo([]byte(" hello.com"))
+	if ip != nil {
+		t.Error("ip should be nil")
+	}
+	if err != nil {
+		t.Error(err)
+	}
+	if domain != "hello.com" {
+		t.Error("domain not hello.com")
+	}
+
+	domain, ip, err = s.Ehlo([]byte(" [211.0.0.3]"))
+	if err != nil {
+		t.Error(err)
+	}
+	if ip == nil {
+		t.Error("ip should not be nil")
+	}
+
+	if domain != "211.0.0.3" {
+		t.Error("expecting domain to be 211.0.0.3")
+	}
+}
+
+func TestHelo(t *testing.T) {
+	var s Parser
+	domain, err := s.Helo([]byte(" example.com"))
+	if err != nil {
+		t.Error(err)
+	}
+	if domain != "example.com" {
+		t.Error("expecting domain = example.com")
+	}
+
+	domain, err = s.Helo([]byte(" exam_ple.com"))
+	if err == nil {
+		t.Error("expecting domain exam_ple.com to be invalid")
+	}
+}
+
+
 func TestTransport(t *testing.T) {
 
 	path := PathParam([]string{"BODY", "8bitmime"})
@@ -607,4 +673,4 @@ func TestTransport(t *testing.T) {
 	if transport != TransportTypeUnspecified {
 		t.Error("transport was not unspecified")
 	}
-}
+}

+ 11 - 3
response/enhanced.go

@@ -127,6 +127,7 @@ type Responses struct {
 	FailNoRecipientsDataCmd      *Response
 	FailUnrecognizedCmd          *Response
 	FailMaxUnrecognizedCmd       *Response
+	FailSyntaxError              *Response
 	FailReadLimitExceededDataCmd *Response
 	FailMessageSizeExceeded      *Response
 	FailReadErrorDataCmd         *Response
@@ -267,16 +268,23 @@ func init() {
 		Comment:      "Server is shutting down. Please try again later. Sayonara!",
 	}
 
-	Canned.FailReadLimitExceededDataCmd = &Response{
+	Canned.FailSyntaxError = &Response{
 		EnhancedCode: SyntaxError,
 		BasicCode:    550,
 		Class:        ClassPermanentFailure,
+		Comment:      "Syntax error",
+	}
+
+	Canned.FailReadLimitExceededDataCmd = &Response{
+		EnhancedCode: MessageLengthExceedsAdministrativeLimit,
+		BasicCode:    550,
+		Class:        ClassPermanentFailure,
 		Comment:      "Error:",
 	}
 
 	Canned.FailMessageSizeExceeded = &Response{
-		EnhancedCode: SyntaxError,
-		BasicCode:    550,
+		EnhancedCode: OtherOrUndefinedNetworkOrRoutingStatus,
+		BasicCode:    552,
 		Class:        ClassPermanentFailure,
 		Comment:      "Error:",
 	}

+ 44 - 5
server.go

@@ -6,6 +6,7 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"fmt"
+	"github.com/sirupsen/logrus"
 	"io"
 	"io/ioutil"
 	"net"
@@ -112,13 +113,13 @@ func newServer(sc *ServerConfig, b backends.Backend, mainlog log.Logger) (*serve
 	}
 	server.setConfig(sc)
 	server.setTimeout(sc.Timeout)
-	if err := server.configureSSL(); err != nil {
+	if err := server.configureTLS(); err != nil {
 		return server, err
 	}
 	return server, nil
 }
 
-func (s *server) configureSSL() error {
+func (s *server) configureTLS() error {
 	sConfig := s.configStore.Load().(ServerConfig)
 	if sConfig.TLS.AlwaysOn || sConfig.TLS.StartTLSOn {
 		cert, err := tls.LoadX509KeyPair(sConfig.TLS.PublicKeyFile, sConfig.TLS.PrivateKeyFile)
@@ -217,6 +218,11 @@ func (s *server) setAllowedHosts(allowedHosts []string) {
 	for _, h := range allowedHosts {
 		if strings.Contains(h, "*") {
 			s.hosts.wildcards = append(s.hosts.wildcards, strings.ToLower(h))
+		} else if len(h) > 5 && h[0] == '[' && h[len(h)-1] == ']' {
+			if ip := net.ParseIP(h[1 : len(h)-1]); ip != nil {
+				// this will save the normalized ip, as ip.String always returns ipv6 in short form
+				s.hosts.table["["+ip.String()+"]"] = true
+			}
 		} else {
 			s.hosts.table[strings.ToLower(h)] = true
 		}
@@ -319,6 +325,11 @@ func (s *server) allowsHost(host string) bool {
 	return false
 }
 
+func (s *server) allowsIp(ip net.IP) bool {
+	ipStr := ip.String()
+	return s.allowsHost("[" + ipStr + "]")
+}
+
 const commandSuffix = "\r\n"
 
 // Reads from the client until a \n terminator is encountered,
@@ -432,12 +443,26 @@ func (s *server) handleClient(client *client) {
 			cmd := bytes.ToUpper(input[:cmdLen])
 			switch {
 			case cmdHELO.match(cmd):
-				client.Helo = string(bytes.Trim(input[4:], " "))
+				if h, err := client.parser.Helo(input[4:]); err == nil {
+					client.Helo = h
+				} else {
+					s.log().WithFields(logrus.Fields{"helo": h, "client": client.ID}).Warn("invalid helo")
+					client.sendResponse(r.FailSyntaxError)
+					break
+				}
 				client.resetTransaction()
 				client.sendResponse(helo)
 
 			case cmdEHLO.match(cmd):
-				client.Helo = string(bytes.Trim(input[4:], " "))
+				if h, _, err := client.parser.Ehlo(input[4:]); err == nil {
+					client.Helo = h
+				} else {
+					client.sendResponse(r.FailSyntaxError)
+					s.log().WithFields(logrus.Fields{"ehlo": h, "client": client.ID}).Warn("invalid ehlo")
+					client.sendResponse(r.FailSyntaxError)
+					break
+				}
+				client.ESMTP = true
 				client.resetTransaction()
 				client.sendResponse(ehlo,
 					messageSize,
@@ -506,7 +531,8 @@ func (s *server) handleClient(client *client) {
 					client.sendResponse(err.Error())
 					break
 				}
-				if !s.allowsHost(to.Host) {
+				s.defaultHost(&to)
+				if (to.IP != nil && !s.allowsIp(to.IP)) || (to.IP == nil && !s.allowsHost(to.Host)) {
 					client.sendResponse(r.ErrorRelayDenied, " ", to.Host)
 				} else {
 					client.PushRcpt(to)
@@ -679,3 +705,16 @@ func (s *server) loadLog(value *atomic.Value) log.Logger {
 	}
 	return l
 }
+
+// defaultHost ensures that the host attribute is set, if addressed to Postmaster
+func (s *server) defaultHost(a *mail.Address) {
+	if a.Host == "" && a.IsPostmaster() {
+		sc := s.configStore.Load().(ServerConfig)
+		a.Host = sc.Hostname
+		if !s.allowsHost(a.Host) {
+			s.log().WithFields(
+				logrus.Fields{"hostname": sc.Hostname}).
+				Warn("the hostname is not present in AllowedHosts config setting")
+		}
+	}
+}

+ 561 - 1
server_test.go

@@ -222,7 +222,7 @@ func TestTLSConfig(t *testing.T) {
 			Protocols:      []string{"tls1.0", "tls1.2"},
 		},
 	})
-	if err := s.configureSSL(); err != nil {
+	if err := s.configureTLS(); err != nil {
 		t.Error(err)
 	}
 
@@ -302,6 +302,550 @@ func TestHandleClient(t *testing.T) {
 	wg.Wait() // wait for handleClient to exit
 }
 
+func TestGithubIssue197(t *testing.T) {
+	var mainlog log.Logger
+	var logOpenError error
+	defer cleanTestArtifacts(t)
+	sc := getMockServerConfig()
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	conn, server := getMockServerConn(sc, t)
+	server.backend().Start()
+	// we assume that 1.1.1.1 is a domain (ip-literal syntax is incorrect)
+	// [2001:DB8::FF00:42:8329] is an address literal
+	server.setAllowedHosts([]string{"1.1.1.1", "[2001:DB8::FF00:42:8329]"})
+
+	client := NewClient(conn.Server, 1, mainlog, mail.NewPool(5))
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		server.handleClient(client)
+		wg.Done()
+	}()
+	// Wait for the greeting from the server
+	r := textproto.NewReader(bufio.NewReader(conn.Client))
+	line, _ := r.ReadLine()
+	//	fmt.Println(line)
+	w := textproto.NewWriter(bufio.NewWriter(conn.Client))
+	if err := w.PrintfLine("HELO test.test.com"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// Case 1
+	if err := w.PrintfLine("rcpt to: <hi@[1.1.1.1]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if client.parser.IP == nil {
+		t.Error("[1.1.1.1] not parsed as address-liteal")
+	}
+
+	// case 2, should be parsed as domain
+	if err := w.PrintfLine("rcpt to: <[email protected]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if client.parser.IP != nil {
+		t.Error("1.1.1.1 should not be parsed as an IP (syntax requires IP addresses to be in braces, eg <hi@[1.1.1.1]>")
+	}
+
+	// case 3
+	// prefix ipv6 is case insensitive
+	if err := w.PrintfLine("rcpt to: <hi@[ipv6:2001:DB8::FF00:42:8329]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if client.parser.IP == nil {
+		t.Error("[ipv6:2001:DB8::FF00:42:8329] should be parsed as an address-literal, it wasnt")
+	}
+
+	// case 4
+	if err := w.PrintfLine("rcpt to: <hi@[IPv6:2001:0db8:0000:0000:0000:ff00:0042:8329]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if client.parser.Domain != "2001:DB8::FF00:42:8329" && client.parser.IP == nil {
+		t.Error("[IPv6:2001:0db8:0000:0000:0000:ff00:0042:8329] is same as 2001:DB8::FF00:42:8329, lol")
+	}
+
+	if err := w.PrintfLine("QUIT"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	//fmt.Println("line is:", line)
+	expected := "221 2.0.0 Bye"
+	if strings.Index(line, expected) != 0 {
+		t.Error("expected", expected, "but got:", line)
+	}
+	wg.Wait() // wait for handleClient to exit
+}
+
+var githubIssue198data string
+
+var customBackend = func() backends.Decorator {
+	return func(p backends.Processor) backends.Processor {
+		return backends.ProcessWith(
+			func(e *mail.Envelope, task backends.SelectTask) (backends.Result, error) {
+				if task == backends.TaskSaveMail {
+					githubIssue198data = e.DeliveryHeader + e.Data.String()
+				}
+				return p.Process(e, task)
+			})
+	}
+}
+
+// TestGithubIssue198 is an interesting test because it shows how to do an integration test for
+// a backend using a custom backend.
+func TestGithubIssue198(t *testing.T) {
+	var mainlog log.Logger
+	var logOpenError error
+	defer cleanTestArtifacts(t)
+	sc := getMockServerConfig()
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
+
+	backends.Svc.AddProcessor("custom", customBackend)
+
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	conn, server := getMockServerConn(sc, t)
+	be, err := backends.New(map[string]interface{}{
+		"save_process": "HeadersParser|Header|custom", "primary_mail_host": "example.com"},
+		mainlog)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	server.setBackend(be)
+	if err := server.backend().Start(); err != nil {
+		t.Error(err)
+		return
+	}
+
+	server.setAllowedHosts([]string{"1.1.1.1", "[2001:DB8::FF00:42:8329]"})
+
+	client := NewClient(conn.Server, 1, mainlog, mail.NewPool(5))
+	client.RemoteIP = "127.0.0.1"
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		server.handleClient(client)
+		wg.Done()
+	}()
+	// Wait for the greeting from the server
+	r := textproto.NewReader(bufio.NewReader(conn.Client))
+	line, _ := r.ReadLine()
+
+	w := textproto.NewWriter(bufio.NewWriter(conn.Client))
+	// Test with HELO greeting
+	line = sendMessage("HELO", true, w, t, line, r, err, client)
+	if !strings.Contains(githubIssue198data, " SMTPS ") {
+		t.Error("'with SMTPS' not present")
+	}
+
+	if !strings.Contains(githubIssue198data, "from 127.0.0.1") {
+		t.Error("'from 127.0.0.1' not present")
+	}
+
+	/////////////////////
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	// Test with EHLO
+	line, _ = r.ReadLine()
+	line = sendMessage("EHLO", true, w, t, line, r, err, client)
+	if !strings.Contains(githubIssue198data, " ESMTPS ") {
+		t.Error("'with ESMTPS' not present")
+	}
+	/////////////////////
+
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// Test with EHLO & no TLS
+
+	line = sendMessage("EHLO", false, w, t, line, r, err, client)
+
+	/////////////////////
+
+	if !strings.Contains(githubIssue198data, " ESMTP ") {
+		t.Error("'with ESTMP' not present")
+	}
+
+	if err := w.PrintfLine("QUIT"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	expected := "221 2.0.0 Bye"
+	if strings.Index(line, expected) != 0 {
+		t.Error("expected", expected, "but got:", line)
+	}
+	wg.Wait() // wait for handleClient to exit
+}
+
+func sendMessage(greet string, TLS bool, w *textproto.Writer, t *testing.T, line string, r *textproto.Reader, err error, client *client) string {
+	if err := w.PrintfLine(greet + " test.test.com"); err != nil {
+		t.Error(err)
+	}
+	for {
+		line, _ = r.ReadLine()
+		if strings.Index(line, "250 ") == 0 {
+			break
+		}
+		if strings.Index(line, "250") != 0 {
+			t.Error(err)
+		}
+	}
+
+	if err := w.PrintfLine("MAIL FROM: [email protected]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if err := w.PrintfLine("RCPT TO: <hi@[ipv6:2001:DB8::FF00:42:8329]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	client.Hashes = append(client.Hashes, "abcdef1526777763")
+	client.TLS = TLS
+	if err := w.PrintfLine("DATA"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if err := w.PrintfLine("Subject: Test subject\r\n\r\nHello Sir,\nThis is a test.\r\n."); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	return line
+}
+
+func TestGithubIssue199(t *testing.T) {
+	var mainlog log.Logger
+	var logOpenError error
+	defer cleanTestArtifacts(t)
+	sc := getMockServerConfig()
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	conn, server := getMockServerConn(sc, t)
+	server.backend().Start()
+
+	server.setAllowedHosts([]string{"grr.la", "fake.com", "[1.1.1.1]", "[2001:db8::8a2e:370:7334]", "saggydimes.test.com"})
+
+	client := NewClient(conn.Server, 1, mainlog, mail.NewPool(5))
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		server.handleClient(client)
+		wg.Done()
+	}()
+	// Wait for the greeting from the server
+	r := textproto.NewReader(bufio.NewReader(conn.Client))
+	line, _ := r.ReadLine()
+	//	fmt.Println(line)
+	w := textproto.NewWriter(bufio.NewWriter(conn.Client))
+	if err := w.PrintfLine("HELO test"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// case 1
+	if err := w.PrintfLine(
+		"MAIL FROM: <\"  yo-- man wazz'''up? surprise surprise, this is [email protected] \"@example.com>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	// [SPACE][SPACE]yo--[SPACE]man[SPACE]wazz'''up?[SPACE]surprise[SPACE]surprise,[SPACE]this[SPACE]is[SPACE][email protected][SPACE]
+	if client.parser.LocalPart != "  yo-- man wazz'''up? surprise surprise, this is [email protected] " {
+		t.Error("expecting local part: [  yo-- man wazz'''up? surprise surprise, this is [email protected] ], got client.parser.LocalPart")
+	}
+	if !client.parser.LocalPartQuotes {
+		t.Error("was expecting client.parser.LocalPartQuotes true, got false")
+	}
+	// from should just as above but without angle brackets <>
+	if from := client.MailFrom.String(); from != "\"  yo-- man wazz'''up? surprise surprise, this is [email protected] \"@example.com" {
+		t.Error("mail from was:", from)
+	}
+	if line != "250 2.1.0 OK" {
+		t.Error("line did not have: 250 2.1.0 OK, got", line)
+	}
+
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// case 2, address literal mailboxes
+	if err := w.PrintfLine("MAIL FROM: <hi@[1.1.1.1]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// stringer should be aware its an ip and return the host part in angle brackets
+	if from := client.MailFrom.String(); from != "hi@[1.1.1.1]" {
+		t.Error("mail from was:", from)
+	}
+
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// case 3
+
+	if err := w.PrintfLine("MAIL FROM: <hi@[IPv6:2001:0db8:0000:0000:0000:8a2e:0370:7334]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// stringer should be aware its an ip and return the host part in angle brackets, and ipv6 should be normalized
+	if from := client.MailFrom.String(); from != "hi@[2001:db8::8a2e:370:7334]" {
+		t.Error("mail from was:", from)
+	}
+
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// case 4
+	// rcpt to: <hi@[IPv6:2001:0db8:0000:0000:0000:ff00:0042:8329]>
+
+	if err := w.PrintfLine("MAIL FROM: <>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if err := w.PrintfLine("RCPT TO: <Postmaster>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// stringer should return an empty string
+	if from := client.MailFrom.String(); from != "" {
+		t.Error("mail from was:", from)
+	}
+
+	// note here the saggydimes.test.com was added because no host was specified in the RCPT TO command
+	if rcpt := client.RcptTo[0].String(); rcpt != "[email protected]" {
+		t.Error("mail from was:", rcpt)
+	}
+
+	// additional cases
+
+	/*
+		user part:
+		" al\ph\a "@grr.la should be " alpha "@grr.la.
+		"alpha"@grr.la should be [email protected].
+		"alp\h\a"@grr.la should be [email protected].
+	*/
+
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if err := w.PrintfLine("RCPT TO: <\" al\\ph\\a \"@grr.la>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if client.RcptTo[0].User != " alpha " {
+		t.Error(client.RcptTo[0].User)
+	}
+
+	// the unnecessary \\ should be removed
+	if rcpt := client.RcptTo[0].String(); rcpt != "\" alpha \"@grr.la" {
+		t.Error(rcpt)
+	}
+
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if err := w.PrintfLine("RCPT TO: <\"alpha\"@grr.la>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// we don't need to quote, so stringer should return without the quotes
+	if rcpt := client.RcptTo[0].String(); rcpt != "[email protected]" {
+		t.Error(rcpt)
+	}
+
+	if err := w.PrintfLine("RSET"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	if err := w.PrintfLine("RCPT TO: <\"a\\l\\pha\"@grr.la>"); err != nil {
+		t.Error(err)
+	}
+
+	line, _ = r.ReadLine()
+
+	// we don't need to quote, so stringer should return without the quotes
+	if rcpt := client.RcptTo[0].String(); rcpt != "[email protected]" {
+		t.Error(rcpt)
+	}
+
+	if err := w.PrintfLine("QUIT"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	wg.Wait() // wait for handleClient to exit
+}
+
+func TestGithubIssue200(t *testing.T) {
+	var mainlog log.Logger
+	var logOpenError error
+	defer cleanTestArtifacts(t)
+	sc := getMockServerConfig()
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	conn, server := getMockServerConn(sc, t)
+	server.backend().Start()
+	server.setAllowedHosts([]string{"1.1.1.1", "[2001:DB8::FF00:42:8329]"})
+
+	client := NewClient(conn.Server, 1, mainlog, mail.NewPool(5))
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		server.handleClient(client)
+		wg.Done()
+	}()
+	// Wait for the greeting from the server
+	r := textproto.NewReader(bufio.NewReader(conn.Client))
+	line, _ := r.ReadLine()
+	//	fmt.Println(line)
+	w := textproto.NewWriter(bufio.NewWriter(conn.Client))
+	if err := w.PrintfLine("HELO test\"><script>alert('hi')</script>test.com"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if line != "550 5.5.2 Syntax error" {
+		t.Error("line expected to be: 550 5.5.2 Syntax error, got", line)
+	}
+
+	if err := w.PrintfLine("HELO test.com"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if !strings.Contains(line, "250") {
+		t.Error("line did not have 250 code, got", line)
+	}
+	if err := w.PrintfLine("QUIT"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	//fmt.Println("line is:", line)
+	expected := "221 2.0.0 Bye"
+	if strings.Index(line, expected) != 0 {
+		t.Error("expected", expected, "but got:", line)
+	}
+	wg.Wait() // wait for handleClient to exit
+}
+
+func TestGithubIssue201(t *testing.T) {
+	var mainlog log.Logger
+	var logOpenError error
+	defer cleanTestArtifacts(t)
+	sc := getMockServerConfig()
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	conn, server := getMockServerConn(sc, t)
+	server.backend().Start()
+	// note that saggydimes.test.com is the hostname of the server, it comes form the config
+	// it will be used for rcpt to:<postmaster> which does not specify a host
+	server.setAllowedHosts([]string{"a.com", "saggydimes.test.com"})
+
+	client := NewClient(conn.Server, 1, mainlog, mail.NewPool(5))
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		server.handleClient(client)
+		wg.Done()
+	}()
+	// Wait for the greeting from the server
+	r := textproto.NewReader(bufio.NewReader(conn.Client))
+	line, _ := r.ReadLine()
+	//	fmt.Println(line)
+	w := textproto.NewWriter(bufio.NewWriter(conn.Client))
+	if err := w.PrintfLine("HELO test"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+
+	// case 1
+	if err := w.PrintfLine("RCPT TO: <[email protected]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if line != "250 2.1.5 OK" {
+		t.Error("line did not have: 250 2.1.5 OK, got", line)
+	}
+	// case 2
+	if err := w.PrintfLine("RCPT TO: <[email protected]>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if line != "454 4.1.1 Error: Relay access denied: not-a.com" {
+		t.Error("line is not:454 4.1.1 Error: Relay access denied: not-a.com, got", line)
+	}
+	// case 3 (no host specified)
+
+	if err := w.PrintfLine("RCPT TO: <poSTmAsteR>"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if line != "250 2.1.5 OK" {
+		t.Error("line is not:[250 2.1.5 OK], got", line)
+	}
+
+	// case 4
+	if err := w.PrintfLine("RCPT TO: <\"po\\ST\\mAs\\t\\eR\">"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	if line != "250 2.1.5 OK" {
+		t.Error("line is not:[250 2.1.5 OK], got", line)
+	}
+	// the local part should be just "postmaster" (normalized)
+	if client.parser.LocalPart != "postmaster" {
+		t.Error("client.parser.LocalPart was not postmaster, got:", client.parser.LocalPart)
+	}
+
+	if client.parser.LocalPartQuotes {
+		t.Error("client.parser.LocalPartQuotes was true, expecting false")
+	}
+
+	if err := w.PrintfLine("QUIT"); err != nil {
+		t.Error(err)
+	}
+	line, _ = r.ReadLine()
+	//fmt.Println("line is:", line)
+	expected := "221 2.0.0 Bye"
+	if strings.Index(line, expected) != 0 {
+		t.Error("expected", expected, "but got:", line)
+	}
+	wg.Wait() // wait for handleClient to exit
+}
+
 func TestXClient(t *testing.T) {
 	var mainlog log.Logger
 	var logOpenError error
@@ -552,6 +1096,9 @@ func TestAllowsHosts(t *testing.T) {
 		"*.test",
 		"wild*.card",
 		"multiple*wild*cards.*",
+		"[::FFFF:C0A8:1]",          // ip4 in ipv6 format. It's actually 192.168.0.1
+		"[2001:db8::ff00:42:8329]", // same as 2001:0db8:0000:0000:0000:ff00:0042:8329
+		"[127.0.0.1]",
 	}
 	s.setAllowedHosts(allowedHosts)
 
@@ -572,6 +1119,19 @@ func TestAllowsHosts(t *testing.T) {
 		}
 	}
 
+	testTableIP := map[string]bool{
+
+		"192.168.0.1": true,
+		"2001:0db8:0000:0000:0000:ff00:0042:8329": true,
+		"127.0.0.1": true,
+	}
+
+	for host, allows := range testTableIP {
+		if res := s.allowsIp(net.ParseIP(host)); res != allows {
+			t.Error(host, ": expected", allows, "but got", res)
+		}
+	}
+
 	// only wildcard - should match anything
 	s.setAllowedHosts([]string{"*"})
 	if !s.allowsHost("match.me") {

+ 15 - 0
tls_go1.13.go

@@ -0,0 +1,15 @@
+// +build go1.13
+
+package guerrilla
+
+import "crypto/tls"
+
+// TLS 1.3 was introduced in go 1.12 as an option and enabled for production in go 1.13
+// release notes: https://golang.org/doc/go1.12#tls_1_3
+func init() {
+	TLSProtocols["tls1.3"] = tls.VersionTLS13
+
+	TLSCiphers["TLS_AES_128_GCM_SHA256"] = tls.TLS_AES_128_GCM_SHA256
+	TLSCiphers["TLS_AES_256_GCM_SHA384"] = tls.TLS_AES_256_GCM_SHA384
+	TLSCiphers["TLS_CHACHA20_POLY1305_SHA256"] = tls.TLS_CHACHA20_POLY1305_SHA256
+}

+ 13 - 0
tls_go1.14.go

@@ -0,0 +1,13 @@
+// +build !go1.14
+
+package guerrilla
+
+import "crypto/tls"
+
+func init() {
+
+	TLSProtocols["ssl3.0"] = tls.VersionSSL30 // deprecated since GO 1.13, removed 1.14
+
+	// Include to prevent downgrade attacks (SSLv3 only, deprecated in Go 1.13)
+	TLSCiphers["TLS_FALLBACK_SCSV"] = tls.TLS_FALLBACK_SCSV
+}