Browse Source

Merge branch 'master' into stream

flashmob 5 years ago
parent
commit
9a24d7a2b7

+ 1 - 2
.travis.yml

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

+ 1 - 1
README.md

@@ -270,7 +270,7 @@ Using Nginx as a proxy
 
 
 For such purposes as load balancing, terminating TLS early,
 For such purposes as load balancing, terminating TLS early,
  or supporting SSL versions not supported by Go (highly not recommended if you
  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).
  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,
 					e.Subject,
 					ts)
 					ts)
 				e.QueuedId = hash
 				e.QueuedId = hash
+
 				// Add extra headers
 				// Add extra headers
+				protocol := "SMTP"
+				if e.ESMTP {
+					protocol = "E" + protocol
+				}
+				if e.TLS {
+					protocol = protocol + "S"
+				}
 				var addHead string
 				var addHead string
 				addHead += "Delivered-To: " + to + "\r\n"
 				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"
 				addHead += "	" + time.Now().Format(time.RFC1123Z) + "\r\n"
 
 
 				// data will be compressed when printed, with addHead added to beginning
 				// 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 {
 				if len(e.Hashes) > 0 {
 					hash = e.Hashes[0]
 					hash = e.Hashes[0]
 				}
 				}
+				protocol := "SMTP"
+				if e.ESMTP {
+					protocol = "E" + protocol
+				}
+				if e.TLS {
+					protocol = protocol + "S"
+				}
 				var addHead string
 				var addHead string
 				addHead += "Delivered-To: " + to + "\n"
 				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 {
 				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"
 				addHead += "	" + time.Now().Format(time.RFC1123Z) + "\n"
 				// save the result
 				// save the result

+ 1 - 1
backends/p_redis.go

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

+ 2 - 0
client.go

@@ -229,6 +229,8 @@ func (c *client) parsePath(in []byte, p pathParser) (mail.Address, error) {
 			ADL:        c.parser.ADL,
 			ADL:        c.parser.ADL,
 			PathParams: c.parser.PathParams,
 			PathParams: c.parser.PathParams,
 			NullPath:   c.parser.NullPath,
 			NullPath:   c.parser.NullPath,
+			Quoted:     c.parser.LocalPartQuotes,
+			IP:         c.parser.IP,
 		}
 		}
 	}
 	}
 	return address, err
 	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 {
 	if conn, buffin, err := test.Connect(newServer, 20); err != nil {
 		t.Error("Could not connect to new server", newServer.ListenInterface, err)
 		t.Error("Could not connect to new server", newServer.ListenInterface, err)
 	} else {
 	} 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"
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				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 {
 	if conn, buffin, err := test.Connect(newConf.Servers[1], 20); err != nil {
 		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
 		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
 	} else {
 	} 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"
 			expect := "250 enable.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				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 {
 	if conn, buffin, err := test.Connect(newConf.Servers[1], 20); err != nil {
 		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
 		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
 	} else {
 	} 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"
 			expect := "250 enable.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				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 {
 	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)
 		t.Error("Could not connect to new server", conf.Servers[1].ListenInterface, err)
 	} else {
 	} 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"
 			expect := "250 secure.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				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 {
 	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)
 		t.Error("Could not connect to new server", conf.Servers[1].ListenInterface, err)
 	} else {
 	} 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"
 			expect := "250 secure.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				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 {
 		if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 			t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 			t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 		} else {
 		} 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"
 				expect := "250 mail.test.com Hello"
 				if strings.Index(result, expect) != 0 {
 				if strings.Index(result, expect) != 0 {
 					t.Error("Expected", expect, "but got", result)
 					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 {
 	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} 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"
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				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 {
 	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} 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"
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				t.Error("Expected", expect, "but got", result)
@@ -1373,7 +1373,7 @@ func TestSetTimeoutEvent(t *testing.T) {
 	} else {
 	} else {
 		waitTimeout.Add(1)
 		waitTimeout.Add(1)
 		go func() {
 		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"
 				expect := "250 mail.test.com Hello"
 				if strings.Index(result, expect) != 0 {
 				if strings.Index(result, expect) != 0 {
 					t.Error("Expected", expect, "but got", result)
 					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 {
 	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} 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"
 			expect := "250 mail.test.com Hello"
 			if strings.Index(result, expect) != 0 {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 				t.Error("Expected", expect, "but got", result)

+ 11 - 6
config.go

@@ -42,6 +42,8 @@ type ServerConfig struct {
 	LogFile string `json:"log_file,omitempty"`
 	LogFile string `json:"log_file,omitempty"`
 	// Hostname will be used in the server's reply to HELO/EHLO. If TLS enabled
 	// 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()
 	// 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"`
 	Hostname string `json:"host_name"`
 	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	ListenInterface string `json:"listen_interface"`
 	ListenInterface string `json:"listen_interface"`
@@ -96,9 +98,13 @@ type ServerTLSConfig struct {
 // https://golang.org/pkg/crypto/tls/#pkg-constants
 // https://golang.org/pkg/crypto/tls/#pkg-constants
 // Ciphers introduced before Go 1.7 are listed here,
 // Ciphers introduced before Go 1.7 are listed here,
 // ciphers since Go 1.8, see tls_go1.8.go
 // ciphers since Go 1.8, see tls_go1.8.go
+// ....... since Go 1.13, see tls_go1.13.go
 var TLSCiphers = map[string]uint16{
 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_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_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,
 	"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_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,
 	"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
 // https://golang.org/pkg/crypto/tls/#pkg-constants
 var TLSProtocols = map[string]uint16{
 var TLSProtocols = map[string]uint16{
-	"ssl3.0": tls.VersionSSL30,
 	"tls1.0": tls.VersionTLS10,
 	"tls1.0": tls.VersionTLS10,
 	"tls1.1": tls.VersionTLS11,
 	"tls1.1": tls.VersionTLS11,
 	"tls1.2": tls.VersionTLS12,
 	"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++ {
 	for i := 0; i < len(c.Servers); i++ {
 		if err := c.Servers[i].loadTlsKeyTimestamps(); err != nil {
 		if err := c.Servers[i].loadTlsKeyTimestamps(); err != nil {
 			return err
 			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 {
 func (sc *ServerConfig) loadTlsKeyTimestamps() error {
 	var statErr = func(iface string, err error) error {
 	var statErr = func(iface string, err error) error {
 		return fmt.Errorf(
 		return fmt.Errorf(

+ 4 - 4
goguerrilla.conf.sample

@@ -31,8 +31,8 @@
                 "tls_always_on":false,
                 "tls_always_on":false,
                 "private_key_file":"/path/to/pem/file/test.com.key",
                 "private_key_file":"/path/to/pem/file/test.com.key",
                 "public_key_file":"/path/to/pem/file/test.com.crt",
                 "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"],
                 "curves" : ["P256", "P384", "P521", "X25519"],
                 "client_auth_type" : "NoClientCert"
                 "client_auth_type" : "NoClientCert"
             }
             }
@@ -50,8 +50,8 @@
                 "public_key_file":"/path/to/pem/file/test.com.crt",
                 "public_key_file":"/path/to/pem/file/test.com.crt",
                  "start_tls_on":false,
                  "start_tls_on":false,
                  "tls_always_on":true,
                  "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"],
                  "curves" : ["P256", "P384", "P521", "X25519"],
                  "client_auth_type" : "NoClientCert"
                  "client_auth_type" : "NoClientCert"
             }
             }

+ 1 - 1
guerrilla.go

@@ -320,7 +320,7 @@ func (g *guerrilla) subscribeEvents() {
 	// TLS changes
 	// TLS changes
 	events[EventConfigServerTLSConfig] = serverEvent(func(sc *ServerConfig) {
 	events[EventConfigServerTLSConfig] = serverEvent(func(sc *ServerConfig) {
 		if server, err := g.findServer(sc.ListenInterface); err == nil {
 		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)
 				g.mainlog().Infof("Server [%s] new TLS configuration loaded", sc.ListenInterface)
 			} else {
 			} else {
 				g.mainlog().WithError(err).Errorf("Server [%s] failed to load the new TLS configuration", sc.ListenInterface)
 				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]>")
 	str = mail.MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
 	if strings.Index(str, "André Pirard") != 0 {
 	if strings.Index(str, "André Pirard") != 0 {
 		t.Error("expecting André Pirard, got:", str)
 		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"
 	"crypto/md5"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"io"
 	"io"
 	"mime"
 	"mime"
-	"net/mail"
+	"net"
 	"net/textproto"
 	"net/textproto"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
+
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 )
 )
 
 
 // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
 // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
@@ -39,37 +40,83 @@ type Address struct {
 	PathParams []smtp.PathParam
 	PathParams []smtp.PathParam
 	// NullPath is true if <> was received
 	// NullPath is true if <> was received
 	NullPath bool
 	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 ap = mail.AddressParser{}
 var apLock sync.Mutex // guards mail.AddressParser
 var apLock sync.Mutex // guards mail.AddressParser
 
 
 // NewAddress takes a string of an RFC 5322 address of the
 // NewAddress takes a string of an RFC 5322 address of the
 // form "Gogh Fir <[email protected]>" or "[email protected]".
 // form "Gogh Fir <[email protected]>" or "[email protected]".
-func NewAddress(str string) (Address, error) {
+func NewAddress(str string) (*Address, error) {
 	apLock.Lock()
 	apLock.Lock()
 	defer apLock.Unlock()
 	defer apLock.Unlock()
-	a, err := ap.Parse(str)
+	l, err := ap.Address([]byte(str))
 	if err != nil {
 	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.
 // Envelope of Email represents a single SMTP message.
@@ -100,6 +147,8 @@ type Envelope struct {
 	QueuedId string
 	QueuedId string
 	// TransportType indicates whenever 8BITMIME extension has been signaled
 	// TransportType indicates whenever 8BITMIME extension has been signaled
 	TransportType smtp.TransportType
 	TransportType smtp.TransportType
+	// ESMTP: true if EHLO was used
+	ESMTP bool
 	// When locked, it means that the envelope is being processed by the backend
 	// When locked, it means that the envelope is being processed by the backend
 	sync.Mutex
 	sync.Mutex
 }
 }
@@ -180,6 +229,7 @@ func (e *Envelope) Reseed(remoteIP string, clientID uint64) {
 	e.QueuedId = queuedID(clientID)
 	e.QueuedId = queuedID(clientID)
 	e.Helo = ""
 	e.Helo = ""
 	e.TLS = false
 	e.TLS = false
+	e.ESMTP = false
 }
 }
 
 
 // PushRcpt adds a recipient email address to the envelope
 // PushRcpt adds a recipient email address to the envelope
@@ -194,57 +244,166 @@ func (e *Envelope) PopRcpt() Address {
 	return ret
 	return ret
 }
 }
 
 
+const (
+	statePlainText = iota
+	stateStartEncodedWord
+	stateEncodedWord
+	stateEncoding
+	stateCharset
+	statePayload
+	statePayloadEnd
+)
+
 // MimeHeaderDecode converts 7 bit encoded mime header strings to UTF-8
 // MimeHeaderDecode converts 7 bit encoded mime header strings to UTF-8
 func MimeHeaderDecode(str string) string {
 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++ {
 	for i := 0; i < len(str); i++ {
 		switch state {
 		switch state {
-		case 0:
+		case statePlainText:
+			if ptextStart == -1 {
+				ptextStart = i
+			}
 			if str[i] == '=' {
 			if str[i] == '=' {
-				buf.WriteByte(str[i])
-				state = 1
+				state = stateStartEncodedWord
+				wordStart = i
+				wordLen = 1
 			} else {
 			} else {
-				out = append(out, str[i])
+				ptextLen++
 			}
 			}
-		case 1:
+		case stateStartEncodedWord:
 			if str[i] == '?' {
 			if str[i] == '?' {
-				buf.WriteByte(str[i])
-				state = 2
+				wordLen++
+				state = stateCharset
 			} else {
 			} 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 {
 				} 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 {
 			} 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)
 	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
 // Envelopes have their own pool
 
 
 type Pool struct {
 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?=")
 	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]>")
 	str = MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
 	if strings.Index(str, "André Pirard") != 0 {
 	if strings.Index(str, "André Pirard") != 0 {
 		t.Error("expecting André Pirard, got:", str)
 		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) {
 func TestNewAddress(t *testing.T) {
 
 
 	addr, err := NewAddress("<hoop>")
 	addr, err := NewAddress("<hoop>")
 	if err == nil {
 	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]>`)
 	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)
 		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) {
 func TestEnvelope(t *testing.T) {
 	e := NewEnvelope("127.0.0.1", 22)
 	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
 	// The minimum total number of recipients that must be buffered is 100
 	LimitRecipients = 100
 	LimitRecipients = 100
 )
 )
-
 type PathParam []string
 type PathParam []string
 
 
 type TransportType int
 type TransportType int
@@ -50,11 +49,16 @@ func (p PathParam) Transport() TransportType {
 	return TransportTypeInvalid
 	return TransportTypeInvalid
 }
 }
 
 
+var atExpected = errors.New("@ expected as part of mailbox")
+
+
 // Parse Email Addresses according to https://tools.ietf.org/html/rfc5321
 // Parse Email Addresses according to https://tools.ietf.org/html/rfc5321
 type Parser struct {
 type Parser struct {
 	NullPath  bool
 	NullPath  bool
 	LocalPart string
 	LocalPart string
+	LocalPartQuotes bool   // does the local part need quotes?
 	Domain    string
 	Domain    string
+	IP              net.IP
 
 
 	ADL        []string
 	ADL        []string
 	PathParams []PathParam
 	PathParams []PathParam
@@ -83,6 +87,8 @@ func (s *Parser) Reset() {
 		s.LocalPart = ""
 		s.LocalPart = ""
 		s.Domain = ""
 		s.Domain = ""
 		s.accept.Reset()
 		s.accept.Reset()
+		s.LocalPartQuotes = false
+		s.IP = nil
 	}
 	}
 }
 }
 
 
@@ -125,14 +131,15 @@ func (s *Parser) forwardPath() (err error) {
 	if s.peek() == ' ' {
 	if s.peek() == ' ' {
 		s.next() // tolerate a space at the front
 		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 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
 //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
 	return nil
 }
 }
 
 
-const postmasterPath = "<postmaster>"
-const postmasterLocalPart = "Postmaster"
+const postmasterLocalPart = "postmaster"
 
 
 //RcptTo accepts the following syntax: ( "<Postmaster@" Domain ">" / "<Postmaster>" /
 //RcptTo accepts the following syntax: ( "<Postmaster@" Domain ">" / "<Postmaster>" /
 //                  Forward-path ) [SP Rcpt-parameters] CRLF
 //                  Forward-path ) [SP Rcpt-parameters] CRLF
@@ -340,7 +346,7 @@ func (s *Parser) subdomain() error {
 				state = 1
 				state = 1
 				continue
 				continue
 			}
 			}
-			return errors.New("parse err")
+			return errors.New("subdomain parse err")
 		case 1:
 		case 1:
 			p := s.peek()
 			p := s.peek()
 			if isLetDig(c) || c == '-' {
 			if isLetDig(c) || c == '-' {
@@ -348,7 +354,7 @@ func (s *Parser) subdomain() error {
 			}
 			}
 			if !isLetDig(p) && p != '-' {
 			if !isLetDig(p) && p != '-' {
 				if c == '-' {
 				if c == '-' {
-					return errors.New("parse err")
+					return errors.New("subdomain parse err")
 				}
 				}
 				return nil
 				return nil
 			}
 			}
@@ -369,7 +375,7 @@ func (s *Parser) mailbox() error {
 		return err
 		return err
 	}
 	}
 	if s.ch != '@' {
 	if s.ch != '@' {
-		return errors.New("@ expected as part of mailbox")
+		return atExpected
 	}
 	}
 	if p := s.peek(); p == '[' {
 	if p := s.peek(); p == '[' {
 		return s.addressLiteral()
 		return s.addressLiteral()
@@ -416,6 +422,11 @@ func (s *Parser) ipv4AddressLiteral() error {
 		}
 		}
 		s.accept.WriteByte(s.ch)
 		s.accept.WriteByte(s.ch)
 	}
 	}
+	ip := net.ParseIP(s.accept.String())
+	if ip == nil {
+		return errors.New("invalid ip")
+	}
+	s.IP = ip
 	return nil
 	return nil
 }
 }
 
 
@@ -429,7 +440,7 @@ func (s *Parser) snum() error {
 		c := s.next()
 		c := s.next()
 		if state == 0 {
 		if state == 0 {
 			if !(c >= 48 && c <= 57) {
 			if !(c >= 48 && c <= 57) {
-				return errors.New("parse error")
+				return errors.New("snum parse error")
 			} else {
 			} else {
 				num.WriteByte(s.ch)
 				num.WriteByte(s.ch)
 				s.accept.WriteByte(s.ch)
 				s.accept.WriteByte(s.ch)
@@ -465,7 +476,8 @@ func (s *Parser) ipv6AddressLiteral() error {
 			c != ':' && c != '.' {
 			c != ':' && c != '.' {
 			ipstr := ip.String()
 			ipstr := ip.String()
 			if v := net.ParseIP(ipstr); v != nil {
 			if v := net.ParseIP(ipstr); v != nil {
-				s.accept.WriteString(ipstr)
+				s.accept.WriteString(v.String())
+				s.IP = v
 				return nil
 				return nil
 			}
 			}
 			return errors.New("invalid ipv6")
 			return errors.New("invalid ipv6")
@@ -514,15 +526,19 @@ func (s *Parser) QcontentSMTP() error {
 	state := 0
 	state := 0
 	for {
 	for {
 		ch := s.next()
 		ch := s.next()
+
 		switch state {
 		switch state {
 		case 0:
 		case 0:
 			if ch == '\\' {
 			if ch == '\\' {
 				state = 1
 				state = 1
-				s.accept.WriteByte(ch)
+				//	s.accept.WriteByte(ch)
 				continue
 				continue
 			} else if ch == 32 || ch == 33 ||
 			} else if ch == 32 || ch == 33 ||
 				(ch >= 35 && ch <= 91) ||
 				(ch >= 35 && ch <= 91) ||
 				(ch >= 93 && ch <= 126) {
 				(ch >= 93 && ch <= 126) {
+				if s.LocalPartQuotes == false && !s.isAtext(ch) {
+					s.LocalPartQuotes = true
+				}
 				s.accept.WriteByte(ch)
 				s.accept.WriteByte(ch)
 				continue
 				continue
 			}
 			}
@@ -530,6 +546,9 @@ func (s *Parser) QcontentSMTP() error {
 		case 1:
 		case 1:
 			// escaped character state
 			// escaped character state
 			if ch >= 32 && ch <= 126 {
 			if ch >= 32 && ch <= 126 {
+				if s.LocalPartQuotes == false && !s.isAtext(ch) {
+					s.LocalPartQuotes = true
+				}
 				s.accept.WriteByte(ch)
 				s.accept.WriteByte(ch)
 				state = 0
 				state = 0
 				continue
 				continue
@@ -560,7 +579,7 @@ func (s *Parser) atom() error {
 	for {
 	for {
 		if state == 0 {
 		if state == 0 {
 			if !s.isAtext(s.next()) {
 			if !s.isAtext(s.next()) {
-				return errors.New("parse error")
+				return errors.New("atom parse error")
 			} else {
 			} else {
 				s.accept.WriteByte(s.ch)
 				s.accept.WriteByte(s.ch)
 				state = 1
 				state = 1
@@ -599,7 +618,8 @@ atext           =       ALPHA / DIGIT / ; Any character except controls,
 
 
 func (s *Parser) isAtext(c byte) bool {
 func (s *Parser) isAtext(c byte) bool {
 	if ('0' <= c && c <= '9') ||
 	if ('0' <= c && c <= '9') ||
-		('A' <= c && c <= 'z') ||
+		('a' <= c && c <= 'z') ||
+		('A' <= c && c <= 'Z') ||
 		c == '!' || c == '#' ||
 		c == '!' || c == '#' ||
 		c == '$' || c == '%' ||
 		c == '$' || c == '%' ||
 		c == '&' || c == '\'' ||
 		c == '&' || c == '\'' ||
@@ -617,8 +637,56 @@ func (s *Parser) isAtext(c byte) bool {
 
 
 func isLetDig(c byte) bool {
 func isLetDig(c byte) bool {
 	if ('0' <= c && c <= '9') ||
 	if ('0' <= c && c <= '9') ||
-		('A' <= c && c <= 'z') {
+		('A' <= c && c <= 'Z') ||
+		('a' <= c && c <= 'z') {
 		return true
 		return true
 	}
 	}
 	return false
 	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 {
 	if err != nil {
 		t.Error("error not expected ", err)
 		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"))
 	err = s.RcptTo([]byte("<[email protected]> NOTIFY=SUCCESS,FAILURE"))
@@ -60,7 +60,11 @@ func TestParseRcptTo(t *testing.T) {
 		t.Error("error not expected ", err)
 		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) {
 func TestParseForwardPath(t *testing.T) {
@@ -189,16 +193,16 @@ func TestParseReversePath(t *testing.T) {
 func TestParseIpv6Address(t *testing.T) {
 func TestParseIpv6Address(t *testing.T) {
 	s := NewParser([]byte("2001:0000:3238:DFE1:0063:0000:0000:FEFB"))
 	s := NewParser([]byte("2001:0000:3238:DFE1:0063:0000:0000:FEFB"))
 	err := s.ipv6AddressLiteral()
 	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 {
 	if err != nil {
 		t.Error("error not expected ", err)
 		t.Error("error not expected ", err)
 	}
 	}
 	s = NewParser([]byte("2001:3238:DFE1:6323:FEFB:2536:1.2.3.2"))
 	s = NewParser([]byte("2001:3238:DFE1:6323:FEFB:2536:1.2.3.2"))
 	err = s.ipv6AddressLiteral()
 	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 {
 	if err != nil {
 		t.Error("error not expected ", err)
 		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"))
 	s = NewParser([]byte("2001:0000:3238:DFE1:63:0000:0000:FEFB"))
 	err = s.ipv6AddressLiteral()
 	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 {
 	if err != nil {
 		t.Error("error not expected ", err)
 		t.Error("error not expected ", err)
@@ -215,8 +219,8 @@ func TestParseIpv6Address(t *testing.T) {
 
 
 	s = NewParser([]byte("2001:0000:3238:DFE1:63::FEFB"))
 	s = NewParser([]byte("2001:0000:3238:DFE1:63::FEFB"))
 	err = s.ipv6AddressLiteral()
 	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 {
 	if err != nil {
 		t.Error("error not expected ", err)
 		t.Error("error not expected ", err)
@@ -224,8 +228,8 @@ func TestParseIpv6Address(t *testing.T) {
 
 
 	s = NewParser([]byte("2001:0:3238:DFE1:63::FEFB"))
 	s = NewParser([]byte("2001:0:3238:DFE1:63::FEFB"))
 	err = s.ipv6AddressLiteral()
 	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 {
 	if err != nil {
 		t.Error("error not expected ", err)
 		t.Error("error not expected ", err)
@@ -295,7 +299,7 @@ func TestParseMailbox(t *testing.T) {
 	s := NewParser([]byte("jsmith@[IPv6:2001:db8::1]"))
 	s := NewParser([]byte("jsmith@[IPv6:2001:db8::1]"))
 	err := s.mailbox()
 	err := s.mailbox()
 	if s.Domain != "2001:db8::1" {
 	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 {
 	if err != nil {
 		t.Error("error not expected ")
 		t.Error("error not expected ")
@@ -321,8 +325,8 @@ func TestParseMailbox(t *testing.T) {
 
 
 	s = NewParser([]byte("Joe.\\[email protected]"))
 	s = NewParser([]byte("Joe.\\[email protected]"))
 	err = s.mailbox()
 	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"))
 	s = NewParser([]byte("\"Abc@def\"@example.com"))
 	err = s.mailbox()
 	err = s.mailbox()
@@ -360,9 +364,12 @@ func TestParseMailbox(t *testing.T) {
 func TestParseLocalPart(t *testing.T) {
 func TestParseLocalPart(t *testing.T) {
 	s := NewParser([]byte("\"qu\\{oted\""))
 	s := NewParser([]byte("\"qu\\{oted\""))
 	err := s.localPart()
 	err := s.localPart()
-	if s.LocalPart != "qu\\{oted" {
+	if s.LocalPart != "qu{oted" {
 		t.Error("expected qu\\{oted, got:", s.LocalPart)
 		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 {
 	if err != nil {
 		t.Error("error not expected ")
 		t.Error("error not expected ")
 	}
 	}
@@ -393,8 +400,8 @@ func TestParseLocalPart(t *testing.T) {
 func TestParseQuotedString(t *testing.T) {
 func TestParseQuotedString(t *testing.T) {
 	s := NewParser([]byte("\"qu\\ oted\""))
 	s := NewParser([]byte("\"qu\\ oted\""))
 	err := s.quotedString()
 	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 {
 	if err != nil {
 		t.Error("error not expected ")
 		t.Error("error not expected ")
@@ -412,16 +419,16 @@ func TestParseQuotedString(t *testing.T) {
 
 
 func TestParseDotString(t *testing.T) {
 func TestParseDotString(t *testing.T) {
 
 
-	s := NewParser([]byte("Joe..\\\\Blow"))
+	s := NewParser([]byte("Joe..Blow"))
 	err := s.dotString()
 	err := s.dotString()
 	if err == nil {
 	if err == nil {
 		t.Error("error expected ")
 		t.Error("error expected ")
 	}
 	}
 
 
-	s = NewParser([]byte("Joe.\\\\Blow"))
+	s = NewParser([]byte("Joe.Blow"))
 	err = s.dotString()
 	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 {
 	if err != nil {
 		t.Error("error not expected ")
 		t.Error("error not expected ")
@@ -476,6 +483,12 @@ func TestParseDomain(t *testing.T) {
 		t.Error("error not expected ")
 		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"))
 	s = NewParser([]byte("a.com.gov"))
 	err = s.domain()
 	err = s.domain()
 	if err != nil {
 	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) {
 func TestParse(t *testing.T) {
 
 
 	s := NewParser([]byte("<"))
 	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) {
 func TestTransport(t *testing.T) {
 
 
 	path := PathParam([]string{"BODY", "8bitmime"})
 	path := PathParam([]string{"BODY", "8bitmime"})
@@ -607,4 +673,4 @@ func TestTransport(t *testing.T) {
 	if transport != TransportTypeUnspecified {
 	if transport != TransportTypeUnspecified {
 		t.Error("transport was not unspecified")
 		t.Error("transport was not unspecified")
 	}
 	}
-}
+}

+ 11 - 3
response/enhanced.go

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

+ 44 - 5
server.go

@@ -6,6 +6,7 @@ import (
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
 	"fmt"
 	"fmt"
+	"github.com/sirupsen/logrus"
 	"io"
 	"io"
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
@@ -112,13 +113,13 @@ func newServer(sc *ServerConfig, b backends.Backend, mainlog log.Logger) (*serve
 	}
 	}
 	server.setConfig(sc)
 	server.setConfig(sc)
 	server.setTimeout(sc.Timeout)
 	server.setTimeout(sc.Timeout)
-	if err := server.configureSSL(); err != nil {
+	if err := server.configureTLS(); err != nil {
 		return server, err
 		return server, err
 	}
 	}
 	return server, nil
 	return server, nil
 }
 }
 
 
-func (s *server) configureSSL() error {
+func (s *server) configureTLS() error {
 	sConfig := s.configStore.Load().(ServerConfig)
 	sConfig := s.configStore.Load().(ServerConfig)
 	if sConfig.TLS.AlwaysOn || sConfig.TLS.StartTLSOn {
 	if sConfig.TLS.AlwaysOn || sConfig.TLS.StartTLSOn {
 		cert, err := tls.LoadX509KeyPair(sConfig.TLS.PublicKeyFile, sConfig.TLS.PrivateKeyFile)
 		cert, err := tls.LoadX509KeyPair(sConfig.TLS.PublicKeyFile, sConfig.TLS.PrivateKeyFile)
@@ -217,6 +218,11 @@ func (s *server) setAllowedHosts(allowedHosts []string) {
 	for _, h := range allowedHosts {
 	for _, h := range allowedHosts {
 		if strings.Contains(h, "*") {
 		if strings.Contains(h, "*") {
 			s.hosts.wildcards = append(s.hosts.wildcards, strings.ToLower(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 {
 		} else {
 			s.hosts.table[strings.ToLower(h)] = true
 			s.hosts.table[strings.ToLower(h)] = true
 		}
 		}
@@ -319,6 +325,11 @@ func (s *server) allowsHost(host string) bool {
 	return false
 	return false
 }
 }
 
 
+func (s *server) allowsIp(ip net.IP) bool {
+	ipStr := ip.String()
+	return s.allowsHost("[" + ipStr + "]")
+}
+
 const commandSuffix = "\r\n"
 const commandSuffix = "\r\n"
 
 
 // Reads from the client until a \n terminator is encountered,
 // 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])
 			cmd := bytes.ToUpper(input[:cmdLen])
 			switch {
 			switch {
 			case cmdHELO.match(cmd):
 			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.resetTransaction()
 				client.sendResponse(helo)
 				client.sendResponse(helo)
 
 
 			case cmdEHLO.match(cmd):
 			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.resetTransaction()
 				client.sendResponse(ehlo,
 				client.sendResponse(ehlo,
 					messageSize,
 					messageSize,
@@ -506,7 +531,8 @@ func (s *server) handleClient(client *client) {
 					client.sendResponse(err.Error())
 					client.sendResponse(err.Error())
 					break
 					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)
 					client.sendResponse(r.ErrorRelayDenied, " ", to.Host)
 				} else {
 				} else {
 					client.PushRcpt(to)
 					client.PushRcpt(to)
@@ -679,3 +705,16 @@ func (s *server) loadLog(value *atomic.Value) log.Logger {
 	}
 	}
 	return l
 	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"},
 			Protocols:      []string{"tls1.0", "tls1.2"},
 		},
 		},
 	})
 	})
-	if err := s.configureSSL(); err != nil {
+	if err := s.configureTLS(); err != nil {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
 
 
@@ -302,6 +302,550 @@ func TestHandleClient(t *testing.T) {
 	wg.Wait() // wait for handleClient to exit
 	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) {
 func TestXClient(t *testing.T) {
 	var mainlog log.Logger
 	var mainlog log.Logger
 	var logOpenError error
 	var logOpenError error
@@ -552,6 +1096,9 @@ func TestAllowsHosts(t *testing.T) {
 		"*.test",
 		"*.test",
 		"wild*.card",
 		"wild*.card",
 		"multiple*wild*cards.*",
 		"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)
 	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
 	// only wildcard - should match anything
 	s.setAllowedHosts([]string{"*"})
 	s.setAllowedHosts([]string{"*"})
 	if !s.allowsHost("match.me") {
 	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
+}