Browse Source

Merge branch 'master' into tests

flashmob 6 years ago
parent
commit
1bce19ab82

+ 3 - 2
.travis.yml

@@ -1,9 +1,10 @@
 language: go
 sudo: false
 go:
-  - 1.5
-  - 1.6
   - 1.7
+  - 1.8
+  - 1.9
+  - 1.10.x
   - master
 
 install:

+ 3 - 1
README.md

@@ -1,6 +1,8 @@
 
 [![Build Status](https://travis-ci.org/flashmob/go-guerrilla.svg?branch=master)](https://travis-ci.org/flashmob/go-guerrilla)
 
+Breaking change: The structure of the config has recently changed to accommodate more advanced TLS settings.
+
 Go-Guerrilla SMTP Daemon
 ====================
 
@@ -265,7 +267,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 recommenced if you
+ or supporting SSL versions not supported by Go (highly not recommended if you
  want to use older SSL versions), 
  it is possible to [use NGINX as a proxy](https://github.com/flashmob/go-guerrilla/wiki/Using-Nginx-as-a-proxy).
 

+ 1 - 1
api.go

@@ -93,7 +93,7 @@ func (d *Daemon) LoadConfig(path string) (AppConfig, error) {
 	var ac AppConfig
 	data, err := ioutil.ReadFile(path)
 	if err != nil {
-		return ac, fmt.Errorf("Could not read config file: %s", err.Error())
+		return ac, fmt.Errorf("could not read config file: %s", err.Error())
 	}
 	err = ac.Load(data)
 	if err != nil {

+ 77 - 16
api_test.go

@@ -2,10 +2,12 @@ package guerrilla
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
+	"github.com/flashmob/go-guerrilla/response"
 	"io/ioutil"
 	"net"
 	"os"
@@ -133,13 +135,15 @@ func TestSMTPLoadFile(t *testing.T) {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -161,13 +165,15 @@ func TestSMTPLoadFile(t *testing.T) {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+ 				"private_key_file":"config_test.go",
+				"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -310,9 +316,9 @@ func TestSetConfigError(t *testing.T) {
 
 	// lets add a new server with bad TLS
 	sc.ListenInterface = "127.0.0.1:2527"
-	sc.StartTLSOn = true
-	sc.PublicKeyFile = "tests/testlog" // totally wrong :->
-	sc.PublicKeyFile = "tests/testlog" // totally wrong :->
+	sc.TLS.StartTLSOn = true
+	sc.TLS.PublicKeyFile = "tests/testlog"  // totally wrong :->
+	sc.TLS.PrivateKeyFile = "tests/testlog" // totally wrong :->
 
 	cfg.Servers = append(cfg.Servers, sc)
 
@@ -345,7 +351,7 @@ var funkyLogger = func() backends.Decorator {
 		return backends.ProcessWith(
 			func(e *mail.Envelope, task backends.SelectTask) (backends.Result, error) {
 				if task == backends.TaskValidateRcpt {
-					// validate the last recipient appended to e.Rcpt
+					// log the last recipient appended to e.Rcpt
 					backends.Log().Infof(
 						"another funky recipient [%s]",
 						e.RcptTo[len(e.RcptTo)-1])
@@ -417,7 +423,7 @@ func talkToServer(address string) {
 	str, err = in.ReadString('\n')
 	fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
 	str, err = in.ReadString('\n')
-	fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+	fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 	str, err = in.ReadString('\n')
 	fmt.Fprint(conn, "DATA\r\n")
 	str, err = in.ReadString('\n')
@@ -543,7 +549,7 @@ func TestSkipAllowsHost(t *testing.T) {
 	}
 	in := bufio.NewReader(conn)
 	fmt.Fprint(conn, "HELO test\r\n")
-	fmt.Fprint(conn, "RCPT TO: [email protected]\r\n")
+	fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 	in.ReadString('\n')
 	in.ReadString('\n')
 	str, _ := in.ReadString('\n')
@@ -552,3 +558,58 @@ func TestSkipAllowsHost(t *testing.T) {
 	}
 
 }
+
+var customBackend2 = 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.TaskValidateRcpt {
+					return p.Process(e, task)
+				} else if task == backends.TaskSaveMail {
+					backends.Log().Info("Another funky email!")
+					err := errors.New("system shock")
+					return backends.NewResult(response.Canned.FailReadErrorDataCmd, response.SP, err), err
+				}
+				return p.Process(e, task)
+			})
+	}
+}
+
+// Test a custom backend response
+func TestCustomBackendResult(t *testing.T) {
+	os.Truncate("tests/testlog", 0)
+	cfg := &AppConfig{
+		LogFile:      "tests/testlog",
+		AllowedHosts: []string{"grr.la"},
+		BackendConfig: backends.BackendConfig{
+			"save_process":     "HeadersParser|Debugger|Custom",
+			"validate_process": "Custom",
+		},
+	}
+	d := Daemon{Config: cfg}
+	d.AddProcessor("Custom", customBackend2)
+
+	if err := d.Start(); err != nil {
+		t.Error(err)
+	}
+	// lets have a talk with the server
+	talkToServer("127.0.0.1:2525")
+
+	d.Shutdown()
+
+	b, err := ioutil.ReadFile("tests/testlog")
+	if err != nil {
+		t.Error("could not read logfile")
+		return
+	}
+	// lets check for fingerprints
+	if strings.Index(string(b), "451 4.3.0 Error") < 0 {
+		t.Error("did not log: 451 4.3.0 Error")
+	}
+
+	if strings.Index(string(b), "system shock") < 0 {
+		t.Error("did not log: system shock")
+	}
+
+}

+ 22 - 7
backends/backend.go

@@ -1,6 +1,7 @@
 package backends
 
 import (
+	"bytes"
 	"fmt"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
@@ -54,6 +55,7 @@ type BaseConfig interface{}
 type notifyMsg struct {
 	err      error
 	queuedID string
+	result   Result
 }
 
 // Result represents a response to an SMTP client after receiving DATA.
@@ -66,16 +68,18 @@ type Result interface {
 }
 
 // Internal implementation of BackendResult for use by backend implementations.
-type result string
+type result struct {
+	bytes.Buffer
+}
 
-func (br result) String() string {
-	return string(br)
+func (r *result) String() string {
+	return r.Buffer.String()
 }
 
 // Parses the SMTP code from the first 3 characters of the SMTP message.
 // Returns 554 if code cannot be parsed.
-func (br result) Code() int {
-	trimmed := strings.TrimSpace(string(br))
+func (r *result) Code() int {
+	trimmed := strings.TrimSpace(r.String())
 	if len(trimmed) < 3 {
 		return 554
 	}
@@ -86,8 +90,19 @@ func (br result) Code() int {
 	return code
 }
 
-func NewResult(message string) Result {
-	return result(message)
+func NewResult(r ...interface{}) Result {
+	buf := new(result)
+	for _, item := range r {
+		switch v := item.(type) {
+		case error:
+			buf.WriteString(v.Error())
+		case fmt.Stringer:
+			buf.WriteString(v.String())
+		case string:
+			buf.WriteString(v)
+		}
+	}
+	return buf
 }
 
 type processorInitializer interface {

+ 30 - 24
backends/gateway.go

@@ -128,7 +128,7 @@ func (w *workerMsg) reset(e *mail.Envelope, task SelectTask) {
 // Process distributes an envelope to one of the backend workers with a TaskSaveMail task
 func (gw *BackendGateway) Process(e *mail.Envelope) Result {
 	if gw.State != BackendStateRunning {
-		return NewResult(response.Canned.FailBackendNotRunning + gw.State.String())
+		return NewResult(response.Canned.FailBackendNotRunning, response.SP, gw.State)
 	}
 	// borrow a workerMsg from the pool
 	workerMsg := workerMsgPool.Get().(*workerMsg)
@@ -139,11 +139,32 @@ func (gw *BackendGateway) Process(e *mail.Envelope) Result {
 	// or timeout
 	select {
 	case status := <-workerMsg.notifyMe:
-		workerMsgPool.Put(workerMsg) // can be recycled since we used the notifyMe channel
+		// email saving transaction completed
+		if status.result == BackendResultOK && status.queuedID != "" {
+			return NewResult(response.Canned.SuccessMessageQueued, response.SP, status.queuedID)
+		}
+
+		// A custom result, there was probably an error, if so, log it
+		if status.result != nil {
+			if status.err != nil {
+				Log().Error(status.err)
+			}
+			return status.result
+		}
+
+		// if there was no result, but there's an error, then make a new result from the error
 		if status.err != nil {
-			return NewResult(response.Canned.FailBackendTransaction + status.err.Error())
+			if _, err := strconv.Atoi(status.err.Error()[:3]); err != nil {
+				return NewResult(response.Canned.FailBackendTransaction, response.SP, status.err)
+			}
+			return NewResult(status.err)
 		}
-		return NewResult(response.Canned.SuccessMessageQueued + status.queuedID)
+
+		// both result & error are nil (should not happen)
+		err := errors.New("no response from backend - processor did not return a result or an error")
+		Log().Error(err)
+		return NewResult(response.Canned.FailBackendTransaction, response.SP, err)
+
 	case <-time.After(gw.saveTimeout()):
 		Log().Error("Backend has timed out while saving email")
 		e.Lock() // lock the envelope - it's still processing here, we don't want the server to recycle it
@@ -434,27 +455,12 @@ func (gw *BackendGateway) workDispatcher(
 			return
 		case msg = <-workIn:
 			state = dispatcherStateWorking // recovers from panic if in this state
+			result, err := save.Process(msg.e, msg.task)
+			state = dispatcherStateNotify
 			if msg.task == TaskSaveMail {
-				// process the email here
-				result, _ := save.Process(msg.e, TaskSaveMail)
-				state = dispatcherStateNotify
-				if result.Code() < 300 {
-					// if all good, let the gateway know that it was saved
-					msg.notifyMe <- &notifyMsg{nil, msg.e.QueuedId}
-				} else {
-					// notify the gateway about the error
-					msg.notifyMe <- &notifyMsg{err: errors.New(result.String())}
-				}
-			} else if msg.task == TaskValidateRcpt {
-				_, err := validate.Process(msg.e, TaskValidateRcpt)
-				state = dispatcherStateNotify
-				if err != nil {
-					// validation failed
-					msg.notifyMe <- &notifyMsg{err: err}
-				} else {
-					// all good.
-					msg.notifyMe <- &notifyMsg{err: nil}
-				}
+				msg.notifyMe <- &notifyMsg{err: err, result: result, queuedID: msg.e.QueuedId}
+			} else {
+				msg.notifyMe <- &notifyMsg{err: err, result: result}
 			}
 		}
 		state = dispatcherStateIdle

+ 4 - 4
backends/p_guerrilla_db_redis.go

@@ -13,7 +13,6 @@ import (
 	"time"
 
 	"github.com/flashmob/go-guerrilla/mail"
-	"github.com/garyburd/redigo/redis"
 )
 
 // ----------------------------------------------------------------------------------
@@ -85,7 +84,7 @@ func (g *GuerrillaDBAndRedisBackend) getNumberOfWorkers() int {
 
 type redisClient struct {
 	isConnected bool
-	conn        redis.Conn
+	conn        RedisConn
 	time        int
 }
 
@@ -319,7 +318,7 @@ func (g *GuerrillaDBAndRedisBackend) sqlConnect() (*sql.DB, error) {
 		// do we have access?
 		_, err = db.Query("SELECT mail_id FROM " + g.config.Table + " LIMIT 1")
 		if err != nil {
-			Log().Error("cannot select table", err)
+			Log().Error("cannot select table:", err)
 			return nil, err
 		}
 		return db, nil
@@ -328,7 +327,7 @@ func (g *GuerrillaDBAndRedisBackend) sqlConnect() (*sql.DB, error) {
 
 func (c *redisClient) redisConnection(redisInterface string) (err error) {
 	if c.isConnected == false {
-		c.conn, err = redis.Dial("tcp", redisInterface)
+		c.conn, err = RedisDialer("tcp", redisInterface)
 		if err != nil {
 			// handle error
 			return err
@@ -420,6 +419,7 @@ func GuerrillaDbRedis() Decorator {
 					e.MailFrom.String(),
 					e.Subject,
 					ts)
+				e.QueuedId = hash
 				// Add extra headers
 				var addHead string
 				addHead += "Delivered-To: " + to + "\r\n"

+ 3 - 5
backends/p_redis.go

@@ -5,8 +5,6 @@ import (
 
 	"github.com/flashmob/go-guerrilla/mail"
 	"github.com/flashmob/go-guerrilla/response"
-
-	"github.com/garyburd/redigo/redis"
 )
 
 // ----------------------------------------------------------------------------------
@@ -39,12 +37,12 @@ type RedisProcessorConfig struct {
 
 type RedisProcessor struct {
 	isConnected bool
-	conn        redis.Conn
+	conn        RedisConn
 }
 
 func (r *RedisProcessor) redisConnection(redisInterface string) (err error) {
 	if r.isConnected == false {
-		r.conn, err = redis.Dial("tcp", redisInterface)
+		r.conn, err = RedisDialer("tcp", redisInterface)
 		if err != nil {
 			// handle error
 			return err
@@ -113,7 +111,7 @@ func Redis() Decorator {
 					}
 					e.Values["redis"] = "redis" // the next processor will know to look in redis for the message data
 				} else {
-					Log().Error("Redis needs a Hash() process before it")
+					Log().Error("Redis needs a Hasher() process before it")
 					result := NewResult(response.Canned.FailBackendTransaction)
 					return result, StorageError
 				}

+ 55 - 0
backends/p_redis_test.go

@@ -0,0 +1,55 @@
+package backends
+
+import (
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/mail"
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+)
+
+func TestRedisGeneric(t *testing.T) {
+
+	e := mail.NewEnvelope("127.0.0.1", 1)
+	e.RcptTo = append(e.RcptTo, mail.Address{User: "test", Host: "grr.la"})
+
+	l, _ := log.GetLogger("./test_redis.log", "debug")
+	g, err := New(BackendConfig{
+		"save_process":         "Hasher|Redis",
+		"redis_interface":      "127.0.0.1:6379",
+		"redis_expire_seconds": 7200,
+	}, l)
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	err = g.Start()
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	defer g.Shutdown()
+	if gateway, ok := g.(*BackendGateway); ok {
+		r := gateway.Process(e)
+		if strings.Index(r.String(), "250 2.0.0 OK") == -1 {
+			t.Error("redis processor didn't result with expected result, it said", r)
+		}
+	}
+	// check the log
+	if _, err := os.Stat("./test_redis.log"); err != nil {
+		t.Error(err)
+		return
+	}
+	if b, err := ioutil.ReadFile("./test_redis.log"); err != nil {
+		t.Error(err)
+		return
+	} else {
+		if strings.Index(string(b), "SETEX") == -1 {
+			t.Error("Log did not contain SETEX, the log was: ", string(b))
+		}
+	}
+
+	os.Remove("./test_redis.log")
+
+}

+ 6 - 4
backends/p_sql.go

@@ -181,7 +181,6 @@ func SQL() Decorator {
 
 				hash := ""
 				if len(e.Hashes) > 0 {
-					// if saved in redis, hash will be the redis key
 					hash = e.Hashes[0]
 					e.QueuedId = e.Hashes[0]
 				}
@@ -246,9 +245,12 @@ func SQL() Decorator {
 						recipient,
 						s.ip2bint(e.RemoteIP).Bytes(),         // ip_addr store as varbinary(16)
 						trimToLimit(e.MailFrom.String(), 255), // return_path
-						e.TLS,   // is_tls
-						mid,     // message_id
-						replyTo, // reply_to
+						// is_tls
+						e.TLS,
+						// message_id
+						mid,
+						// reply_to
+						replyTo,
 						sender,
 					)
 

+ 45 - 0
backends/redis_generic.go

@@ -0,0 +1,45 @@
+package backends
+
+import (
+	"net"
+	"time"
+)
+
+func init() {
+	RedisDialer = func(network, address string, options ...RedisDialOption) (RedisConn, error) {
+		return new(RedisMockConn), nil
+	}
+}
+
+// RedisConn interface provides a generic way to access Redis via drivers
+type RedisConn interface {
+	Close() error
+	Do(commandName string, args ...interface{}) (reply interface{}, err error)
+}
+
+type RedisMockConn struct{}
+
+func (m *RedisMockConn) Close() error {
+	return nil
+}
+
+func (m *RedisMockConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
+	Log().Info("redis mock driver command: ", commandName)
+	return nil, nil
+}
+
+type dialOptions struct {
+	readTimeout  time.Duration
+	writeTimeout time.Duration
+	dial         func(network, addr string) (net.Conn, error)
+	db           int
+	password     string
+}
+
+type RedisDialOption struct {
+	f func(*dialOptions)
+}
+
+type redisDial func(network, address string, options ...RedisDialOption) (RedisConn, error)
+
+var RedisDialer redisDial

+ 10 - 0
backends/storage/redigo/driver.go

@@ -0,0 +1,10 @@
+package redigo_driver
+
+import "github.com/flashmob/go-guerrilla/backends"
+import redigo "github.com/gomodule/redigo/redis"
+
+func init() {
+	backends.RedisDialer = func(network, address string, options ...backends.RedisDialOption) (backends.RedisConn, error) {
+		return redigo.Dial(network, address)
+	}
+}

+ 68 - 32
client.go

@@ -4,9 +4,12 @@ import (
 	"bufio"
 	"bytes"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
+	"github.com/flashmob/go-guerrilla/mail/rfc5321"
+	"github.com/flashmob/go-guerrilla/response"
 	"net"
 	"net/textproto"
 	"sync"
@@ -38,8 +41,9 @@ type client struct {
 	errors       int
 	state        ClientState
 	messagesSent int
-	// Response to be written to the client
+	// Response to be written to the client (for debugging)
 	response   bytes.Buffer
+	bufErr     error
 	conn       net.Conn
 	bufin      *smtpBufferedReader
 	bufout     *bufio.Writer
@@ -48,6 +52,7 @@ type client struct {
 	// guards access to conn
 	connGuard sync.Mutex
 	log       log.Logger
+	parser    rfc5321.Parser
 }
 
 // NewClient allocates a new client.
@@ -69,39 +74,38 @@ func NewClient(conn net.Conn, clientID uint64, logger log.Logger, envelope *mail
 	return c
 }
 
-// setResponse adds a response to be written on the next turn
+// sendResponse adds a response to be written on the next turn
+// the response gets buffered
 func (c *client) sendResponse(r ...interface{}) {
 	c.bufout.Reset(c.conn)
 	if c.log.IsDebug() {
-		// us additional buffer so that we can log the response in debug mode only
+		// an additional buffer so that we can log the response in debug mode only
 		c.response.Reset()
 	}
+	var out string
+	if c.bufErr != nil {
+		c.bufErr = nil
+	}
 	for _, item := range r {
 		switch v := item.(type) {
-		case string:
-			if _, err := c.bufout.WriteString(v); err != nil {
-				c.log.WithError(err).Error("could not write to c.bufout")
-			}
-			if c.log.IsDebug() {
-				c.response.WriteString(v)
-			}
 		case error:
-			if _, err := c.bufout.WriteString(v.Error()); err != nil {
-				c.log.WithError(err).Error("could not write to c.bufout")
-			}
-			if c.log.IsDebug() {
-				c.response.WriteString(v.Error())
-			}
+			out = v.Error()
 		case fmt.Stringer:
-			if _, err := c.bufout.WriteString(v.String()); err != nil {
-				c.log.WithError(err).Error("could not write to c.bufout")
-			}
-			if c.log.IsDebug() {
-				c.response.WriteString(v.String())
-			}
+			out = v.String()
+		case string:
+			out = v
+		}
+		if _, c.bufErr = c.bufout.WriteString(out); c.bufErr != nil {
+			c.log.WithError(c.bufErr).Error("could not write to c.bufout")
+		}
+		if c.log.IsDebug() {
+			c.response.WriteString(out)
+		}
+		if c.bufErr != nil {
+			return
 		}
 	}
-	c.bufout.WriteString("\r\n")
+	_, c.bufErr = c.bufout.WriteString("\r\n")
 	if c.log.IsDebug() {
 		c.response.WriteString("\r\n")
 	}
@@ -120,8 +124,7 @@ func (c *client) resetTransaction() {
 // A transaction starts after a MAIL command gets issued by the client.
 // Call resetTransaction to end the transaction
 func (c *client) isInTransaction() bool {
-	isMailFromEmpty := c.MailFrom == (mail.Address{})
-	if isMailFromEmpty {
+	if len(c.MailFrom.User) == 0 && !c.MailFrom.NullPath {
 		return false
 	}
 	return true
@@ -176,20 +179,20 @@ func (c *client) getID() uint64 {
 }
 
 // UpgradeToTLS upgrades a client connection to TLS
-func (client *client) upgradeToTLS(tlsConfig *tls.Config) error {
+func (c *client) upgradeToTLS(tlsConfig *tls.Config) error {
 	var tlsConn *tls.Conn
-	// load the config thread-safely
-	tlsConn = tls.Server(client.conn, tlsConfig)
+	// wrap c.conn in a new TLS server side connection
+	tlsConn = tls.Server(c.conn, tlsConfig)
 	// Call handshake here to get any handshake error before reading starts
 	err := tlsConn.Handshake()
 	if err != nil {
 		return err
 	}
 	// convert tlsConn to net.Conn
-	client.conn = net.Conn(tlsConn)
-	client.bufout.Reset(client.conn)
-	client.bufin.Reset(client.conn)
-	client.TLS = true
+	c.conn = net.Conn(tlsConn)
+	c.bufout.Reset(c.conn)
+	c.bufin.Reset(c.conn)
+	c.TLS = true
 	return err
 }
 
@@ -201,3 +204,36 @@ func getRemoteAddr(conn net.Conn) string {
 		return conn.RemoteAddr().Network()
 	}
 }
+
+type pathParser func([]byte) error
+
+func (c *client) parsePath(in []byte, p pathParser) (mail.Address, error) {
+	address := mail.Address{}
+	var err error
+	if len(in) > rfc5321.LimitPath {
+		return address, errors.New(response.Canned.FailPathTooLong.String())
+	}
+	if err = p(in); err != nil {
+		return address, errors.New(response.Canned.FailInvalidAddress.String())
+	} else if c.parser.NullPath {
+		// bounce has empty from address
+		address = mail.Address{}
+	} else if len(c.parser.LocalPart) > rfc5321.LimitLocalPart {
+		err = errors.New(response.Canned.FailLocalPartTooLong.String())
+	} else if len(c.parser.Domain) > rfc5321.LimitDomain {
+		err = errors.New(response.Canned.FailDomainTooLong.String())
+	} else {
+		address = mail.Address{
+			User:       c.parser.LocalPart,
+			Host:       c.parser.Domain,
+			ADL:        c.parser.ADL,
+			PathParams: c.parser.PathParams,
+			NullPath:   c.parser.NullPath,
+		}
+	}
+	return address, err
+}
+
+func (s *server) rcptTo(in []byte) (address mail.Address, err error) {
+	return address, err
+}

+ 7 - 1
cmd/guerrillad/serve.go

@@ -12,8 +12,14 @@ import (
 
 	"github.com/flashmob/go-guerrilla"
 	"github.com/flashmob/go-guerrilla/log"
+
+	// enable the Redis redigo driver
+	_ "github.com/flashmob/go-guerrilla/backends/storage/redigo"
+
+	// Choose iconv or mail/encoding package which uses golang.org/x/net/html/charset
 	//_ "github.com/flashmob/go-guerrilla/mail/iconv"
 	_ "github.com/flashmob/go-guerrilla/mail/encoding"
+
 	"github.com/spf13/cobra"
 
 	_ "github.com/go-sql-driver/mysql"
@@ -136,7 +142,7 @@ func readConfig(path string, pidFile string) (*guerrilla.AppConfig, error) {
 	// command line flags can override config values
 	appConfig, err := d.LoadConfig(path)
 	if err != nil {
-		return &appConfig, fmt.Errorf("Could not read config file: %s", err.Error())
+		return &appConfig, fmt.Errorf("could not read config file: %s", err.Error())
 	}
 	// override config pidFile with with flag from the command line
 	if len(pidFile) > 0 {

+ 96 - 53
cmd/guerrillad/serve_test.go

@@ -43,27 +43,31 @@ var configJsonA = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:3536",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : false,
             "host_name":"enable.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2228",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+				"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -92,20 +96,22 @@ var configJsonB = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:3536",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+            	"start_tls_on":true,
+            	"tls_always_on":false
+			}
         }
     ]
 }
 `
 
-// backend_name changed, is guerrilla-redis-db + added a server
+// added a server
 var configJsonC = `
 {
     "log_file" : "../../tests/testlog",
@@ -118,46 +124,49 @@ var configJsonC = `
       "guerrillamail.net",
       "guerrillamail.org"
     ],
-    "backend_name": "guerrilla-redis-db",
     "backend_config" :
         {
             "sql_driver": "mysql",
-            "sql_dsn": "root:ok@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10&writeTimeout=10",
+            "sql_dsn": "root:ok@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10s&writeTimeout=10s",
             "mail_table":"new_mail",
             "redis_interface" : "127.0.0.1:6379",
             "redis_expire_seconds" : 7200,
             "save_workers_size" : 3,
             "primary_mail_host":"sharklasers.com",
             "save_workers_size" : 1,
-	    "save_process": "HeadersParser|Debugger",
-	    "log_received_mails": true
+	    	"save_process": "HeadersParser|Debugger",
+	    	"log_received_mails": true
         },
     "servers" : [
         {
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:25",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size":1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:465",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -186,27 +195,31 @@ var configJsonD = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2552",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"secure.test.com",
             "max_size":1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:4655",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+				"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -231,7 +244,7 @@ var configJsonE = `
             "save_process": "GuerrillaRedisDB",
             "log_received_mails" : true,
             "sql_driver": "mysql",
-            "sql_dsn": "root:secret@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10&writeTimeout=10",
+            "sql_dsn": "root:secret@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10s&writeTimeout=10s",
             "mail_table":"new_mail",
             "redis_interface" : "127.0.0.1:6379",
             "redis_expire_seconds" : 7200,
@@ -243,27 +256,31 @@ var configJsonE = `
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2552",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"secure.test.com",
             "max_size":1000000,
-            "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:4655",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "../../tests/testlog"
+            "log_file" : "../../tests/testlog",
+			"tls" : {
+				"private_key_file":"../../tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -667,6 +684,32 @@ func TestServerStopEvent(t *testing.T) {
 
 }
 
+// just a utility for debugging when using the debugger, skipped by default
+func TestDebug(t *testing.T) {
+
+	t.SkipNow()
+	conf := guerrilla.ServerConfig{ListenInterface: "127.0.0.1:2526"}
+	if conn, buffin, err := test.Connect(conf, 20); err != nil {
+		t.Error("Could not connect to new server", conf.ListenInterface, err)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 mai1.guerrillamail.com Hello"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			} else {
+				if result, err = test.Command(conn, buffin, "RCPT TO:<[email protected]>"); err == nil {
+					expect := "250 2.1.5 OK"
+					if strings.Index(result, expect) != 0 {
+						t.Error("Expected:", expect, "but got:", result)
+					}
+				}
+			}
+		}
+		conn.Close()
+
+	}
+}
+
 // Start with configJsonD.json,
 // then connect to 127.0.0.1:4655 & HELO & try RCPT TO with an invalid host [grr.la]
 // then change the config to enable add new host [grr.la] to allowed_hosts
@@ -701,7 +744,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 			} else {
-				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
+				if result, err = test.Command(conn, buffin, "RCPT TO:<[email protected]>"); err == nil {
 					expect := "454 4.1.1 Error: Relay access denied: grr.la"
 					if strings.Index(result, expect) != 0 {
 						t.Error("Expected:", expect, "but got:", result)
@@ -734,7 +777,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 			if strings.Index(result, expect) != 0 {
 				t.Error("Expected", expect, "but got", result)
 			} else {
-				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
+				if result, err = test.Command(conn, buffin, "RCPT TO:<[email protected]>"); err == nil {
 					expect := "250 2.1.5 OK"
 					if strings.Index(result, expect) != 0 {
 						t.Error("Expected:", expect, "but got:", result)

+ 137 - 56
config.go

@@ -41,20 +41,13 @@ type ServerConfig struct {
 	// MaxSize is the maximum size of an email that will be accepted for delivery.
 	// Defaults to 10 Mebibytes
 	MaxSize int64 `json:"max_size"`
-	// PrivateKeyFile path to cert private key in PEM format. Will be ignored if blank
-	PrivateKeyFile string `json:"private_key_file"`
-	// PublicKeyFile path to cert (public key) chain in PEM format.
-	// Will be ignored if blank
-	PublicKeyFile string `json:"public_key_file"`
+	// TLS Configuration
+	TLS ServerTLSConfig `json:"tls,omitempty"`
 	// Timeout specifies the connection timeout in seconds. Defaults to 30
 	Timeout int `json:"timeout"`
 	// Listen interface specified in <ip>:<port> - defaults to 127.0.0.1:2525
 	ListenInterface string `json:"listen_interface"`
-	// StartTLSOn should we offer STARTTLS command. Cert must be valid.
-	// False by default
-	StartTLSOn bool `json:"start_tls_on,omitempty"`
-	// TLSAlwaysOn run this server as a pure TLS server, i.e. SMTPS
-	TLSAlwaysOn bool `json:"tls_always_on,omitempty"`
+
 	// MaxClients controls how many maxiumum clients we can handle at once.
 	// Defaults to 100
 	MaxClients int `json:"max_clients"`
@@ -64,10 +57,95 @@ type ServerConfig struct {
 	// XClientOn when using a proxy such as Nginx, XCLIENT command is used to pass the
 	// original client's IP address & client's HELO
 	XClientOn bool `json:"xclient_on,omitempty"`
+}
+
+type ServerTLSConfig struct {
+
+	// StartTLSOn should we offer STARTTLS command. Cert must be valid.
+	// False by default
+	StartTLSOn bool `json:"start_tls_on,omitempty"`
+	// AlwaysOn run this server as a pure TLS server, i.e. SMTPS
+	AlwaysOn bool `json:"tls_always_on,omitempty"`
+	// PrivateKeyFile path to cert private key in PEM format.
+	PrivateKeyFile string `json:"private_key_file"`
+	// PublicKeyFile path to cert (public key) chain in PEM format.
+	PublicKeyFile string `json:"public_key_file"`
+
+	// TLS Protocols to use. [0] = min, [1]max
+	// Use Go's default if empty
+	Protocols []string `json:"protocols,omitempty"`
+	// TLS Ciphers to use.
+	// Use Go's default if empty
+	Ciphers []string `json:"ciphers,omitempty"`
+	// TLS Curves to use.
+	// Use Go's default if empty
+	Curves []string `json:"curves,omitempty"`
+	// TLS Root cert authorities to use. "A PEM encoded CA's certificate file.
+	// Defaults to system's root CA file if empty
+	RootCAs string `json:"root_cas_file,omitempty"`
+	// declares the policy the server will follow for TLS Client Authentication.
+	// Use Go's default if empty
+	ClientAuthType string `json:"client_auth_type,omitempty"`
+	// controls whether the server selects the
+	// client's most preferred ciphersuite
+	PreferServerCipherSuites bool `json:"prefer_server_cipher_suites,omitempty"`
 
 	// The following used to watch certificate changes so that the TLS can be reloaded
-	_privateKeyFile_mtime int
-	_publicKeyFile_mtime  int
+	_privateKeyFile_mtime int64
+	_publicKeyFile_mtime  int64
+}
+
+// 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
+var TLSCiphers = map[string]uint16{
+
+	// // Note: Generally avoid using CBC unless for compatibility
+	"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,
+	"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA":  tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA":   tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+	"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA":   tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+
+	"TLS_RSA_WITH_RC4_128_SHA":        tls.TLS_RSA_WITH_RC4_128_SHA,
+	"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+	"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+
+	"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA":        tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+	"TLS_ECDHE_RSA_WITH_RC4_128_SHA":          tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+	"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+	"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,
+}
+
+// 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,
+}
+
+// https://golang.org/pkg/crypto/tls/#CurveID
+var TLSCurves = map[string]tls.CurveID{
+	"P256": tls.CurveP256,
+	"P384": tls.CurveP384,
+	"P521": tls.CurveP521,
+}
+
+// https://golang.org/pkg/crypto/tls/#ClientAuthType
+var TLSClientAuthTypes = map[string]tls.ClientAuthType{
+	"NoClientCert":               tls.NoClientCert,
+	"RequestClientCert":          tls.RequestClientCert,
+	"RequireAnyClientCert":       tls.RequireAnyClientCert,
+	"VerifyClientCertIfGiven":    tls.VerifyClientCertIfGiven,
+	"RequireAndVerifyClientCert": tls.RequireAndVerifyClientCert,
 }
 
 // Unmarshalls json data into AppConfig struct and any other initialization of the struct
@@ -274,11 +352,16 @@ func (c *AppConfig) setBackendDefaults() error {
 // All events are fired and run synchronously
 func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) {
 	// get a list of changes
-	changes := getDiff(
+	changes := getChanges(
 		*oldServer,
 		*sc,
 	)
-	if len(changes) > 0 {
+	tlsChanges := getChanges(
+		(*oldServer).TLS,
+		(*sc).TLS,
+	)
+
+	if len(changes) > 0 || len(tlsChanges) > 0 {
 		// something changed in the server config
 		app.Publish(EventConfigServerConfig, sc)
 	}
@@ -309,22 +392,7 @@ func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla)
 		app.Publish(EventConfigServerMaxClients, sc)
 	}
 
-	// tls changed
-	if ok := func() bool {
-		if _, ok := changes["PrivateKeyFile"]; ok {
-			return true
-		}
-		if _, ok := changes["PublicKeyFile"]; ok {
-			return true
-		}
-		if _, ok := changes["StartTLSOn"]; ok {
-			return true
-		}
-		if _, ok := changes["TLSAlwaysOn"]; ok {
-			return true
-		}
-		return false
-	}(); ok {
+	if len(tlsChanges) > 0 {
 		app.Publish(EventConfigServerTLSConfig, sc)
 	}
 }
@@ -338,37 +406,31 @@ func (sc *ServerConfig) loadTlsKeyTimestamps() error {
 				iface,
 				err.Error()))
 	}
-	if info, err := os.Stat(sc.PrivateKeyFile); err == nil {
-		sc._privateKeyFile_mtime = info.ModTime().Second()
+	if info, err := os.Stat(sc.TLS.PrivateKeyFile); err == nil {
+		sc.TLS._privateKeyFile_mtime = info.ModTime().Unix()
 	} else {
 		return statErr(sc.ListenInterface, err)
 	}
-	if info, err := os.Stat(sc.PublicKeyFile); err == nil {
-		sc._publicKeyFile_mtime = info.ModTime().Second()
+	if info, err := os.Stat(sc.TLS.PublicKeyFile); err == nil {
+		sc.TLS._publicKeyFile_mtime = info.ModTime().Unix()
 	} else {
 		return statErr(sc.ListenInterface, err)
 	}
 	return nil
 }
 
-// Gets the timestamp of the TLS certificates. Returns a unix time of when they were last modified
-// when the config was read. We use this info to determine if TLS needs to be re-loaded.
-func (sc *ServerConfig) getTlsKeyTimestamps() (int, int) {
-	return sc._privateKeyFile_mtime, sc._publicKeyFile_mtime
-}
-
 // Validate validates the server's configuration.
 func (sc *ServerConfig) Validate() error {
 	var errs Errors
 
-	if sc.StartTLSOn || sc.TLSAlwaysOn {
-		if sc.PublicKeyFile == "" {
+	if sc.TLS.StartTLSOn || sc.TLS.AlwaysOn {
+		if sc.TLS.PublicKeyFile == "" {
 			errs = append(errs, errors.New("PublicKeyFile is empty"))
 		}
-		if sc.PrivateKeyFile == "" {
+		if sc.TLS.PrivateKeyFile == "" {
 			errs = append(errs, errors.New("PrivateKeyFile is empty"))
 		}
-		if _, err := tls.LoadX509KeyPair(sc.PublicKeyFile, sc.PrivateKeyFile); err != nil {
+		if _, err := tls.LoadX509KeyPair(sc.TLS.PublicKeyFile, sc.TLS.PrivateKeyFile); err != nil {
 			errs = append(errs,
 				errors.New(fmt.Sprintf("cannot use TLS config for [%s], %v", sc.ListenInterface, err)))
 		}
@@ -380,28 +442,42 @@ func (sc *ServerConfig) Validate() error {
 	return nil
 }
 
-// Returns a diff between struct a & struct b.
+// Gets the timestamp of the TLS certificates. Returns a unix time of when they were last modified
+// when the config was read. We use this info to determine if TLS needs to be re-loaded.
+func (stc *ServerTLSConfig) getTlsKeyTimestamps() (int64, int64) {
+	return stc._privateKeyFile_mtime, stc._publicKeyFile_mtime
+}
+
+// Returns value changes between struct a & struct b.
 // Results are returned in a map, where each key is the name of the field that was different.
 // a and b are struct values, must not be pointer
 // and of the same struct type
-func getDiff(a interface{}, b interface{}) map[string]interface{} {
+func getChanges(a interface{}, b interface{}) map[string]interface{} {
 	ret := make(map[string]interface{}, 5)
 	compareWith := structtomap(b)
 	for key, val := range structtomap(a) {
+		if sliceOfStr, ok := val.([]string); ok {
+			val, _ = json.Marshal(sliceOfStr)
+			val = string(val.([]uint8))
+		}
+		if sliceOfStr, ok := compareWith[key].([]string); ok {
+			compareWith[key], _ = json.Marshal(sliceOfStr)
+			compareWith[key] = string(compareWith[key].([]uint8))
+		}
 		if val != compareWith[key] {
 			ret[key] = compareWith[key]
 		}
 	}
-	// detect tls changes (have the key files been modified?)
-	if oldServer, ok := a.(ServerConfig); ok {
-		t1, t2 := oldServer.getTlsKeyTimestamps()
-		if newServer, ok := b.(ServerConfig); ok {
-			t3, t4 := newServer.getTlsKeyTimestamps()
+	// detect changes to TLS keys (have the key files been modified?)
+	if oldTLS, ok := a.(ServerTLSConfig); ok {
+		t1, t2 := oldTLS.getTlsKeyTimestamps()
+		if newTLS, ok := b.(ServerTLSConfig); ok {
+			t3, t4 := newTLS.getTlsKeyTimestamps()
 			if t1 != t3 {
-				ret["PrivateKeyFile"] = newServer.PrivateKeyFile
+				ret["PrivateKeyFile"] = newTLS.PrivateKeyFile
 			}
 			if t2 != t4 {
-				ret["PublicKeyFile"] = newServer.PublicKeyFile
+				ret["PublicKeyFile"] = newTLS.PublicKeyFile
 			}
 		}
 	}
@@ -409,7 +485,8 @@ func getDiff(a interface{}, b interface{}) map[string]interface{} {
 }
 
 // Convert fields of a struct to a map
-// only able to convert int, bool and string; not recursive
+// only able to convert int, bool, slice-of-strings and string; not recursive
+// slices are marshal'd to json for convenient comparison later
 func structtomap(obj interface{}) map[string]interface{} {
 	ret := make(map[string]interface{}, 0)
 	v := reflect.ValueOf(obj)
@@ -417,9 +494,11 @@ func structtomap(obj interface{}) map[string]interface{} {
 	for index := 0; index < v.NumField(); index++ {
 		vField := v.Field(index)
 		fName := t.Field(index).Name
-
-		switch vField.Kind() {
+		k := vField.Kind()
+		switch k {
 		case reflect.Int:
+			fallthrough
+		case reflect.Int64:
 			value := vField.Int()
 			ret[fName] = value
 		case reflect.String:
@@ -428,6 +507,8 @@ func structtomap(obj interface{}) map[string]interface{} {
 		case reflect.Bool:
 			value := vField.Bool()
 			ret[fName] = value
+		case reflect.Slice:
+			ret[fName] = vField.Interface().([]string)
 		}
 	}
 	return ret

+ 62 - 50
config_test.go

@@ -33,55 +33,59 @@ var configJsonA = `
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"start_tls_on":false,
+            	"tls_always_on":false,
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go"
+			}
         },
-
         {
             "is_enabled" : true,
             "host_name":"mail2.guerrillamail.com",
             "max_size":1000001,
-            "private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file":"./tests/mail2.guerrillamail.com.cert.pem",
             "timeout":180,
             "listen_interface":"127.0.0.1:2527",
-            "start_tls_on":true,
-            "tls_always_on":false,
-            "max_clients":1
+			"max_clients":1,
+			"tls" : {
+ 				"private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file":"./tests/mail2.guerrillamail.com.cert.pem",
+				"tls_always_on":false,
+            	"start_tls_on":true
+			}
         },
 
         {
             "is_enabled" : true,
             "host_name":"mail.stopme.com",
-            "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
+            "max_size": 100017, 
             "timeout":160,
-            "listen_interface":"127.0.0.1:9999",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "listen_interface":"127.0.0.1:9999", 
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         },
-
         {
             "is_enabled" : true,
             "host_name":"mail.disableme.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:3333",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : { 
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":false,
+				"tls_always_on":false
+			}
         }
-
-
     ]
 }
 `
@@ -106,52 +110,60 @@ var configJsonB = `
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":161,
             "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":false,
-            "tls_always_on":true,
-            "max_clients": 3
+            "max_clients": 3,
+			"tls" : {
+ 				"private_key_file":"./config_test.go",
+            	"public_key_file":"./config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         },
         {
             "is_enabled" : true,
             "host_name":"mail2.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
-            "public_key_file": "./tests/mail2.guerrillamail.com.cert.pem",
             "timeout":160,
             "listen_interface":"127.0.0.1:2527",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "log_file" : "./tests/testlog",
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"./tests/mail2.guerrillamail.com.key.pem",
+            	"public_key_file": "./tests/mail2.guerrillamail.com.cert.pem",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
 
         {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size":1000001,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":180,
             "listen_interface":"127.0.0.1:4654",
-            "start_tls_on":false,
-            "tls_always_on":false,
-            "max_clients":1
+            "max_clients":1,
+			"tls" : {
+				"private_key_file":"config_test.go",
+				"public_key_file":"config_test.go",
+				"start_tls_on":false,
+            	"tls_always_on":false
+			}
         },
 
         {
             "is_enabled" : false,
             "host_name":"mail.disbaleme.com",
             "max_size": 100017,
-            "private_key_file":"config_test.go",
-            "public_key_file":"config_test.go",
             "timeout":160,
             "listen_interface":"127.0.0.1:3333",
-            "start_tls_on":true,
-            "tls_always_on":false,
-            "max_clients": 2
+            "max_clients": 2,
+			"tls" : {
+				"private_key_file":"config_test.go",
+            	"public_key_file":"config_test.go",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         }
     ]
 }
@@ -169,8 +181,8 @@ func TestConfigLoad(t *testing.T) {
 		t.SkipNow()
 	}
 	// did we got the timestamps?
-	if ac.Servers[0]._privateKeyFile_mtime <= 0 {
-		t.Error("failed to read timestamp for _privateKeyFile_mtime, got", ac.Servers[0]._privateKeyFile_mtime)
+	if ac.Servers[0].TLS._privateKeyFile_mtime <= 0 {
+		t.Error("failed to read timestamp for _privateKeyFile_mtime, got", ac.Servers[0].TLS._privateKeyFile_mtime)
 	}
 }
 
@@ -209,8 +221,8 @@ func TestConfigChangeEvents(t *testing.T) {
 	// simulate timestamp change
 
 	time.Sleep(time.Second + time.Millisecond*500)
-	os.Chtimes(oldconf.Servers[1].PrivateKeyFile, time.Now(), time.Now())
-	os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now())
+	os.Chtimes(oldconf.Servers[1].TLS.PrivateKeyFile, time.Now(), time.Now())
+	os.Chtimes(oldconf.Servers[1].TLS.PublicKeyFile, time.Now(), time.Now())
 	newconf := &AppConfig{}
 	newconf.Load([]byte(configJsonB))
 	newconf.Servers[0].LogFile = log.OutputOff.String() // test for log file change

+ 19 - 10
glide.lock

@@ -1,5 +1,5 @@
-hash: ab5586e1ee56f15336e425d99f774acd4f6bc0f042ab597248366592d8c0b1bf
-updated: 2018-03-11T11:39:28.566276841+11:00
+hash: 9d467015838b5163cc5f1ebb92fa68fd4721445b2dbf236a03d002c92b4ba52a
+updated: 2018-12-24T15:20:01.207901397Z
 imports:
 - name: github.com/asaskevich/EventBus
   version: 68a521d7cbbb7a859c2608b06342f384b3bd5f5a
@@ -9,25 +9,29 @@ imports:
   - internal
   - redis
 - name: github.com/go-sql-driver/mysql
-  version: a0583e0143b1624142adab07e0e97fe106d99561
+  version: 72cd26f257d44c1114970e19afddcd812016007e
+- name: github.com/gomodule/redigo
+  version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
+  subpackages:
+  - redis
 - name: github.com/inconshreveable/mousetrap
   version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
 - name: github.com/rakyll/statik
-  version: 274df120e9065bdd08eb1120e0375e3dc1ae8465
+  version: 79258177a57a85a8ab2eca7ce0936aad80307f4e
   subpackages:
   - fs
 - name: github.com/sirupsen/logrus
-  version: d682213848ed68c0a260ca37d6dd5ace8423f5ba
+  version: 3e01752db0189b9157070a0e1668a620f9a85da2
 - name: github.com/spf13/cobra
-  version: b62566898a99f2db9c68ed0026aa0a052e59678d
+  version: d2d81d9a96e23f0255397222bb0b4e3165e492dc
 - name: github.com/spf13/pflag
-  version: 25f8b5b07aece3207895bf19f7ab517eb3b22a40
+  version: 24fa6976df40757dce6aea913e7b81ade90530e1
 - name: golang.org/x/crypto
-  version: c7dcf104e3a7a1417abc0230cb0d5240d764159d
+  version: 505ab145d0a99da450461ae2c1a9f6cd10d1f447
   subpackages:
   - ssh/terminal
 - name: golang.org/x/net
-  version: d0aafc73d5cdc42264b0af071c261abac580695e
+  version: 927f97764cc334a6575f4b7a1584a147864d5723
   subpackages:
   - html
   - html/atom
@@ -38,7 +42,7 @@ imports:
   - unix
   - windows
 - name: golang.org/x/text
-  version: b7ef84aaf62aa3e70962625c80a571ae7c17cb40
+  version: 17bcc049122f272a32787ba38073ee47433023e9
   subpackages:
   - encoding
   - encoding/charmap
@@ -51,11 +55,16 @@ imports:
   - encoding/traditionalchinese
   - encoding/unicode
   - internal/language
+  - internal/language/compact
   - internal/tag
   - internal/utf8internal
   - language
   - runes
   - transform
+- name: google.golang.org/appengine
+  version: e9657d882bb81064595ca3b56cbe2546bbabf7b1
+  subpackages:
+  - cloudsql
 - name: gopkg.in/iconv.v1
   version: 16a760eb7e186ae0e3aedda00d4a1daa4d0701d8
 testImports: []

+ 2 - 2
glide.yaml

@@ -2,8 +2,8 @@ package: github.com/flashmob/go-guerrilla
 import:
 - package: github.com/sirupsen/logrus
   version: ~1.0.4
-- package: github.com/garyburd/redigo
-  version: ~1.0.0
+- package: github.com/gomodule/redigo
+  version: ~2.0.0
   subpackages:
   - redis
 - package: github.com/spf13/cobra

+ 22 - 10
goguerrilla.conf.sample

@@ -22,27 +22,39 @@
             "is_enabled" : true,
             "host_name":"mail.test.com",
             "max_size": 1000000,
-            "private_key_file":"/path/to/pem/file/test.com.key",
-            "public_key_file":"/path/to/pem/file/test.com.crt",
             "timeout":180,
             "listen_interface":"127.0.0.1:25",
-            "start_tls_on":true,
-            "tls_always_on":false,
             "max_clients": 1000,
-            "log_file" : "stderr"
+            "log_file" : "stderr",
+            "tls" : {
+                "start_tls_on":true,
+                "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"],
+                "curves" : ["P256", "P384", "P521", "X25519"],
+                "client_auth_type" : "NoClientCert"
+            }
         },
         {
             "is_enabled" : false,
             "host_name":"mail.test.com",
             "max_size":1000000,
-            "private_key_file":"/path/to/pem/file/test.com.key",
-            "public_key_file":"/path/to/pem/file/test.com.crt",
             "timeout":180,
             "listen_interface":"127.0.0.1:465",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":500,
-            "log_file" : "stderr"
+            "log_file" : "stderr",
+            "tls" : {
+                "private_key_file":"/path/to/pem/file/test.com.key",
+                "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"],
+                 "curves" : ["P256", "P384", "P521", "X25519"],
+                 "client_auth_type" : "NoClientCert"
+            }
         }
     ]
 }

+ 1 - 0
guerrilla.go

@@ -247,6 +247,7 @@ func (g *guerrilla) subscribeEvents() {
 	// server config was updated
 	g.Subscribe(EventConfigServerConfig, func(sc *ServerConfig) {
 		g.setServerConfig(sc)
+		g.mainlog().Infof("server %s config change event, a new config has been saved", sc.ListenInterface)
 	})
 
 	// add a new server to the config & start

+ 8 - 0
mail/envelope.go

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

+ 595 - 0
mail/rfc5321/parse.go

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

+ 583 - 0
mail/rfc5321/parse_test.go

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

+ 109 - 93
response/enhanced.go

@@ -22,6 +22,9 @@ const (
 	ClassPermanentFailure = 5
 )
 
+// space char
+const SP = " "
+
 // class is a type for ClassSuccess, ClassTransientFailure and ClassPermanentFailure constants
 type class int
 
@@ -118,39 +121,39 @@ var (
 type Responses struct {
 
 	// The 500's
-	FailLineTooLong              string
-	FailNestedMailCmd            string
-	FailNoSenderDataCmd          string
-	FailNoRecipientsDataCmd      string
-	FailUnrecognizedCmd          string
-	FailMaxUnrecognizedCmd       string
-	FailReadLimitExceededDataCmd string
-	FailMessageSizeExceeded      string
-	FailReadErrorDataCmd         string
-	FailPathTooLong              string
-	FailInvalidAddress           string
-	FailLocalPartTooLong         string
-	FailDomainTooLong            string
-	FailBackendNotRunning        string
-	FailBackendTransaction       string
-	FailBackendTimeout           string
-	FailRcptCmd                  string
+	FailLineTooLong              *Response
+	FailNestedMailCmd            *Response
+	FailNoSenderDataCmd          *Response
+	FailNoRecipientsDataCmd      *Response
+	FailUnrecognizedCmd          *Response
+	FailMaxUnrecognizedCmd       *Response
+	FailReadLimitExceededDataCmd *Response
+	FailMessageSizeExceeded      *Response
+	FailReadErrorDataCmd         *Response
+	FailPathTooLong              *Response
+	FailInvalidAddress           *Response
+	FailLocalPartTooLong         *Response
+	FailDomainTooLong            *Response
+	FailBackendNotRunning        *Response
+	FailBackendTransaction       *Response
+	FailBackendTimeout           *Response
+	FailRcptCmd                  *Response
 
 	// The 400's
-	ErrorTooManyRecipients string
-	ErrorRelayDenied       string
-	ErrorShutdown          string
+	ErrorTooManyRecipients *Response
+	ErrorRelayDenied       *Response
+	ErrorShutdown          *Response
 
 	// The 200's
-	SuccessMailCmd       string
-	SuccessRcptCmd       string
-	SuccessResetCmd      string
-	SuccessVerifyCmd     string
-	SuccessNoopCmd       string
-	SuccessQuitCmd       string
-	SuccessDataCmd       string
-	SuccessStartTLSCmd   string
-	SuccessMessageQueued string
+	SuccessMailCmd       *Response
+	SuccessRcptCmd       *Response
+	SuccessResetCmd      *Response
+	SuccessVerifyCmd     *Response
+	SuccessNoopCmd       *Response
+	SuccessQuitCmd       *Response
+	SuccessDataCmd       *Response
+	SuccessStartTLSCmd   *Response
+	SuccessMessageQueued *Response
 }
 
 // Called automatically during package load to build up the Responses struct
@@ -158,191 +161,195 @@ func init() {
 
 	Canned = Responses{}
 
-	Canned.FailLineTooLong = (&Response{
+	Canned.FailLineTooLong = &Response{
 		EnhancedCode: InvalidCommand,
 		BasicCode:    554,
 		Class:        ClassPermanentFailure,
 		Comment:      "Line too long.",
-	}).String()
+	}
 
-	Canned.FailNestedMailCmd = (&Response{
+	Canned.FailNestedMailCmd = &Response{
 		EnhancedCode: InvalidCommand,
 		BasicCode:    503,
 		Class:        ClassPermanentFailure,
 		Comment:      "Error: nested MAIL command",
-	}).String()
+	}
 
-	Canned.SuccessMailCmd = (&Response{
+	Canned.SuccessMailCmd = &Response{
 		EnhancedCode: OtherAddressStatus,
 		Class:        ClassSuccess,
-	}).String()
+	}
 
-	Canned.SuccessRcptCmd = (&Response{
+	Canned.SuccessRcptCmd = &Response{
 		EnhancedCode: DestinationMailboxAddressValid,
 		Class:        ClassSuccess,
-	}).String()
+	}
 
 	Canned.SuccessResetCmd = Canned.SuccessMailCmd
-	Canned.SuccessNoopCmd = (&Response{
+
+	Canned.SuccessNoopCmd = &Response{
 		EnhancedCode: OtherStatus,
 		Class:        ClassSuccess,
-	}).String()
+	}
 
-	Canned.SuccessVerifyCmd = (&Response{
+	Canned.SuccessVerifyCmd = &Response{
 		EnhancedCode: OtherOrUndefinedProtocolStatus,
 		BasicCode:    252,
 		Class:        ClassSuccess,
 		Comment:      "Cannot verify user",
-	}).String()
+	}
 
-	Canned.ErrorTooManyRecipients = (&Response{
+	Canned.ErrorTooManyRecipients = &Response{
 		EnhancedCode: TooManyRecipients,
 		BasicCode:    452,
 		Class:        ClassTransientFailure,
 		Comment:      "Too many recipients",
-	}).String()
+	}
 
-	Canned.ErrorRelayDenied = (&Response{
+	Canned.ErrorRelayDenied = &Response{
 		EnhancedCode: BadDestinationMailboxAddress,
 		BasicCode:    454,
 		Class:        ClassTransientFailure,
-		Comment:      "Error: Relay access denied: ",
-	}).String()
+		Comment:      "Error: Relay access denied:",
+	}
 
-	Canned.SuccessQuitCmd = (&Response{
+	Canned.SuccessQuitCmd = &Response{
 		EnhancedCode: OtherStatus,
 		BasicCode:    221,
 		Class:        ClassSuccess,
 		Comment:      "Bye",
-	}).String()
+	}
 
-	Canned.FailNoSenderDataCmd = (&Response{
+	Canned.FailNoSenderDataCmd = &Response{
 		EnhancedCode: InvalidCommand,
 		BasicCode:    503,
 		Class:        ClassPermanentFailure,
 		Comment:      "Error: No sender",
-	}).String()
+	}
 
-	Canned.FailNoRecipientsDataCmd = (&Response{
+	Canned.FailNoRecipientsDataCmd = &Response{
 		EnhancedCode: InvalidCommand,
 		BasicCode:    503,
 		Class:        ClassPermanentFailure,
 		Comment:      "Error: No recipients",
-	}).String()
+	}
 
-	Canned.SuccessDataCmd = "354 Enter message, ending with '.' on a line by itself"
+	Canned.SuccessDataCmd = &Response{
+		BasicCode: 354,
+		Comment:   "354 Enter message, ending with '.' on a line by itself",
+	}
 
-	Canned.SuccessStartTLSCmd = (&Response{
+	Canned.SuccessStartTLSCmd = &Response{
 		EnhancedCode: OtherStatus,
 		BasicCode:    220,
 		Class:        ClassSuccess,
 		Comment:      "Ready to start TLS",
-	}).String()
+	}
 
-	Canned.FailUnrecognizedCmd = (&Response{
+	Canned.FailUnrecognizedCmd = &Response{
 		EnhancedCode: InvalidCommand,
 		BasicCode:    554,
 		Class:        ClassPermanentFailure,
 		Comment:      "Unrecognized command",
-	}).String()
+	}
 
-	Canned.FailMaxUnrecognizedCmd = (&Response{
+	Canned.FailMaxUnrecognizedCmd = &Response{
 		EnhancedCode: InvalidCommand,
 		BasicCode:    554,
 		Class:        ClassPermanentFailure,
 		Comment:      "Too many unrecognized commands",
-	}).String()
+	}
 
-	Canned.ErrorShutdown = (&Response{
+	Canned.ErrorShutdown = &Response{
 		EnhancedCode: OtherOrUndefinedMailSystemStatus,
 		BasicCode:    421,
 		Class:        ClassTransientFailure,
 		Comment:      "Server is shutting down. Please try again later. Sayonara!",
-	}).String()
+	}
 
-	Canned.FailReadLimitExceededDataCmd = (&Response{
+	Canned.FailReadLimitExceededDataCmd = &Response{
 		EnhancedCode: SyntaxError,
 		BasicCode:    550,
 		Class:        ClassPermanentFailure,
-		Comment:      "Error: ",
-	}).String()
+		Comment:      "Error:",
+	}
 
-	Canned.FailMessageSizeExceeded = (&Response{
+	Canned.FailMessageSizeExceeded = &Response{
 		EnhancedCode: SyntaxError,
 		BasicCode:    550,
 		Class:        ClassPermanentFailure,
-		Comment:      "Error: ",
-	}).String()
+		Comment:      "Error:",
+	}
 
-	Canned.FailReadErrorDataCmd = (&Response{
+	Canned.FailReadErrorDataCmd = &Response{
 		EnhancedCode: OtherOrUndefinedMailSystemStatus,
 		BasicCode:    451,
 		Class:        ClassTransientFailure,
-		Comment:      "Error: ",
-	}).String()
+		Comment:      "Error:",
+	}
 
-	Canned.FailPathTooLong = (&Response{
+	Canned.FailPathTooLong = &Response{
 		EnhancedCode: InvalidCommandArguments,
 		BasicCode:    550,
 		Class:        ClassPermanentFailure,
 		Comment:      "Path too long",
-	}).String()
+	}
 
-	Canned.FailInvalidAddress = (&Response{
+	Canned.FailInvalidAddress = &Response{
 		EnhancedCode: InvalidCommandArguments,
 		BasicCode:    501,
 		Class:        ClassPermanentFailure,
 		Comment:      "Invalid address",
-	}).String()
+	}
 
-	Canned.FailLocalPartTooLong = (&Response{
+	Canned.FailLocalPartTooLong = &Response{
 		EnhancedCode: InvalidCommandArguments,
 		BasicCode:    550,
 		Class:        ClassPermanentFailure,
 		Comment:      "Local part too long, cannot exceed 64 characters",
-	}).String()
+	}
 
-	Canned.FailDomainTooLong = (&Response{
+	Canned.FailDomainTooLong = &Response{
 		EnhancedCode: InvalidCommandArguments,
 		BasicCode:    550,
 		Class:        ClassPermanentFailure,
 		Comment:      "Domain cannot exceed 255 characters",
-	}).String()
+	}
 
-	Canned.FailBackendNotRunning = (&Response{
+	Canned.FailBackendNotRunning = &Response{
 		EnhancedCode: OtherOrUndefinedProtocolStatus,
 		BasicCode:    554,
 		Class:        ClassPermanentFailure,
-		Comment:      "Transaction failed - backend not running ",
-	}).String()
+		Comment:      "Transaction failed - backend not running",
+	}
 
-	Canned.FailBackendTransaction = (&Response{
+	Canned.FailBackendTransaction = &Response{
 		EnhancedCode: OtherOrUndefinedProtocolStatus,
 		BasicCode:    554,
 		Class:        ClassPermanentFailure,
-		Comment:      "Error: ",
-	}).String()
+		Comment:      "Error:",
+	}
 
-	Canned.SuccessMessageQueued = (&Response{
+	Canned.SuccessMessageQueued = &Response{
 		EnhancedCode: OtherStatus,
 		BasicCode:    250,
 		Class:        ClassSuccess,
-		Comment:      "OK : queued as ",
-	}).String()
+		Comment:      "OK: queued as",
+	}
 
-	Canned.FailBackendTimeout = (&Response{
+	Canned.FailBackendTimeout = &Response{
 		EnhancedCode: OtherOrUndefinedProtocolStatus,
 		BasicCode:    554,
 		Class:        ClassPermanentFailure,
 		Comment:      "Error: transaction timeout",
-	}).String()
+	}
 
-	Canned.FailRcptCmd = (&Response{
+	Canned.FailRcptCmd = &Response{
 		EnhancedCode: BadDestinationMailboxAddress,
 		BasicCode:    550,
 		Class:        ClassPermanentFailure,
 		Comment:      "User unknown in local recipient table",
-	}).String()
+	}
 
 }
 
@@ -409,6 +416,7 @@ type Response struct {
 	Class        class
 	// Comment is optional
 	Comment string
+	cached  string
 }
 
 // it looks like this ".5.4"
@@ -428,6 +436,14 @@ func (e EnhancedStatusCode) String() string {
 // String returns a custom Response as a string
 func (r *Response) String() string {
 
+	if r.cached != "" {
+		return r.cached
+	}
+	if r.EnhancedCode == "" {
+		r.cached = r.Comment
+		return r.Comment
+	}
+
 	basicCode := r.BasicCode
 	comment := r.Comment
 	if len(comment) == 0 && r.BasicCode == 0 {
@@ -447,8 +463,8 @@ func (r *Response) String() string {
 	if r.BasicCode == 0 {
 		basicCode = getBasicStatusCode(e)
 	}
-
-	return fmt.Sprintf("%d %s %s", basicCode, e.String(), comment)
+	r.cached = fmt.Sprintf("%d %s %s", basicCode, e.String(), comment)
+	return r.cached
 }
 
 // getBasicStatusCode gets the basic status code from codeMap, or fallback code if not mapped

+ 152 - 86
server.go

@@ -1,11 +1,15 @@
 package guerrilla
 
 import (
+	"bytes"
 	"crypto/rand"
 	"crypto/tls"
+	"crypto/x509"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"net"
+	"path/filepath"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -14,6 +18,7 @@ import (
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
+	"github.com/flashmob/go-guerrilla/mail/rfc5321"
 	"github.com/flashmob/go-guerrilla/response"
 )
 
@@ -22,14 +27,6 @@ const (
 	CommandLineMaxLength = 1024
 	// Number of allowed unrecognized commands before we terminate the connection
 	MaxUnrecognizedCommands = 5
-	// The maximum total length of a reverse-path or forward-path is 256
-	RFC2821LimitPath = 256
-	// The maximum total length of a user name or other local-part is 64
-	RFC2832LimitLocalPart = 64
-	//The maximum total length of a domain name or number is 255
-	RFC2821LimitDomain = 255
-	// The minimum total number of recipients that must be buffered is 100
-	RFC2821LimitRecipients = 100
 )
 
 const (
@@ -52,7 +49,7 @@ type server struct {
 	clientPool      *Pool
 	wg              sync.WaitGroup // for waiting to shutdown
 	listener        net.Listener
-	closedListener  chan (bool)
+	closedListener  chan bool
 	hosts           allowedHosts // stores map[string]bool for faster lookup
 	state           int
 	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
@@ -64,9 +61,31 @@ type server struct {
 
 type allowedHosts struct {
 	table      map[string]bool // host lookup table
+	wildcards  []string        // host wildcard list (* is used as a wildcard)
 	sync.Mutex                 // guard access to the map
 }
 
+type command []byte
+
+var (
+	cmdHELO     command = []byte("HELO")
+	cmdEHLO     command = []byte("EHLO")
+	cmdHELP     command = []byte("HELP")
+	cmdXCLIENT  command = []byte("XCLIENT")
+	cmdMAIL     command = []byte("MAIL FROM:")
+	cmdRCPT     command = []byte("RCPT TO:")
+	cmdRSET     command = []byte("RSET")
+	cmdVRFY     command = []byte("VRFY")
+	cmdNOOP     command = []byte("NOOP")
+	cmdQUIT     command = []byte("QUIT")
+	cmdDATA     command = []byte("DATA")
+	cmdSTARTTLS command = []byte("STARTTLS")
+)
+
+func (c command) match(in []byte) bool {
+	return bytes.Index(in, []byte(c)) == 0
+}
+
 // Creates and returns a new ready-to-run Server from a configuration
 func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, error) {
 	server := &server{
@@ -100,8 +119,8 @@ func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, err
 
 func (s *server) configureSSL() error {
 	sConfig := s.configStore.Load().(ServerConfig)
-	if sConfig.TLSAlwaysOn || sConfig.StartTLSOn {
-		cert, err := tls.LoadX509KeyPair(sConfig.PublicKeyFile, sConfig.PrivateKeyFile)
+	if sConfig.TLS.AlwaysOn || sConfig.TLS.StartTLSOn {
+		cert, err := tls.LoadX509KeyPair(sConfig.TLS.PublicKeyFile, sConfig.TLS.PrivateKeyFile)
 		if err != nil {
 			return fmt.Errorf("error while loading the certificate: %s", err)
 		}
@@ -110,6 +129,47 @@ func (s *server) configureSSL() error {
 			ClientAuth:   tls.VerifyClientCertIfGiven,
 			ServerName:   sConfig.Hostname,
 		}
+		if len(sConfig.TLS.Protocols) > 0 {
+			if min, ok := TLSProtocols[sConfig.TLS.Protocols[0]]; ok {
+				tlsConfig.MinVersion = min
+			}
+		}
+		if len(sConfig.TLS.Protocols) > 1 {
+			if max, ok := TLSProtocols[sConfig.TLS.Protocols[1]]; ok {
+				tlsConfig.MaxVersion = max
+			}
+		}
+		if len(sConfig.TLS.Ciphers) > 0 {
+			for _, val := range sConfig.TLS.Ciphers {
+				if c, ok := TLSCiphers[val]; ok {
+					tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c)
+				}
+			}
+		}
+		if len(sConfig.TLS.Curves) > 0 {
+			for _, val := range sConfig.TLS.Curves {
+				if c, ok := TLSCurves[val]; ok {
+					tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
+				}
+			}
+		}
+		if len(sConfig.TLS.RootCAs) > 0 {
+			caCert, err := ioutil.ReadFile(sConfig.TLS.RootCAs)
+			if err != nil {
+				s.log().WithError(err).Errorf("failed opening TLSRootCAs file [%s]", sConfig.TLS.RootCAs)
+			} else {
+				caCertPool := x509.NewCertPool()
+				caCertPool.AppendCertsFromPEM(caCert)
+				tlsConfig.RootCAs = caCertPool
+			}
+
+		}
+		if len(sConfig.TLS.ClientAuthType) > 0 {
+			if ca, ok := TLSClientAuthTypes[sConfig.TLS.ClientAuthType]; ok {
+				tlsConfig.ClientAuth = ca
+			}
+		}
+		tlsConfig.PreferServerCipherSuites = sConfig.TLS.PreferServerCipherSuites
 		tlsConfig.Rand = rand.Reader
 		s.tlsConfigStore.Store(tlsConfig)
 	}
@@ -152,8 +212,13 @@ func (server *server) setAllowedHosts(allowedHosts []string) {
 	server.hosts.Lock()
 	defer server.hosts.Unlock()
 	server.hosts.table = make(map[string]bool, len(allowedHosts))
+	server.hosts.wildcards = nil
 	for _, h := range allowedHosts {
-		server.hosts.table[strings.ToLower(h)] = true
+		if strings.Index(h, "*") != -1 {
+			server.hosts.wildcards = append(server.hosts.wildcards, strings.ToLower(h))
+		} else {
+			server.hosts.table[strings.ToLower(h)] = true
+		}
 	}
 }
 
@@ -235,6 +300,7 @@ func (server *server) GetActiveClientsCount() int {
 func (server *server) allowsHost(host string) bool {
 	server.hosts.Lock()
 	defer server.hosts.Unlock()
+	// if hosts contains a single dot, further processing is skipped
 	if len(server.hosts.table) == 1 {
 		if _, ok := server.hosts.table["."]; ok {
 			return true
@@ -243,30 +309,31 @@ func (server *server) allowsHost(host string) bool {
 	if _, ok := server.hosts.table[strings.ToLower(host)]; ok {
 		return true
 	}
+	// check the willdcards
+	for _, w := range server.hosts.wildcards {
+		if matched, err := filepath.Match(w, strings.ToLower(host)); matched && err == nil {
+			return true
+		}
+	}
 	return false
 }
 
-// Reads from the client until a terminating sequence is encountered,
+const commandSuffix = "\r\n"
+
+// Reads from the client until a \n terminator is encountered,
 // or until a timeout occurs.
-func (server *server) readCommand(client *client, maxSize int64) (string, error) {
-	var input, reply string
+func (server *server) readCommand(client *client) ([]byte, error) {
+	//var input string
 	var err error
+	var bs []byte
 	// In command state, stop reading at line breaks
-	suffix := "\r\n"
-	for {
-		client.setTimeout(server.timeout.Load().(time.Duration))
-		reply, err = client.bufin.ReadString('\n')
-		input = input + reply
-		if err != nil {
-			break
-		}
-		if strings.HasSuffix(input, suffix) {
-			// discard the suffix and stop reading
-			input = input[0 : len(input)-len(suffix)]
-			break
-		}
+	bs, err = client.bufin.ReadSlice('\n')
+	if err != nil {
+		return bs, err
+	} else if bytes.HasSuffix(bs, []byte(commandSuffix)) {
+		return bs[:len(bs)-2], err
 	}
-	return input, err
+	return bs[:len(bs)-1], err
 }
 
 // flushResponse a response to the client. Flushes the client.bufout buffer to the connection
@@ -303,7 +370,7 @@ func (server *server) handleClient(client *client) {
 	// Also, Last line has no dash -
 	help := "250 HELP"
 
-	if sc.TLSAlwaysOn {
+	if sc.TLS.AlwaysOn {
 		tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 		if !ok {
 			server.mainlog().Error("Failed to load *tls.Config")
@@ -315,7 +382,7 @@ func (server *server) handleClient(client *client) {
 			client.kill()
 		}
 	}
-	if !sc.StartTLSOn {
+	if !sc.TLS.StartTLSOn {
 		// STARTTLS turned off, don't advertise it
 		advertiseTLS = ""
 	}
@@ -327,7 +394,7 @@ func (server *server) handleClient(client *client) {
 			client.state = ClientCmd
 		case ClientCmd:
 			client.bufin.setLimit(CommandLineMaxLength)
-			input, err := server.readCommand(client, sc.MaxSize)
+			input, err := server.readCommand(client)
 			server.log().Debugf("Client sent: %s", input)
 			if err == io.EOF {
 				server.log().WithError(err).Warnf("Client closed the connection: %s", client.RemoteIP)
@@ -349,20 +416,19 @@ func (server *server) handleClient(client *client) {
 				continue
 			}
 
-			input = strings.Trim(input, " \r\n")
 			cmdLen := len(input)
 			if cmdLen > CommandVerbMaxLength {
 				cmdLen = CommandVerbMaxLength
 			}
-			cmd := strings.ToUpper(input[:cmdLen])
+			cmd := bytes.ToUpper(input[:cmdLen])
 			switch {
-			case strings.Index(cmd, "HELO") == 0:
-				client.Helo = strings.Trim(input[4:], " ")
+			case cmdHELO.match(cmd):
+				client.Helo = string(bytes.Trim(input[4:], " "))
 				client.resetTransaction()
 				client.sendResponse(helo)
 
-			case strings.Index(cmd, "EHLO") == 0:
-				client.Helo = strings.Trim(input[4:], " ")
+			case cmdEHLO.match(cmd):
+				client.Helo = string(bytes.Trim(input[4:], " "))
 				client.resetTransaction()
 				client.sendResponse(ehlo,
 					messageSize,
@@ -371,88 +437,83 @@ func (server *server) handleClient(client *client) {
 					advertiseEnhancedStatusCodes,
 					help)
 
-			case strings.Index(cmd, "HELP") == 0:
+			case cmdHELP.match(cmd):
 				quote := response.GetQuote()
-				client.sendResponse("214-OK\r\n" + quote)
+				client.sendResponse("214-OK\r\n", quote)
 
-			case sc.XClientOn && strings.Index(cmd, "XCLIENT ") == 0:
-				if toks := strings.Split(input[8:], " "); len(toks) > 0 {
+			case sc.XClientOn && cmdXCLIENT.match(cmd):
+				if toks := bytes.Split(input[8:], []byte{' '}); len(toks) > 0 {
 					for i := range toks {
-						if vals := strings.Split(toks[i], "="); len(vals) == 2 {
-							if vals[1] == "[UNAVAILABLE]" {
+						if vals := bytes.Split(toks[i], []byte{'='}); len(vals) == 2 {
+							if bytes.Compare(vals[1], []byte("[UNAVAILABLE]")) == 0 {
 								// skip
 								continue
 							}
-							if vals[0] == "ADDR" {
-								client.RemoteIP = vals[1]
+							if bytes.Compare(vals[0], []byte("ADDR")) == 0 {
+								client.RemoteIP = string(vals[1])
 							}
-							if vals[0] == "HELO" {
-								client.Helo = vals[1]
+							if bytes.Compare(vals[0], []byte("HELO")) == 0 {
+								client.Helo = string(vals[1])
 							}
 						}
 					}
 				}
 				client.sendResponse(response.Canned.SuccessMailCmd)
-			case strings.Index(cmd, "MAIL FROM:") == 0:
+			case cmdMAIL.match(cmd):
 				if client.isInTransaction() {
 					client.sendResponse(response.Canned.FailNestedMailCmd)
 					break
 				}
-				addr := input[10:]
-				if !(strings.Index(addr, "<>") == 0) &&
-					!(strings.Index(addr, " <>") == 0) {
-					// Not Bounce, extract mail.
-					if from, err := extractEmail(addr); err != nil {
-						client.sendResponse(err)
-						break
-					} else {
-						client.MailFrom = from
-					}
-
-				} else {
+				client.MailFrom, err = client.parsePath([]byte(input[10:]), client.parser.MailFrom)
+				if err != nil {
+					server.log().WithError(err).Error("MAIL parse error", "["+string(input[10:])+"]")
+					client.sendResponse(err)
+					break
+				} else if client.parser.NullPath {
 					// bounce has empty from address
 					client.MailFrom = mail.Address{}
 				}
 				client.sendResponse(response.Canned.SuccessMailCmd)
 
-			case strings.Index(cmd, "RCPT TO:") == 0:
-				if len(client.RcptTo) > RFC2821LimitRecipients {
+			case cmdRCPT.match(cmd):
+				if len(client.RcptTo) > rfc5321.LimitRecipients {
 					client.sendResponse(response.Canned.ErrorTooManyRecipients)
 					break
 				}
-				to, err := extractEmail(input[8:])
+				to, err := client.parsePath([]byte(input[8:]), client.parser.RcptTo)
 				if err != nil {
+					server.log().WithError(err).Error("RCPT parse error", "["+string(input[8:])+"]")
 					client.sendResponse(err.Error())
+					break
+				}
+				if !server.allowsHost(to.Host) {
+					client.sendResponse(response.Canned.ErrorRelayDenied, " ", to.Host)
 				} else {
-					if !server.allowsHost(to.Host) {
-						client.sendResponse(response.Canned.ErrorRelayDenied, to.Host)
+					client.PushRcpt(to)
+					rcptError := server.backend().ValidateRcpt(client.Envelope)
+					if rcptError != nil {
+						client.PopRcpt()
+						client.sendResponse(response.Canned.FailRcptCmd, " ", rcptError.Error())
 					} else {
-						client.PushRcpt(to)
-						rcptError := server.backend().ValidateRcpt(client.Envelope)
-						if rcptError != nil {
-							client.PopRcpt()
-							client.sendResponse(response.Canned.FailRcptCmd + " " + rcptError.Error())
-						} else {
-							client.sendResponse(response.Canned.SuccessRcptCmd)
-						}
+						client.sendResponse(response.Canned.SuccessRcptCmd)
 					}
 				}
 
-			case strings.Index(cmd, "RSET") == 0:
+			case cmdRSET.match(cmd):
 				client.resetTransaction()
 				client.sendResponse(response.Canned.SuccessResetCmd)
 
-			case strings.Index(cmd, "VRFY") == 0:
+			case cmdVRFY.match(cmd):
 				client.sendResponse(response.Canned.SuccessVerifyCmd)
 
-			case strings.Index(cmd, "NOOP") == 0:
+			case cmdNOOP.match(cmd):
 				client.sendResponse(response.Canned.SuccessNoopCmd)
 
-			case strings.Index(cmd, "QUIT") == 0:
+			case cmdQUIT.match(cmd):
 				client.sendResponse(response.Canned.SuccessQuitCmd)
 				client.kill()
 
-			case strings.Index(cmd, "DATA") == 0:
+			case cmdDATA.match(cmd):
 				if len(client.RcptTo) == 0 {
 					client.sendResponse(response.Canned.FailNoRecipientsDataCmd)
 					break
@@ -460,7 +521,7 @@ func (server *server) handleClient(client *client) {
 				client.sendResponse(response.Canned.SuccessDataCmd)
 				client.state = ClientData
 
-			case sc.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
+			case sc.TLS.StartTLSOn && cmdSTARTTLS.match(cmd):
 
 				client.sendResponse(response.Canned.SuccessStartTLSCmd)
 				client.state = ClientStartTLS
@@ -486,13 +547,13 @@ func (server *server) handleClient(client *client) {
 			}
 			if err != nil {
 				if err == LineLimitExceeded {
-					client.sendResponse(response.Canned.FailReadLimitExceededDataCmd, LineLimitExceeded.Error())
+					client.sendResponse(response.Canned.FailReadLimitExceededDataCmd, " ", LineLimitExceeded.Error())
 					client.kill()
 				} else if err == MessageSizeExceeded {
-					client.sendResponse(response.Canned.FailMessageSizeExceeded, MessageSizeExceeded.Error())
+					client.sendResponse(response.Canned.FailMessageSizeExceeded, " ", MessageSizeExceeded.Error())
 					client.kill()
 				} else {
-					client.sendResponse(response.Canned.FailReadErrorDataCmd, err.Error())
+					client.sendResponse(response.Canned.FailReadErrorDataCmd, " ", err.Error())
 					client.kill()
 				}
 				server.log().WithError(err).Warn("Error reading data")
@@ -504,7 +565,7 @@ func (server *server) handleClient(client *client) {
 			if res.Code() < 300 {
 				client.messagesSent++
 			}
-			client.sendResponse(res.String())
+			client.sendResponse(res)
 			client.state = ClientCmd
 			if server.isShuttingDown() {
 				client.state = ClientShutdown
@@ -512,7 +573,7 @@ func (server *server) handleClient(client *client) {
 			client.resetTransaction()
 
 		case ClientStartTLS:
-			if !client.TLS && sc.StartTLSOn {
+			if !client.TLS && sc.TLS.StartTLSOn {
 				tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 				if !ok {
 					server.mainlog().Error("Failed to load *tls.Config")
@@ -532,13 +593,18 @@ func (server *server) handleClient(client *client) {
 			client.kill()
 		}
 
+		if client.bufErr != nil {
+			server.log().WithError(client.bufErr).Debug("client could not buffer a response")
+			return
+		}
+		// flush the response buffer
 		if client.bufout.Buffered() > 0 {
 			if server.log().IsDebug() {
 				server.log().Debugf("Writing response to client: \n%s", client.response.String())
 			}
 			err := server.flushResponse(client)
 			if err != nil {
-				server.log().WithError(err).Debug("Error writing response")
+				server.log().WithError(err).Debug("error writing response")
 				return
 			}
 		}

+ 205 - 9
server_test.go

@@ -8,26 +8,31 @@ import (
 	"strings"
 	"sync"
 
+	"crypto/tls"
 	"fmt"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
 	"github.com/flashmob/go-guerrilla/mocks"
+	"io/ioutil"
 	"net"
+	"os"
 )
 
 // getMockServerConfig gets a mock ServerConfig struct used for creating a new server
 func getMockServerConfig() *ServerConfig {
 	sc := &ServerConfig{
-		IsEnabled:       true, // not tested here
-		Hostname:        "saggydimes.test.com",
-		MaxSize:         1024, // smtp message max size
-		PrivateKeyFile:  "./tests/mail.guerrillamail.com.key.pem",
-		PublicKeyFile:   "./tests/mail.guerrillamail.com.cert.pem",
+		IsEnabled: true, // not tested here
+		Hostname:  "saggydimes.test.com",
+		MaxSize:   1024, // smtp message max size
+		TLS: ServerTLSConfig{
+			PrivateKeyFile: "./tests/mail.guerrillamail.com.key.pem",
+			PublicKeyFile:  "./tests/mail.guerrillamail.com.cert.pem",
+			StartTLSOn:     true,
+			AlwaysOn:       false,
+		},
 		Timeout:         5,
 		ListenInterface: "127.0.0.1:2529",
-		StartTLSOn:      true,
-		TLSAlwaysOn:     false,
 		MaxClients:      30, // not tested here
 		LogFile:         "./tests/testlog",
 	}
@@ -60,6 +65,150 @@ func getMockServerConn(sc *ServerConfig, t *testing.T) (*mocks.Conn, *server) {
 	return conn, server
 }
 
+// test the RootCAs tls config setting
+var rootCAPK = `-----BEGIN CERTIFICATE-----
+MIIDqjCCApKgAwIBAgIJALh2TrsBR5MiMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8G
+A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv
+c3QwIBcNMTgwNTE4MDYzOTU2WhgPMjExODA0MjQwNjM5NTZaMGkxCzAJBgNVBAYT
+AlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8GA1UE
+CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3Qw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCcb0ulYT1o5ysor5UtWYW
+q/ZY3PyK3/4YBZq5JoX4xk7GNQQ+3p/Km7QPoBXfgjFLZXEV2R0bE5hHMXfLa5Xb
+64acb9VqCqDvPFXcaNP4rEdBKDVN2p0PEi917tcKBSrZn5Yl+iOhtcBpQDvhHgn/
+9MdmIAKB3+yK+4l9YhT40XfDXCQqzfg4XcNaEgTzZHcDJz+KjWJuJChprcx27MTI
+Ndxs9nmFA2rK16rjgjtwjZ4t9dXsljdOcx59s6dIQ0GnEM8qdKxi/vEx4+M/hbGf
+v7H75LsuKRrVJINAmfy9fmc6VAXjFU0ZVxGK5eVnzsh/hY08TSSrlCCKAJpksjJz
+AgMBAAGjUzBRMB0GA1UdDgQWBBSZsYWs+8FYe4z4c6LLmFB4TeeV/jAfBgNVHSME
+GDAWgBSZsYWs+8FYe4z4c6LLmFB4TeeV/jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
+SIb3DQEBCwUAA4IBAQAcXt/FaILkOCMj8bTUx42vi2N9ZTiEuRbYi24IyGOokbDR
+pSsIxiz+HDPUuX6/X/mHidl24aS9wdv5JTXMr44/BeGK1WC7gMueZBxAqONpaG1Q
+VU0e3q1YwXKcupKQ7kVWl0fuY3licv0+s4zBcTLKkmWAYqsb/n0KtCMyqewi+Rqa
+Zj5Z3OcWOq9Ad9fZWKcG8k/sgeTk9z0X1mZcEyWWxqsUmxvN+SdWLoug1xJVVbMN
+CipZ0vBIi9KOhQgzuIFhoTcd6myUtov52/EFqlX6UuFpY2gEWw/f/yu+SI08v4w9
+KwxgAKBkhx2JYZKtu1EsPIMDyS0aahcDnHqnrGAi
+-----END CERTIFICATE-----`
+
+var clientPrvKey = `-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA5ZLmMBdKkVyVmN0VhDSFGvgKp24ejHPCv+wfuf3vlU9cwKfH
+R3vejleZAVRcidscfA0Jsub/Glsr0XwecagtpvTI+Fp1ik6sICOz+VW3958qaAi8
+TjbUMjcDHJeSLcjr725CH5uIvhRzR+daYaJQhAcL2MEt8M9WIF6AjtDZEH9R6oM8
+t5FkO0amImlnipYXNBFghmzkZzfGXXRQLw2A+u6keLcjCrn9h2BaofGIjQfYcu/3
+fH4cIFR4z/soGKameqnCUz7dWmbf4tAI+8QR0VXXBKhiHDm98tPSeH994hC52Uul
+rjEVcM5Uox5hazS2PK06oSc1YuFZONqeeGqj6wIDAQABAoIBADERzRHKaK3ZVEBw
+QQEZGLpC+kP/TZhHxgCvv7hJhsQrSnADbJzi5RcXsiSOm5j7tILvZntO1IgVpLAK
+D5fLkrZ069/pteXyGuhjuTw6DjBnXPEPrPAq2ABDse6SlzQiFgv/TTLkU74NMPbV
+hIQJ5ZvSxb12zRMDviz9Bg2ApmTX6k2iPjQBnEHgKzb64IdMcEb5HE1qNt0v0lRA
+sGBMZZKQWbt2m0pSbAbnB3S9GcpJkRgFFMdTaUScIWO6ICT2hBP2pw2/4M2Zrmlt
+bsyWu9uswBzhvu+/pg2E66V6mji0EzDMlXqjlO5jro6t7P33t1zkd/i/ykKmtDLp
+IpR94UECgYEA9Y4EIjOyaBWJ6TRQ6a/tehGPbwIOgvEiTYXRJqdU49qn/i4YZjSm
+F4iibJz+JeOIQXSwa9F7gRlaspIuHgIJoer7BrITMuhr+afqMLkxK0pijul/qAbm
+HdpFn8IxjpNu4/GoAENbEVy50SMST9yWh5ulEkHHftd4/NJKoJQ2PZ8CgYEA71bb
+lFVh1MFclxRKECmpyoqUAzwGlMoHJy/jaBYuWG4X7rzxqDRrgPH3as6gXpRiSZ+K
+5fC+wcU7dKnHtJOkBDk6J5ev2+hbwg+yq3w4+l3bPDvf2TJyXjXjRDZo12pxFD58
+ybCOF6ItbIDXqT5pvo3PMjgMwu1Ycie+h6hA3jUCgYEAsq93XpQT/R2/T44cWxEE
+VFG2+GacvLhP5+26ttAJPA1/Nb3BT458Vp+84iCT6GpcWpVZU/wKTXVvxIYPPRLq
+g4MEzGiFBASRngiMqIv6ta/ZbHmJxXHPvmV5SLn9aezrQsA1KovZFxdMuF03FBpH
+B8NBKbnoO+r8Ra2ZVKTFm60CgYAZw8Dpi/N3IsWj4eRDLyj/C8H5Qyn2NHVmq4oQ
+d2rPzDI5Wg+tqs7z15hp4Ap1hAW8pTcfn7X5SBEpculzr/0VE1AGWRbuVmoiTuxN
+95ZupVHnfw6O5BZZu/VWL4FDx0qbAksOrznso2G+b3RH3NcnUz69yjjddw1xZIPn
+OJ6bDQKBgDUcWYu/2amU18D5vJpppUgRq2084WPUeXsaniTbmWfOC8NAn8CKLY0N
+V4yGSu98apDuqEVqL0VFQEgqK+5KTvRdXXYi36XYRbbVUgV13xveq2YTvjNbPM60
+QWG9YmgH7hVYGusuh5nQeS0qiIpwyws2H5mBVrGXrQ1Xb0MLWj8/
+-----END RSA PRIVATE KEY-----`
+
+// signed using the Root (rootCAPK)
+var clientPubKey = `-----BEGIN CERTIFICATE-----
+MIIDWDCCAkACCQCHoh4OvUySOzANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJV
+UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MCAX
+DTE4MDUxODA2NDQ0NVoYDzMwMTcwOTE4MDY0NDQ1WjBxMQswCQYDVQQGEwJVUzET
+MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8G
+A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv
+c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlkuYwF0qRXJWY3RWE
+NIUa+Aqnbh6Mc8K/7B+5/e+VT1zAp8dHe96OV5kBVFyJ2xx8DQmy5v8aWyvRfB5x
+qC2m9Mj4WnWKTqwgI7P5Vbf3nypoCLxONtQyNwMcl5ItyOvvbkIfm4i+FHNH51ph
+olCEBwvYwS3wz1YgXoCO0NkQf1Hqgzy3kWQ7RqYiaWeKlhc0EWCGbORnN8ZddFAv
+DYD67qR4tyMKuf2HYFqh8YiNB9hy7/d8fhwgVHjP+ygYpqZ6qcJTPt1aZt/i0Aj7
+xBHRVdcEqGIcOb3y09J4f33iELnZS6WuMRVwzlSjHmFrNLY8rTqhJzVi4Vk42p54
+aqPrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIQmlo8iCpyYggkbpfDmThBPHfy1
+cZcCi/tRFoFe1ccwn2ezLMIKmW38ZebiroawwqrZgU6AP+dMxVKLMjpyLPSrpFKa
+3o/LbVF7qMfH8/y2q8t7javd6rxoENH9uxLyHhauzI1iWy0whoDWBNiZrPBTBCjq
+jDGZARZqGyrPeXi+RNe1cMvZCxAFy7gqEtWFLWWrp0gYNPvxkHhhQBrUcF+8T/Nf
+9G4hKZSN/KAgC0CNBVuNrdyNc3l8H66BfwwL5X0+pesBYZM+MEfmBZOo+p7OWx2r
+ug8tR8eSL1vGleONtFRBUVG7NbtjhBf9FhvPZcSRR10od/vWHku9E01i4xg=
+-----END CERTIFICATE-----`
+
+func TestTLSConfig(t *testing.T) {
+
+	if err := ioutil.WriteFile("rootca.test.pem", []byte(rootCAPK), 0644); err != nil {
+		t.Fatal("couldn't create rootca.test.pem file.", err)
+		return
+	}
+	if err := ioutil.WriteFile("client.test.key", []byte(clientPrvKey), 0644); err != nil {
+		t.Fatal("couldn't create client.test.key file.", err)
+		return
+	}
+	if err := ioutil.WriteFile("client.test.pem", []byte(clientPubKey), 0644); err != nil {
+		t.Fatal("couldn't create client.test.pem file.", err)
+		return
+	}
+
+	s := server{}
+	s.setConfig(&ServerConfig{
+		TLS: ServerTLSConfig{
+			StartTLSOn:     true,
+			PrivateKeyFile: "client.test.key",
+			PublicKeyFile:  "client.test.pem",
+			RootCAs:        "rootca.test.pem",
+			ClientAuthType: "NoClientCert",
+			Curves:         []string{"P521", "P384"},
+			Ciphers:        []string{"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"},
+			Protocols:      []string{"tls1.0", "tls1.2"},
+		},
+	})
+	s.configureSSL()
+
+	c := s.tlsConfigStore.Load().(*tls.Config)
+
+	if len(c.CurvePreferences) != 2 {
+		t.Error("c.CurvePreferences should have two elements")
+	} else if c.CurvePreferences[0] != tls.CurveP521 && c.CurvePreferences[1] != tls.CurveP384 {
+		t.Error("c.CurvePreferences curves not setup")
+	}
+	if strings.Index(string(c.RootCAs.Subjects()[0]), "Mountain View") == -1 {
+		t.Error("c.RootCAs not correctly set")
+	}
+	if c.ClientAuth != tls.NoClientCert {
+		t.Error("c.ClientAuth should be tls.NoClientCert")
+	}
+
+	if len(c.CipherSuites) != 2 {
+		t.Error("c.CipherSuites length should be 2")
+	}
+
+	if c.CipherSuites[0] != tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 && c.CipherSuites[1] != tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA {
+		t.Error("c.CipherSuites not correctly set ")
+	}
+
+	if c.MinVersion != tls.VersionTLS10 {
+		t.Error("c.MinVersion should be tls.VersionTLS10")
+	}
+
+	if c.MaxVersion != tls.VersionTLS12 {
+		t.Error("c.MinVersion should be tls.VersionTLS10")
+	}
+
+	if c.PreferServerCipherSuites != false {
+		t.Error("PreferServerCipherSuites should be false")
+	}
+
+	os.Remove("rootca.test.pem")
+	os.Remove("client.test.key")
+	os.Remove("client.test.pem")
+
+}
+
 func TestHandleClient(t *testing.T) {
 	var mainlog log.Logger
 	var logOpenError error
@@ -188,7 +337,7 @@ func TestGatewayTimeout(t *testing.T) {
 		for i := 0; i < 2; i++ {
 			fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
 			str, err = in.ReadString('\n')
-			fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+			fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 			str, err = in.ReadString('\n')
 			fmt.Fprint(conn, "DATA\r\n")
 			str, err = in.ReadString('\n')
@@ -249,7 +398,7 @@ func TestGatewayPanic(t *testing.T) {
 		for i := 0; i < 2; i++ {
 			fmt.Fprint(conn, "MAIL FROM:<[email protected]>r\r\n")
 			str, err = in.ReadString('\n')
-			fmt.Fprint(conn, "RCPT TO:[email protected]\r\n")
+			fmt.Fprint(conn, "RCPT TO:<[email protected]>\r\n")
 			str, err = in.ReadString('\n')
 			fmt.Fprint(conn, "DATA\r\n")
 			str, err = in.ReadString('\n')
@@ -269,5 +418,52 @@ func TestGatewayPanic(t *testing.T) {
 
 }
 
+func TestAllowsHosts(t *testing.T) {
+	s := server{}
+	allowedHosts := []string{
+		"spam4.me",
+		"grr.la",
+		"newhost.com",
+		"example.*",
+		"*.test",
+		"wild*.card",
+		"multiple*wild*cards.*",
+	}
+	s.setAllowedHosts(allowedHosts)
+
+	testTable := map[string]bool{
+		"spam4.me":                true,
+		"dont.match":              false,
+		"example.com":             true,
+		"another.example.com":     false,
+		"anything.test":           true,
+		"wild.card":               true,
+		"wild.card.com":           false,
+		"multipleXwildXcards.com": true,
+	}
+
+	for host, allows := range testTable {
+		if res := s.allowsHost(host); res != allows {
+			t.Error(host, ": expected", allows, "but got", res)
+		}
+	}
+
+	// only wildcard - should match anything
+	s.setAllowedHosts([]string{"*"})
+	if !s.allowsHost("match.me") {
+		t.Error("match.me: expected true but got false")
+	}
+
+	// turns off
+	s.setAllowedHosts([]string{"."})
+	if !s.allowsHost("match.me") {
+		t.Error("match.me: expected true but got false")
+	}
+
+	// no wilcards
+	s.setAllowedHosts([]string{"grr.la", "example.com"})
+
+}
+
 // TODO
 // - test github issue #44 and #42

+ 1 - 1
tests/client.go

@@ -14,7 +14,7 @@ func Connect(serverConfig guerrilla.ServerConfig, deadline time.Duration) (net.C
 	var bufin *bufio.Reader
 	var conn net.Conn
 	var err error
-	if serverConfig.TLSAlwaysOn {
+	if serverConfig.TLS.AlwaysOn {
 		// start tls automatically
 		conn, err = tls.Dial("tcp", serverConfig.ListenInterface, &tls.Config{
 			InsecureSkipVerify: true,

+ 47 - 41
tests/guerrilla_test.go

@@ -16,6 +16,7 @@ package test
 
 import (
 	"encoding/json"
+	"github.com/flashmob/go-guerrilla/mail/rfc5321"
 	"testing"
 
 	"time"
@@ -40,7 +41,6 @@ import (
 
 type TestConfig struct {
 	guerrilla.AppConfig
-	BackendName   string                 `json:"backend_name"`
 	BackendConfig map[string]interface{} `json:"backend_config"`
 }
 
@@ -86,28 +86,32 @@ var configJson = `
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size": 100017,
-            "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
-            "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
             "timeout":160,
-            "listen_interface":"127.0.0.1:2526",
-            "start_tls_on":true,
-            "tls_always_on":false,
+            "listen_interface":"127.0.0.1:2526", 
             "max_clients": 2,
-            "log_file" : ""
+            "log_file" : "",
+			"tls" : {
+				"private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
+            	"public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
+				"start_tls_on":true,
+            	"tls_always_on":false
+			}
         },
 
         {
             "is_enabled" : true,
             "host_name":"mail.guerrillamail.com",
             "max_size":1000001,
-            "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
-            "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
             "timeout":180,
             "listen_interface":"127.0.0.1:4654",
-            "start_tls_on":false,
-            "tls_always_on":true,
             "max_clients":1,
-            "log_file" : ""
+            "log_file" : "",
+			"tls" : {
+				"private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key",
+            	"public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt",
+				"start_tls_on":false,
+            	"tls_always_on":true
+			}
         }
     ]
 }
@@ -125,8 +129,8 @@ func getBackend(backendConfig map[string]interface{}, l log.Logger) (backends.Ba
 func setupCerts(c *TestConfig) {
 	for i := range c.Servers {
 		testcert.GenerateCert(c.Servers[i].Hostname, "", 365*24*time.Hour, false, 2048, "P256", "./")
-		c.Servers[i].PrivateKeyFile = c.Servers[i].Hostname + ".key.pem"
-		c.Servers[i].PublicKeyFile = c.Servers[i].Hostname + ".cert.pem"
+		c.Servers[i].TLS.PrivateKeyFile = c.Servers[i].Hostname + ".key.pem"
+		c.Servers[i].TLS.PublicKeyFile = c.Servers[i].Hostname + ".cert.pem"
 	}
 }
 
@@ -338,12 +342,12 @@ func TestRFC2821LimitRecipients(t *testing.T) {
 
 			for i := 0; i < 101; i++ {
 				//fmt.Println(fmt.Sprintf("RCPT TO:test%[email protected]", i))
-				if _, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:test%[email protected]", i)); err != nil {
+				if _, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<test%[email protected]>", i)); err != nil {
 					t.Error("RCPT TO", err.Error())
 					break
 				}
 			}
-			response, err := Command(conn, bufin, "RCPT TO:[email protected]")
+			response, err := Command(conn, bufin, "RCPT TO:<[email protected]>")
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -387,7 +391,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 65)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", rfc5321.LimitLocalPart+1)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -397,7 +401,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 			}
 			// what about if it's exactly 64?
 			// repeat > 64 characters in local part
-			response, err = Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 64)))
+			response, err = Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", rfc5321.LimitLocalPart-1)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -441,7 +445,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 256 characters in local part
-			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 257-7)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", 257-7)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -451,7 +455,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 			}
 			// what about if it's exactly 256?
 			response, err = Command(conn, bufin,
-				fmt.Sprintf("RCPT TO:%s@%s.la", strings.Repeat("a", 64), strings.Repeat("b", 257-5-64)))
+				fmt.Sprintf("RCPT TO:<%s@%s.la>", strings.Repeat("a", 64), strings.Repeat("b", 186)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -491,7 +495,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:a@%s.l", strings.Repeat("a", 255-2)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<a@%s.l>", strings.Repeat("a", 255-2)))
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -501,7 +505,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 			}
 			// what about if it's exactly 255?
 			response, err = Command(conn, bufin,
-				fmt.Sprintf("RCPT TO:a@%s.la", strings.Repeat("b", 255-4)))
+				fmt.Sprintf("RCPT TO:<a@%s.la>", strings.Repeat("b", 255-6)))
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -541,7 +545,7 @@ func TestMailFromCmd(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// Basic valid address
-			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -700,15 +704,17 @@ func TestMailFromCmd(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 
-			// SMTPUTF8 not implemented for now, currently still accepted
-			response, err = Command(conn, bufin, "MAIL FROM:<anö[email protected]>")
-			if err != nil {
-				t.Error("command failed", err.Error())
-			}
-			expected = "250 2.1.0 OK"
-			if strings.Index(response, expected) != 0 {
-				t.Error("Server did not respond with", expected, ", it said:"+response)
-			}
+			/*
+				// todo SMTPUTF8 not implemented for now,
+				response, err = Command(conn, bufin, "MAIL FROM:<anö[email protected]>")
+				if err != nil {
+					t.Error("command failed", err.Error())
+				}
+				expected = "250 2.1.0 OK"
+				if strings.Index(response, expected) != 0 {
+					t.Error("Server did not respond with", expected, ", it said:"+response)
+				}
+			*/
 
 			// Reset
 			response, err = Command(conn, bufin, "RSET")
@@ -865,11 +871,11 @@ func TestNestedMailCmd(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
-			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -881,7 +887,7 @@ func TestNestedMailCmd(t *testing.T) {
 			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
-			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -899,7 +905,7 @@ func TestNestedMailCmd(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 
-			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -989,7 +995,7 @@ func TestDataMaxLength(t *testing.T) {
 				t.Error("command failed", err.Error())
 			}
 			//fmt.Println(response)
-			response, err = Command(conn, bufin, "RCPT TO:[email protected]")
+			response, err = Command(conn, bufin, "RCPT TO:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -1008,7 +1014,7 @@ func TestDataMaxLength(t *testing.T) {
 			//expected := "500 Line too long"
 			expected := "451 4.3.0 Error: Maximum DATA size exceeded"
 			if strings.Index(response, expected) != 0 {
-				t.Error("Server did not respond with", expected, ", it said:"+response, err)
+				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 
 		}
@@ -1076,12 +1082,12 @@ func TestDataCommand(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 
-			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
 			//fmt.Println(response)
-			response, err = Command(conn, bufin, "RCPT TO:[email protected]")
+			response, err = Command(conn, bufin, "RCPT TO:<[email protected]>")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -1102,7 +1108,7 @@ func TestDataCommand(t *testing.T) {
 				bufin,
 				email+"\r\n.\r\n")
 			//expected := "500 Line too long"
-			expected := "250 2.0.0 OK : queued as "
+			expected := "250 2.0.0 OK: queued as "
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response, err)
 			}

+ 17 - 0
tls_go1.8.go

@@ -0,0 +1,17 @@
+// +build go1.8
+
+package guerrilla
+
+import "crypto/tls"
+
+// add ciphers introduced since Go 1.8
+func init() {
+	TLSCiphers["TLS_RSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_RSA_WITH_AES_128_CBC_SHA256
+	TLSCiphers["TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
+	TLSCiphers["TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
+	TLSCiphers["TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"] = tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+	TLSCiphers["TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
+	TLSCiphers["TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
+
+	TLSCurves["X25519"] = tls.X25519
+}

+ 0 - 46
util.go

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