Sfoglia il codice sorgente

merge master into dashboard

Jordan Schalm 8 anni fa
parent
commit
31cb5817c8

+ 1 - 1
.travis.gofmt.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 #!/bin/bash
 
 
-if [[ -n $(find . -path '*/vendor/*' -prune -o -name '*.go' -type f -exec gofmt -l {} \;) ]]; then
+if [[ -n $(find . -path '*/vendor/*' -prune -o -path '*.glide/*' -prune -o -name '*.go' -type f -exec gofmt -l {} \;) ]]; then
     echo "Go code is not formatted:"
     echo "Go code is not formatted:"
     gofmt -d .
     gofmt -d .
     exit 1
     exit 1

+ 4 - 1
.travis.yml

@@ -15,4 +15,7 @@ install:
 script:
 script:
   - ./.travis.gofmt.sh
   - ./.travis.gofmt.sh
   - make guerrillad
   - make guerrillad
-  - go test ./tests
+  - go test ./tests
+  - go test
+  - go test ./cmd/guerrillad
+  - go test ./response

+ 4 - 1
Makefile

@@ -25,4 +25,7 @@ guerrillad: *.go */*.go */*/*.go
 	$(GO_VARS) $(GO) build -o="guerrillad" -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad
 	$(GO_VARS) $(GO) build -o="guerrillad" -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad
 
 
 test: *.go */*.go */*/*.go
 test: *.go */*.go */*/*.go
-	$(GO_VARS) $(GO) test -v ./...
+	$(GO_VARS) $(GO) test -v .
+	$(GO_VARS) $(GO) test -v ./tests
+	$(GO_VARS) $(GO) test -v ./cmd/guerrillad
+	$(GO_VARS) $(GO) test -v ./response

+ 68 - 55
README.md

@@ -49,12 +49,20 @@ including the number of clients, memory usage, graph the number of
 connections/bytes/memory used for the last 24h.
 connections/bytes/memory used for the last 24h.
 Show the top source clients by: IP, by domain & by HELO message.
 Show the top source clients by: IP, by domain & by HELO message.
 Using websocket via https & password protected.
 Using websocket via https & password protected.
+Update: Currently WIP, see branch https://github.com/flashmob/go-guerrilla/tree/dashboard.
 (1 BTC for a successful merge)
 (1 BTC for a successful merge)
 
 
-- Testing: Automated test that can start the server and test end-to-end
-a few common cases, some unit tests would be good too. Automate to
- run when code is pushed to github
-(0.25 BTC for a successful merge)
+- Fuzz Testing: Using https://github.com/dvyukov/go-fuzz
+Implement a fuzzing client that will send input to the
+server's connection. 
+Maybe another area to fuzz would be the config file, 
+fuzz the config file and then send a sighup to the server to see if it 
+can crash? Please open an issue before to discuss scope
+(0.25 BTC for a successful merge / bugs found.)
+
+- Testing: Add some automated more tests to increase coverage.
+(0.1 BTC for a successful merge, judged to be a satisfactory increase
+in coverage. Please open an issue before to discuss scope)
 
 
 - Profiling: Simulate a configurable number of simultaneous clients 
 - Profiling: Simulate a configurable number of simultaneous clients 
 (eg 5000) which send commands at random speeds with messages of various 
 (eg 5000) which send commands at random speeds with messages of various 
@@ -62,7 +70,8 @@ lengths. Some connections to use TLS. Some connections may produce
 errors, eg. disconnect randomly after a few commands, issue unexpected
 errors, eg. disconnect randomly after a few commands, issue unexpected
 input or timeout. Provide a report of all the bottlenecks and setup so 
 input or timeout. Provide a report of all the bottlenecks and setup so 
 that the report can be run automatically run when code is pushed to 
 that the report can be run automatically run when code is pushed to 
-github.
+github. (Flame graph maybe? https://github.com/uber/go-torch 
+Please open an issue before to discuss scope)
 (0.25 BTC)
 (0.25 BTC)
 
 
 - Looking for someone to do a code review & possibly fix any tidbits,
 - Looking for someone to do a code review & possibly fix any tidbits,
@@ -114,7 +123,7 @@ If you want to build on the sample `guerrilla-db-redis` module, setup the follow
 in MySQL:
 in MySQL:
 
 
 	CREATE TABLE IF NOT EXISTS `new_mail` (
 	CREATE TABLE IF NOT EXISTS `new_mail` (
-	  `mail_id` int(11) NOT NULL auto_increment,
+	  `mail_id` BIGINT(20) unsigned NOT NULL AUTO_INCREMENT,
 	  `date` datetime NOT NULL,
 	  `date` datetime NOT NULL,
 	  `from` varchar(128) character set latin1 NOT NULL,
 	  `from` varchar(128) character set latin1 NOT NULL,
 	  `to` varchar(128) character set latin1 NOT NULL,
 	  `to` varchar(128) character set latin1 NOT NULL,
@@ -128,9 +137,8 @@ in MySQL:
 	  `recipient` varchar(128) character set latin1 NOT NULL,
 	  `recipient` varchar(128) character set latin1 NOT NULL,
 	  `has_attach` int(11) NOT NULL,
 	  `has_attach` int(11) NOT NULL,
 	  `ip_addr` varchar(15) NOT NULL,
 	  `ip_addr` varchar(15) NOT NULL,
-	  `delivered` bit(1) NOT NULL default b'0',
-	  `attach_info` text NOT NULL,
-	  `dkim_valid` tinyint(4) default NULL,
+	  `return_path` VARCHAR(255) NOT NULL,
+	  `is_tls` BIT(1) DEFAULT b'0' NOT NULL,
 	  PRIMARY KEY  (`mail_id`),
 	  PRIMARY KEY  (`mail_id`),
 	  KEY `to` (`to`),
 	  KEY `to` (`to`),
 	  KEY `hash` (`hash`),
 	  KEY `hash` (`hash`),
@@ -139,7 +147,9 @@ in MySQL:
 
 
 The above table does not store the body of the email which makes it quick
 The above table does not store the body of the email which makes it quick
 to query and join, while the body of the email is fetched from Redis
 to query and join, while the body of the email is fetched from Redis
-if needed.
+for future processing. The `mail` field can contain data in case Redis is down.
+Otherwise, if data is in Redis, the `mail` will be blank, and
+the `body` field will contain the word 'redis'.
 
 
 You can implement your own saveMail function to use whatever storage /
 You can implement your own saveMail function to use whatever storage /
 backend fits for you. Please share them ^_^, in particular, we would 
 backend fits for you. Please share them ^_^, in particular, we would 
@@ -178,13 +188,13 @@ config := &guerrilla.AppConfig{
   AllowedHosts: []string{...}
   AllowedHosts: []string{...}
 }
 }
 backend := &CustomBackend{...}
 backend := &CustomBackend{...}
-app := guerrilla.New(config, backend)
+app, err := guerrilla.New(config, backend)
 ```
 ```
 
 
 ## Start the app.
 ## Start the app.
 `Start` is non-blocking, so make sure the main goroutine is kept busy
 `Start` is non-blocking, so make sure the main goroutine is kept busy
 ```go
 ```go
-app.Start() (startErrors []error)
+startErrors := app.Start()
 ```
 ```
 
 
 ## Shutting down.
 ## Shutting down.
@@ -205,50 +215,51 @@ Copy goguerrilla.conf.sample to goguerrilla.conf
     {
     {
         "allowed_hosts": ["guerrillamail.com","guerrillamailblock.com","sharklasers.com","guerrillamail.net","guerrillamail.org"], // What hosts to accept
         "allowed_hosts": ["guerrillamail.com","guerrillamailblock.com","sharklasers.com","guerrillamail.net","guerrillamail.org"], // What hosts to accept
         "pid_file" : "/var/run/go-guerrilla.pid", // pid = process id, so that other programs can send signals to our server
         "pid_file" : "/var/run/go-guerrilla.pid", // pid = process id, so that other programs can send signals to our server
-                "backend_name": "guerrilla-db-redis", // what backend to use for saving email. See /backends dir
-                "backend_config" :
-                    {
-                        "mysql_db":"gmail_mail",
-                        "mysql_host":"127.0.0.1:3306",
-                        "mysql_pass":"ok",
-                        "mysql_user":"root",
-                        "mail_table":"new_mail",
-                        "redis_interface" : "127.0.0.1:6379",
-                        "redis_expire_seconds" : 7200,
-                        "save_workers_size" : 3,
-                        "primary_mail_host":"sharklasers.com"
-                    },
-                "servers" : [ // the following is an array of objects, each object represents a new server that will be spawned
-                    {
-                        "is_enabled" : true, // boolean
-                        "host_name":"mail.test.com", // the hostname of the server as set by MX record
-                        "max_size": 1000000, // maximum size of an email in bytes
-                        "private_key_file":"/path/to/pem/file/test.com.key",  // full path to pem file private key
-                        "public_key_file":"/path/to/pem/file/test.com.crt", // full path to pem file certificate
-                        "timeout":180, // timeout in number of seconds before an idle connection is closed
-                        "listen_interface":"127.0.0.1:25", // listen on ip and port
-                        "start_tls_on":true, // supports the STARTTLS command?
-                        "tls_always_on":false, // always connect using TLS? If true, start_tls_on will be false
-                        "max_clients": 1000, // max clients at one time
-                        "log_file":"/dev/stdout" // where to log to (currently ignored)
-                    },
-                    // the following is a second server, but listening on port 465 and always using TLS
-                    {
-                        "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:465",
-                        "start_tls_on":false,
-                        "tls_always_on":true,
-                        "max_clients":500,
-                        "log_file":"/dev/stdout"
-                    }
-                    // repeat as many servers as you need
-                ]
+        "log_file" : "stderr", // can be "off", "stderr", "stdout" or any path to a file
+        "log_level" : "info", // can be  "debug", "info", "error", "warn", "fatal", "panic"
+        "backend_name": "guerrilla-db-redis", // what backend to use for saving email. See /backends dir
+        "backend_config" :
+            {
+                "mysql_db":"gmail_mail",
+                "mysql_host":"127.0.0.1:3306",
+                "mysql_pass":"ok",
+                "mysql_user":"root",
+                "mail_table":"new_mail",
+                "redis_interface" : "127.0.0.1:6379",
+                "redis_expire_seconds" : 7200,
+                "save_workers_size" : 3,
+                "primary_mail_host":"sharklasers.com"
+            },
+        "servers" : [ // the following is an array of objects, each object represents a new server that will be spawned
+            {
+                "is_enabled" : true, // boolean
+                "host_name":"mail.test.com", // the hostname of the server as set by MX record
+                "max_size": 1000000, // maximum size of an email in bytes
+                "private_key_file":"/path/to/pem/file/test.com.key",  // full path to pem file private key
+                "public_key_file":"/path/to/pem/file/test.com.crt", // full path to pem file certificate
+                "timeout":180, // timeout in number of seconds before an idle connection is closed
+                "listen_interface":"127.0.0.1:25", // listen on ip and port
+                "start_tls_on":true, // supports the STARTTLS command?
+                "tls_always_on":false, // always connect using TLS? If true, start_tls_on will be false
+                "max_clients": 1000, // max clients at one time
+                "log_file":"/dev/stdout" // optional. Can be "off", "stderr", "stdout" or any path to a file. Will use global setting of empty.
+            },
+            // the following is a second server, but listening on port 465 and always using TLS
+            {
+                "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:465",
+                "start_tls_on":false,
+                "tls_always_on":true,
+                "max_clients":500
             }
             }
+            // repeat as many servers as you need
+        ]
+    }
     }
     }
 
 
 The Json parser is very strict on syntax. If there's a parse error and it
 The Json parser is very strict on syntax. If there's a parse error and it
@@ -276,6 +287,8 @@ Large refactoring of the code.
 - Logging functionality: logrus is now used for logging. Currently output is going to stdout
 - Logging functionality: logrus is now used for logging. Currently output is going to stdout
 - Incompatible change: Config's allowed_hosts is now an array
 - Incompatible change: Config's allowed_hosts is now an array
 - Incompatible change: The server's command is now a command called `guerrillad`
 - Incompatible change: The server's command is now a command called `guerrillad`
+- Config re-loading via SIGHUP: reload TLS, add/remove/enable/disable servers, change allowed hosts, timeout.
+- Begin writing automated tests
  
  
 
 
 1.5.1 - 4nd Nov 2016 (Latest tagged release)
 1.5.1 - 4nd Nov 2016 (Latest tagged release)

+ 157 - 0
backends/abstract.go

@@ -0,0 +1,157 @@
+package backends
+
+import (
+	"errors"
+	"fmt"
+	"github.com/flashmob/go-guerrilla/envelope"
+	"reflect"
+	"strings"
+)
+
+type AbstractBackend struct {
+	config abstractConfig
+	extend Backend
+}
+
+type abstractConfig struct {
+	LogReceivedMails bool `json:"log_received_mails"`
+}
+
+// Your backend should implement this method and set b.config field with a custom config struct
+// Therefore, your implementation would have your own custom config type instead of dummyConfig
+func (b *AbstractBackend) loadConfig(backendConfig BackendConfig) (err error) {
+	// Load the backend config for the backend. It has already been unmarshalled
+	// from the main config file 'backend' config "backend_config"
+	// Now we need to convert each type and copy into the dummyConfig struct
+	configType := baseConfig(&abstractConfig{})
+	bcfg, err := b.extractConfig(backendConfig, configType)
+	if err != nil {
+		return err
+	}
+	m := bcfg.(*abstractConfig)
+	b.config = *m
+	return nil
+}
+
+func (b *AbstractBackend) Initialize(config BackendConfig) error {
+	if b.extend != nil {
+		return b.extend.loadConfig(config)
+	}
+	err := b.loadConfig(config)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (b *AbstractBackend) Shutdown() error {
+	if b.extend != nil {
+		return b.extend.Shutdown()
+	}
+	return nil
+}
+
+func (b *AbstractBackend) Process(mail *envelope.Envelope) BackendResult {
+	if b.extend != nil {
+		return b.extend.Process(mail)
+	}
+	mail.ParseHeaders()
+
+	if b.config.LogReceivedMails {
+		mainlog.Infof("Mail from: %s / to: %v", mail.MailFrom.String(), mail.RcptTo)
+		mainlog.Info("Headers are: %s", mail.Header)
+
+	}
+	return NewBackendResult("250 OK")
+}
+
+func (b *AbstractBackend) saveMailWorker(saveMailChan chan *savePayload) {
+	if b.extend != nil {
+		b.extend.saveMailWorker(saveMailChan)
+		return
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			// recover form closed channel
+			fmt.Println("Recovered in f", r)
+		}
+		// close any connections / files
+		// ...
+
+	}()
+	for {
+		payload := <-saveMailChan
+		if payload == nil {
+			mainlog.Debug("No more saveMailChan payload")
+			return
+		}
+		// process the email here
+		result := b.Process(payload.mail)
+		// if all good
+		if result.Code() < 300 {
+			payload.savedNotify <- &saveStatus{nil, "s0m3l337Ha5hva1u3LOL"}
+		} else {
+			payload.savedNotify <- &saveStatus{errors.New(result.String()), "s0m3l337Ha5hva1u3LOL"}
+		}
+
+	}
+}
+
+func (b *AbstractBackend) getNumberOfWorkers() int {
+	if b.extend != nil {
+		return b.extend.getNumberOfWorkers()
+	}
+	return 1
+}
+
+func (b *AbstractBackend) testSettings() error {
+	if b.extend != nil {
+		return b.extend.testSettings()
+	}
+	return nil
+}
+
+// Load the backend config for the backend. It has already been unmarshalled
+// from the main config file 'backend' config "backend_config"
+// Now we need to convert each type and copy into the guerrillaDBAndRedisConfig struct
+// The reason why using reflection is because we'll get a nice error message if the field is missing
+// the alternative solution would be to json.Marshal() and json.Unmarshal() however that will not give us any
+// error messages
+func (h *AbstractBackend) extractConfig(configData BackendConfig, configType baseConfig) (interface{}, error) {
+	// Use reflection so that we can provide a nice error message
+	s := reflect.ValueOf(configType).Elem() // so that we can set the values
+	m := reflect.ValueOf(configType).Elem()
+	t := reflect.TypeOf(configType).Elem()
+	typeOfT := s.Type()
+
+	for i := 0; i < m.NumField(); i++ {
+		f := s.Field(i)
+		// read the tags of the config struct
+		field_name := t.Field(i).Tag.Get("json")
+		if len(field_name) > 0 {
+			// parse the tag to
+			// get the field name from struct tag
+			split := strings.Split(field_name, ",")
+			field_name = split[0]
+		} else {
+			// could have no tag
+			// so use the reflected field name
+			field_name = typeOfT.Field(i).Name
+		}
+		if f.Type().Name() == "int" {
+			if intVal, converted := configData[field_name].(float64); converted {
+				s.Field(i).SetInt(int64(intVal))
+			} else {
+				return configType, convertError("property missing/invalid: '" + field_name + "' of expected type: " + f.Type().Name())
+			}
+		}
+		if f.Type().Name() == "string" {
+			if stringVal, converted := configData[field_name].(string); converted {
+				s.Field(i).SetString(stringVal)
+			} else {
+				return configType, convertError("missing/invalid: '" + field_name + "' of type: " + f.Type().Name())
+			}
+		}
+	}
+	return configType, nil
+}

+ 216 - 0
backends/backend.go

@@ -0,0 +1,216 @@
+package backends
+
+import (
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/flashmob/go-guerrilla/envelope"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/response"
+)
+
+var mainlog log.Logger
+
+// Backends process received mail. Depending on the implementation, they can store mail in the database,
+// write to a file, check for spam, re-transmit to another server, etc.
+// Must return an SMTP message (i.e. "250 OK") and a boolean indicating
+// whether the message was processed successfully.
+type Backend interface {
+	// Public methods
+	Process(*envelope.Envelope) BackendResult
+	Initialize(BackendConfig) error
+	Shutdown() error
+
+	// start save mail worker(s)
+	saveMailWorker(chan *savePayload)
+	// get the number of workers that will be stared
+	getNumberOfWorkers() int
+	// test database settings, permissions, correct paths, etc, before starting workers
+	testSettings() error
+	// parse the configuration files
+	loadConfig(BackendConfig) error
+}
+
+type BackendConfig map[string]interface{}
+
+var backends = map[string]Backend{}
+
+type baseConfig interface{}
+
+type saveStatus struct {
+	err  error
+	hash string
+}
+
+type savePayload struct {
+	mail        *envelope.Envelope
+	from        *envelope.EmailAddress
+	recipient   *envelope.EmailAddress
+	savedNotify chan *saveStatus
+}
+
+// BackendResult represents a response to an SMTP client after receiving DATA.
+// The String method should return an SMTP message ready to send back to the
+// client, for example `250 OK: Message received`.
+type BackendResult interface {
+	fmt.Stringer
+	// Code should return the SMTP code associated with this response, ie. `250`
+	Code() int
+}
+
+// Internal implementation of BackendResult for use by backend implementations.
+type backendResult string
+
+func (br backendResult) String() string {
+	return string(br)
+}
+
+// Parses the SMTP code from the first 3 characters of the SMTP message.
+// Returns 554 if code cannot be parsed.
+func (br backendResult) Code() int {
+	trimmed := strings.TrimSpace(string(br))
+	if len(trimmed) < 3 {
+		return 554
+	}
+	code, err := strconv.Atoi(trimmed[:3])
+	if err != nil {
+		return 554
+	}
+	return code
+}
+
+func NewBackendResult(message string) BackendResult {
+	return backendResult(message)
+}
+
+// A backend gateway is a proxy that implements the Backend interface.
+// It is used to start multiple goroutine workers for saving mail, and then distribute email saving to the workers
+// via a channel. Shutting down via Shutdown() will stop all workers.
+// The rest of this program always talks to the backend via this gateway.
+type BackendGateway struct {
+	AbstractBackend
+	saveMailChan chan *savePayload
+	// waits for backend workers to start/stop
+	wg sync.WaitGroup
+	b  Backend
+	// controls access to state
+	stateGuard sync.Mutex
+	State      backendState
+	config     BackendConfig
+}
+
+// possible values for state
+const (
+	BackendStateRunning = iota
+	BackendStateShuttered
+	BackendStateError
+)
+
+type backendState int
+
+func (s backendState) String() string {
+	return strconv.Itoa(int(s))
+}
+
+// New retrieve a backend specified by the backendName, and initialize it using
+// backendConfig
+func New(backendName string, backendConfig BackendConfig, l log.Logger) (Backend, error) {
+	backend, found := backends[backendName]
+	mainlog = l
+	if !found {
+		return nil, fmt.Errorf("backend %q not found", backendName)
+	}
+	gateway := &BackendGateway{b: backend, config: backendConfig}
+	err := gateway.Initialize(backendConfig)
+	if err != nil {
+		return nil, fmt.Errorf("error while initializing the backend: %s", err)
+	}
+	gateway.State = BackendStateRunning
+	return gateway, nil
+}
+
+// Process distributes an envelope to one of the backend workers
+func (gw *BackendGateway) Process(e *envelope.Envelope) BackendResult {
+	if gw.State != BackendStateRunning {
+		return NewBackendResult(response.Canned.FailBackendNotRunning + gw.State.String())
+	}
+
+	to := e.RcptTo
+	from := e.MailFrom
+
+	// place on the channel so that one of the save mail workers can pick it up
+	// TODO: support multiple recipients
+	savedNotify := make(chan *saveStatus)
+	gw.saveMailChan <- &savePayload{e, &from, &to[0], savedNotify}
+	// wait for the save to complete
+	// or timeout
+	select {
+	case status := <-savedNotify:
+		if status.err != nil {
+			return NewBackendResult(response.Canned.FailBackendTransaction + status.err.Error())
+		}
+		return NewBackendResult(response.Canned.SuccessMessageQueued + status.hash)
+
+	case <-time.After(time.Second * 30):
+		mainlog.Infof("Backend has timed out")
+		return NewBackendResult(response.Canned.FailBackendTimeout)
+	}
+}
+func (gw *BackendGateway) Shutdown() error {
+	gw.stateGuard.Lock()
+	defer gw.stateGuard.Unlock()
+	if gw.State != BackendStateShuttered {
+		err := gw.b.Shutdown()
+		if err == nil {
+			close(gw.saveMailChan) // workers will stop
+			gw.wg.Wait()
+			gw.State = BackendStateShuttered
+		}
+		return err
+	}
+	return nil
+}
+
+// Reinitialize starts up a backend gateway that was shutdown before
+func (gw *BackendGateway) Reinitialize() error {
+	if gw.State != BackendStateShuttered {
+		return errors.New("backend must be in BackendStateshuttered state to Reinitialize")
+	}
+	err := gw.Initialize(gw.config)
+	if err != nil {
+		return fmt.Errorf("error while initializing the backend: %s", err)
+	}
+	gw.State = BackendStateRunning
+	return err
+}
+
+func (gw *BackendGateway) Initialize(cfg BackendConfig) error {
+	err := gw.b.Initialize(cfg)
+	if err == nil {
+		workersSize := gw.b.getNumberOfWorkers()
+		if workersSize < 1 {
+			gw.State = BackendStateError
+			return errors.New("Must have at least 1 worker")
+		}
+		if err := gw.b.testSettings(); err != nil {
+			gw.State = BackendStateError
+			return err
+		}
+		gw.saveMailChan = make(chan *savePayload, workersSize)
+		// start our savemail workers
+		gw.wg.Add(workersSize)
+		for i := 0; i < workersSize; i++ {
+			go func() {
+				gw.b.saveMailWorker(gw.saveMailChan)
+				gw.wg.Done()
+			}()
+		}
+	} else {
+		gw.State = BackendStateError
+	}
+	return err
+}

+ 25 - 27
backends/dummy.go

@@ -1,39 +1,37 @@
 package backends
 package backends
 
 
-import (
-	log "github.com/Sirupsen/logrus"
-
-	"github.com/flashmob/go-guerrilla"
-)
-
-type DummyBackend struct {
-	config dummyConfig
+func init() {
+	// decorator pattern
+	backends["dummy"] = &AbstractBackend{
+		extend: &DummyBackend{},
+	}
 }
 }
 
 
+// custom configuration we will parse from the json
+// see guerrillaDBAndRedisConfig struct for a more complete example
 type dummyConfig struct {
 type dummyConfig struct {
 	LogReceivedMails bool `json:"log_received_mails"`
 	LogReceivedMails bool `json:"log_received_mails"`
 }
 }
 
 
-func (b *DummyBackend) loadConfig(config map[string]interface{}) {
-	willLog, ok := config["log_received_mails"].(bool)
-	if !ok {
-		b.config = dummyConfig{false}
-	} else {
-		b.config = dummyConfig{willLog}
-	}
-}
-
-func (b *DummyBackend) Initialize(config map[string]interface{}) {
-	b.loadConfig(config)
-}
-
-func (b *DummyBackend) Shutdown() error {
-	return nil
+// putting all the paces we need together
+type DummyBackend struct {
+	config dummyConfig
+	// embed functions form AbstractBackend so that DummyBackend satisfies the Backend interface
+	AbstractBackend
 }
 }
 
 
-func (b *DummyBackend) Process(mail *guerrilla.Envelope) guerrilla.BackendResult {
-	if b.config.LogReceivedMails {
-		log.Infof("Mail from: %s / to: %v", mail.MailFrom.String(), mail.RcptTo)
+// Backends should implement this method and set b.config field with a custom config struct
+// Therefore, your implementation would have a custom config type instead of dummyConfig
+func (b *DummyBackend) loadConfig(backendConfig BackendConfig) (err error) {
+	// Load the backend config for the backend. It has already been unmarshalled
+	// from the main config file 'backend' config "backend_config"
+	// Now we need to convert each type and copy into the dummyConfig struct
+	configType := baseConfig(&dummyConfig{})
+	bcfg, err := b.extractConfig(backendConfig, configType)
+	if err != nil {
+		return err
 	}
 	}
-	return guerrilla.NewBackendResult("250 OK")
+	m := bcfg.(*dummyConfig)
+	b.config = *m
+	return nil
 }
 }

+ 320 - 145
backends/guerrilla_db_redis.go

@@ -1,26 +1,67 @@
 package backends
 package backends
 
 
+// This backend is presented here as an example only, please modify it to your needs.
+// The backend stores the email data in Redis.
+// Other meta-information is stored in MySQL to be joined later.
+// A lot of email gets discarded without viewing on Guerrilla Mail,
+// so it's much faster to put in Redis, where other programs can
+// process it later, without touching the disk.
+//
+// Some features:
+// - It batches the SQL inserts into a single query and inserts either after a time threshold or if the batch is full
+// - If the mysql driver crashes, it's able to recover, log the incident and resume again.
+// - It also does a clean shutdown - it tries to save everything before returning
+//
+// Short history:
+// Started with issuing an insert query for each single email and another query to update the tally
+// Then applied the following optimizations:
+// - Moved tally updates to another background process which does the tallying in a single query
+// - Changed the MySQL queries to insert in batch
+// - Made a Compressor that recycles buffers using sync.Pool
+// The result was around 400% speed improvement. If you know of any more improvements, please share!
+// - Added the recovery mechanism,
+
 import (
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
 	"fmt"
-	"sync"
+
 	"time"
 	"time"
 
 
-	log "github.com/Sirupsen/logrus"
-	"github.com/flashmob/go-guerrilla"
 	"github.com/garyburd/redigo/redis"
 	"github.com/garyburd/redigo/redis"
 
 
-	"github.com/ziutek/mymysql/autorc"
-	_ "github.com/ziutek/mymysql/godrv"
+	"bytes"
+	"compress/zlib"
+	"database/sql"
+	_ "github.com/go-sql-driver/mysql"
+
+	"github.com/go-sql-driver/mysql"
+	"io"
+	"runtime/debug"
+	"strings"
+	"sync"
 )
 )
 
 
+// how many rows to batch at a time
+const GuerrillaDBAndRedisBatchMax = 2
+
+// tick on every...
+const GuerrillaDBAndRedisBatchTimeout = time.Second * 3
+
+func init() {
+	backends["guerrilla-db-redis"] = &AbstractBackend{
+		extend: &GuerrillaDBAndRedisBackend{}}
+}
+
 type GuerrillaDBAndRedisBackend struct {
 type GuerrillaDBAndRedisBackend struct {
-	config       guerrillaDBAndRedisConfig
-	saveMailChan chan *savePayload
-	wg           sync.WaitGroup
+	AbstractBackend
+	config    guerrillaDBAndRedisConfig
+	batcherWg sync.WaitGroup
+	// cache prepared queries
+	cache stmtCache
 }
 }
 
 
+// statement cache. It's an array, not slice
+type stmtCache [GuerrillaDBAndRedisBatchMax]*sql.Stmt
+
 type guerrillaDBAndRedisConfig struct {
 type guerrillaDBAndRedisConfig struct {
 	NumberOfWorkers    int    `json:"save_workers_size"`
 	NumberOfWorkers    int    `json:"save_workers_size"`
 	MysqlTable         string `json:"mail_table"`
 	MysqlTable         string `json:"mail_table"`
@@ -40,146 +81,292 @@ func convertError(name string) error {
 // Load the backend config for the backend. It has already been unmarshalled
 // Load the backend config for the backend. It has already been unmarshalled
 // from the main config file 'backend' config "backend_config"
 // from the main config file 'backend' config "backend_config"
 // Now we need to convert each type and copy into the guerrillaDBAndRedisConfig struct
 // Now we need to convert each type and copy into the guerrillaDBAndRedisConfig struct
-func (g *GuerrillaDBAndRedisBackend) loadConfig(backendConfig map[string]interface{}) error {
-	data, err := json.Marshal(backendConfig)
+func (g *GuerrillaDBAndRedisBackend) loadConfig(backendConfig BackendConfig) (err error) {
+	configType := baseConfig(&guerrillaDBAndRedisConfig{})
+	bcfg, err := g.extractConfig(backendConfig, configType)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	m := bcfg.(*guerrillaDBAndRedisConfig)
+	g.config = *m
+	return nil
+}
 
 
-	err = json.Unmarshal(data, &g.config)
-	if g.config.NumberOfWorkers < 1 {
-		return errors.New("Must have more than 1 worker")
-	}
+func (g *GuerrillaDBAndRedisBackend) getNumberOfWorkers() int {
+	return g.config.NumberOfWorkers
+}
 
 
-	return err
+type redisClient struct {
+	isConnected bool
+	conn        redis.Conn
+	time        int
 }
 }
 
 
-func (g *GuerrillaDBAndRedisBackend) Initialize(backendConfig map[string]interface{}) error {
-	err := g.loadConfig(backendConfig)
-	if err != nil {
-		return err
-	}
+// compressedData struct will be compressed using zlib when printed via fmt
+type compressedData struct {
+	extraHeaders []byte
+	data         *bytes.Buffer
+	pool         *sync.Pool
+}
 
 
-	if err := g.testDbConnections(); err != nil {
-		return err
+// newCompressedData returns a new CompressedData
+func newCompressedData() *compressedData {
+	var p = sync.Pool{
+		New: func() interface{} {
+			var b bytes.Buffer
+			return &b
+		},
+	}
+	return &compressedData{
+		pool: &p,
 	}
 	}
+}
 
 
-	g.saveMailChan = make(chan *savePayload, g.config.NumberOfWorkers)
+// Set the extraheaders and buffer of data to compress
+func (c *compressedData) set(b []byte, d *bytes.Buffer) {
+	c.extraHeaders = b
+	c.data = d
+}
 
 
-	// start some savemail workers
-	g.wg.Add(g.config.NumberOfWorkers)
-	for i := 0; i < g.config.NumberOfWorkers; i++ {
-		go g.saveMail()
+// implement Stringer interface
+func (c *compressedData) String() string {
+	if c.data == nil {
+		return ""
 	}
 	}
+	//borrow a buffer form the pool
+	b := c.pool.Get().(*bytes.Buffer)
+	// put back in the pool
+	defer func() {
+		b.Reset()
+		c.pool.Put(b)
+	}()
 
 
-	return nil
+	var r *bytes.Reader
+	w, _ := zlib.NewWriterLevel(b, zlib.BestSpeed)
+	r = bytes.NewReader(c.extraHeaders)
+	io.Copy(w, r)
+	io.Copy(w, c.data)
+	w.Close()
+	return b.String()
 }
 }
 
 
-func (g *GuerrillaDBAndRedisBackend) Shutdown() error {
-	close(g.saveMailChan) // workers will stop
-	g.wg.Wait()
-	return nil
+// clear it, without clearing the pool
+func (c *compressedData) clear() {
+	c.extraHeaders = []byte{}
+	c.data = nil
 }
 }
 
 
-func (g *GuerrillaDBAndRedisBackend) Process(mail *guerrilla.Envelope) guerrilla.BackendResult {
-	to := mail.RcptTo
-	from := mail.MailFrom
-	if len(to) == 0 {
-		return guerrilla.NewBackendResult("554 Error: no recipient")
+// prepares the sql query with the number of rows that can be batched with it
+func (g *GuerrillaDBAndRedisBackend) prepareInsertQuery(rows int, db *sql.DB) *sql.Stmt {
+	if rows == 0 {
+		panic("rows argument cannot be 0")
+	}
+	if g.cache[rows-1] != nil {
+		return g.cache[rows-1]
 	}
 	}
+	sqlstr := "INSERT INTO " + g.config.MysqlTable + " "
+	sqlstr += "(`date`, `to`, `from`, `subject`, `body`, `charset`, `mail`, `spam_score`, `hash`, `content_type`, `recipient`, `has_attach`, `ip_addr`, `return_path`, `is_tls`)"
+	sqlstr += " values "
+	values := "(NOW(), ?, ?, ?, ? , 'UTF-8' , ?, 0, ?, '', ?, 0, ?, ?, ?)"
+	// add more rows
+	comma := ""
+	for i := 0; i < rows; i++ {
+		sqlstr += comma + values
+		if comma == "" {
+			comma = ","
+		}
+	}
+	stmt, sqlErr := db.Prepare(sqlstr)
+	if sqlErr != nil {
+		mainlog.WithError(sqlErr).Fatalf("failed while db.Prepare(INSERT...)")
+	}
+	// cache it
+	g.cache[rows-1] = stmt
+	return stmt
+}
 
 
-	// to do: timeout when adding to SaveMailChan
-	// place on the channel so that one of the save mail workers can pick it up
-	// TODO: support multiple recipients
-	savedNotify := make(chan *saveStatus)
-	g.saveMailChan <- &savePayload{mail, from, &to[0], savedNotify}
-	// wait for the save to complete
-	// or timeout
-	select {
-	case status := <-savedNotify:
-		if status.err != nil {
-			return guerrilla.NewBackendResult("554 Error: " + status.err.Error())
+func (g *GuerrillaDBAndRedisBackend) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *[]interface{}) {
+	var execErr error
+	defer func() {
+		if r := recover(); r != nil {
+			//logln(1, fmt.Sprintf("Recovered in %v", r))
+			mainlog.Error("Recovered form panic:", r, string(debug.Stack()))
+			sum := 0
+			for _, v := range *vals {
+				if str, ok := v.(string); ok {
+					sum = sum + len(str)
+				}
+			}
+			mainlog.Errorf("panic while inserting query [%s] size:%d, err %v", r, sum, execErr)
+			panic("query failed")
 		}
 		}
-		return guerrilla.NewBackendResult(fmt.Sprintf("250 OK : queued as %s", status.hash))
-	case <-time.After(time.Second * 30):
-		log.Debug("timeout")
-		return guerrilla.NewBackendResult("554 Error: transaction timeout")
+	}()
+	// prepare the query used to insert when rows reaches batchMax
+	insertStmt = g.prepareInsertQuery(c, db)
+	_, execErr = insertStmt.Exec(*vals...)
+	if execErr != nil {
+		mainlog.WithError(execErr).Error("There was a problem the insert")
 	}
 	}
 }
 }
 
 
-type savePayload struct {
-	mail        *guerrilla.Envelope
-	from        *guerrilla.EmailAddress
-	recipient   *guerrilla.EmailAddress
-	savedNotify chan *saveStatus
+// Batches the rows from the feeder chan in to a single INSERT statement.
+// Execute the batches query when:
+// - number of batched rows reaches a threshold, i.e. count n = threshold
+// - or, no new rows within a certain time, i.e. times out
+// The goroutine can either exit if there's a panic or feeder channel closes
+// it returns feederOk which signals if the feeder chanel was ok (still open) while returning
+// if it feederOk is false, then it means the feeder chanel is closed
+func (g *GuerrillaDBAndRedisBackend) insertQueryBatcher(feeder chan []interface{}, db *sql.DB) (feederOk bool) {
+	// controls shutdown
+	defer g.batcherWg.Done()
+	g.batcherWg.Add(1)
+	// vals is where values are batched to
+	var vals []interface{}
+	// how many rows were batched
+	count := 0
+	// The timer will tick every second.
+	// Interrupting the select clause when there's no data on the feeder channel
+	t := time.NewTimer(GuerrillaDBAndRedisBatchTimeout)
+	// prepare the query used to insert when rows reaches batchMax
+	insertStmt := g.prepareInsertQuery(GuerrillaDBAndRedisBatchMax, db)
+	// inserts executes a batched insert query, clears the vals and resets the count
+	insert := func(c int) {
+		if c > 0 {
+			g.doQuery(c, db, insertStmt, &vals)
+		}
+		vals = nil
+		count = 0
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			mainlog.Error("insertQueryBatcher caught a panic", r)
+		}
+	}()
+	// Keep getting values from feeder and add to batch.
+	// if feeder times out, execute the batched query
+	// otherwise, execute the batched query once it reaches the GuerrillaDBAndRedisBatchMax threshold
+	feederOk = true
+	for {
+		select {
+		// it may panic when reading on a closed feeder channel. feederOK detects if it was closed
+		case row, feederOk := <-feeder:
+			if row == nil {
+				mainlog.Info("Query batchaer exiting")
+				// Insert any remaining rows
+				insert(count)
+				return feederOk
+			}
+			vals = append(vals, row...)
+			count++
+			mainlog.Debug("new feeder row:", row, " cols:", len(row), " count:", count, " worker", workerId)
+			if count >= GuerrillaDBAndRedisBatchMax {
+				insert(GuerrillaDBAndRedisBatchMax)
+			}
+			// stop timer from firing (reset the interrupt)
+			if !t.Stop() {
+				<-t.C
+			}
+			t.Reset(GuerrillaDBAndRedisBatchTimeout)
+		case <-t.C:
+			// anything to insert?
+			if n := len(vals); n > 0 {
+				insert(count)
+			}
+			t.Reset(GuerrillaDBAndRedisBatchTimeout)
+		}
+	}
 }
 }
 
 
-type saveStatus struct {
-	err  error
-	hash string
+func trimToLimit(str string, limit int) string {
+	ret := strings.TrimSpace(str)
+	if len(str) > limit {
+		ret = str[:limit]
+	}
+	return ret
 }
 }
 
 
-type redisClient struct {
-	isConnected bool
-	conn        redis.Conn
-	time        int
+var workerId = 0
+
+func (g *GuerrillaDBAndRedisBackend) mysqlConnect() (*sql.DB, error) {
+	conf := mysql.Config{
+		User:         g.config.MysqlUser,
+		Passwd:       g.config.MysqlPass,
+		DBName:       g.config.MysqlDB,
+		Net:          "tcp",
+		Addr:         g.config.MysqlHost,
+		ReadTimeout:  GuerrillaDBAndRedisBatchTimeout + (time.Second * 10),
+		WriteTimeout: GuerrillaDBAndRedisBatchTimeout + (time.Second * 10),
+		Params:       map[string]string{"collation": "utf8_general_ci"},
+	}
+	if db, err := sql.Open("mysql", conf.FormatDSN()); err != nil {
+		mainlog.Error("cannot open mysql", err)
+		return nil, err
+	} else {
+		return db, nil
+	}
+
 }
 }
 
 
-func (g *GuerrillaDBAndRedisBackend) saveMail() {
+func (g *GuerrillaDBAndRedisBackend) saveMailWorker(saveMailChan chan *savePayload) {
 	var to, body string
 	var to, body string
-	var err error
 
 
 	var redisErr error
 	var redisErr error
-	var length int
+
+	workerId++
 
 
 	redisClient := &redisClient{}
 	redisClient := &redisClient{}
-	db := autorc.New(
-		"tcp",
-		"",
-		g.config.MysqlHost,
-		g.config.MysqlUser,
-		g.config.MysqlPass,
-		g.config.MysqlDB)
-	db.Register("set names utf8")
-	sql := "INSERT INTO " + g.config.MysqlTable + " "
-	sql += "(`date`, `to`, `from`, `subject`, `body`, `charset`, `mail`, `spam_score`, `hash`, `content_type`, `recipient`, `has_attach`, `ip_addr`, `return_path`, `is_tls`)"
-	sql += " values (NOW(), ?, ?, ?, ? , 'UTF-8' , ?, 0, ?, '', ?, 0, ?, ?, ?)"
-	ins, sqlErr := db.Prepare(sql)
-	if sqlErr != nil {
-		log.WithError(sqlErr).Fatalf("failed while db.Prepare(INSERT...)")
-	}
-	sql = "UPDATE gm2_setting SET `setting_value` = `setting_value`+1 WHERE `setting_name`='received_emails' LIMIT 1"
-	incr, sqlErr := db.Prepare(sql)
-	if sqlErr != nil {
-		log.WithError(sqlErr).Fatalf("failed while db.Prepare(UPDATE...)")
+	var db *sql.DB
+	var err error
+	db, err = g.mysqlConnect()
+	if err != nil {
+		mainlog.Fatalf("cannot open mysql: %s", err)
 	}
 	}
+
+	// start the query SQL batching where we will send data via the feeder channel
+	feeder := make(chan []interface{}, 1)
+	go func() {
+		for {
+			if feederOK := g.insertQueryBatcher(feeder, db); !feederOK {
+				mainlog.Debug("insertQueryBatcher exited")
+				return
+			}
+			// if insertQueryBatcher panics, it can recover and go in again
+			mainlog.Debug("resuming insertQueryBatcher")
+		}
+
+	}()
+
 	defer func() {
 	defer func() {
 		if r := recover(); r != nil {
 		if r := recover(); r != nil {
-			// recover form closed channel
-			fmt.Println("Recovered in f", r)
-		}
-		if db.Raw != nil {
-			db.Raw.Close()
+			//recover form closed channel
+			mainlog.Error("panic recovered in saveMailWorker", r)
 		}
 		}
+		db.Close()
 		if redisClient.conn != nil {
 		if redisClient.conn != nil {
-			log.Infof("closed redis")
+			mainlog.Infof("closed redis")
 			redisClient.conn.Close()
 			redisClient.conn.Close()
 		}
 		}
+		// close the feeder & wait for query batcher to exit.
+		close(feeder)
+		g.batcherWg.Wait()
 
 
-		g.wg.Done()
 	}()
 	}()
-
+	var vals []interface{}
+	data := newCompressedData()
 	//  receives values from the channel repeatedly until it is closed.
 	//  receives values from the channel repeatedly until it is closed.
+
 	for {
 	for {
-		payload := <-g.saveMailChan
+		payload := <-saveMailChan
 		if payload == nil {
 		if payload == nil {
-			log.Debug("No more saveMailChan payload")
+			mainlog.Debug("No more saveMailChan payload")
 			return
 			return
 		}
 		}
-		to = payload.recipient.User + "@" + g.config.PrimaryHost
-		length = len(payload.mail.Data)
+		mainlog.Debug("Got mail from chan", payload.mail.RemoteAddress)
+		to = trimToLimit(strings.TrimSpace(payload.recipient.User)+"@"+g.config.PrimaryHost, 255)
+		payload.mail.Helo = trimToLimit(payload.mail.Helo, 255)
+		payload.recipient.Host = trimToLimit(payload.recipient.Host, 255)
 		ts := fmt.Sprintf("%d", time.Now().UnixNano())
 		ts := fmt.Sprintf("%d", time.Now().UnixNano())
-		payload.mail.Subject = MimeHeaderDecode(payload.mail.Subject)
+		payload.mail.ParseHeaders()
 		hash := MD5Hex(
 		hash := MD5Hex(
 			to,
 			to,
 			payload.mail.MailFrom.String(),
 			payload.mail.MailFrom.String(),
@@ -191,51 +378,45 @@ func (g *GuerrillaDBAndRedisBackend) saveMail() {
 		addHead += "Received: from " + payload.mail.Helo + " (" + payload.mail.Helo + "  [" + payload.mail.RemoteAddress + "])\r\n"
 		addHead += "Received: from " + payload.mail.Helo + " (" + payload.mail.Helo + "  [" + payload.mail.RemoteAddress + "])\r\n"
 		addHead += "	by " + payload.recipient.Host + " with SMTP id " + hash + "@" + payload.recipient.Host + ";\r\n"
 		addHead += "	by " + payload.recipient.Host + " with SMTP id " + hash + "@" + payload.recipient.Host + ";\r\n"
 		addHead += "	" + time.Now().Format(time.RFC1123Z) + "\r\n"
 		addHead += "	" + time.Now().Format(time.RFC1123Z) + "\r\n"
-		// compress to save space
-		payload.mail.Data = Compress(addHead, payload.mail.Data)
+
+		// data will be compressed when printed, with addHead added to beginning
+
+		data.set([]byte(addHead), &payload.mail.Data)
 		body = "gzencode"
 		body = "gzencode"
+
+		// data will be written to redis - it implements the Stringer interface, redigo uses fmt to
+		// print the data to redis.
+
 		redisErr = redisClient.redisConnection(g.config.RedisInterface)
 		redisErr = redisClient.redisConnection(g.config.RedisInterface)
 		if redisErr == nil {
 		if redisErr == nil {
-			_, doErr := redisClient.conn.Do("SETEX", hash, g.config.RedisExpireSeconds, payload.mail.Data)
+			_, doErr := redisClient.conn.Do("SETEX", hash, g.config.RedisExpireSeconds, data)
 			if doErr == nil {
 			if doErr == nil {
-				payload.mail.Data = ""
-				body = "redis"
+				body = "redis" // the backend system will know to look in redis for the message data
+				data.clear()   // blank
 			}
 			}
 		} else {
 		} else {
-			log.WithError(redisErr).Warn("Error while SETEX on redis")
+			mainlog.WithError(redisErr).Warn("Error while connecting redis")
 		}
 		}
-		// bind data to cursor
-		ins.Bind(
-			to,
-			payload.mail.MailFrom.String(),
-			payload.mail.Subject,
+
+		vals = []interface{}{} // clear the vals
+		vals = append(vals,
+			trimToLimit(to, 255),
+			trimToLimit(payload.mail.MailFrom.String(), 255),
+			trimToLimit(payload.mail.Subject, 255),
 			body,
 			body,
-			payload.mail.Data,
+			data.String(),
 			hash,
 			hash,
-			to,
+			trimToLimit(to, 255),
 			payload.mail.RemoteAddress,
 			payload.mail.RemoteAddress,
-			payload.mail.MailFrom.String(),
-			payload.mail.TLS,
-		)
-		// save, discard result
-		_, _, err = ins.Exec()
-		if err != nil {
-			errMsg := "Database error while inserting"
-			log.WithError(err).Warn(errMsg)
-			payload.savedNotify <- &saveStatus{errors.New(errMsg), hash}
-		} else {
-			log.Debugf("Email saved %s (len=%d)", hash, length)
-			_, _, err = incr.Exec()
-			if err != nil {
-				log.WithError(err).Warn("Database error while incr count")
-			}
-			payload.savedNotify <- &saveStatus{nil, hash}
-		}
+			trimToLimit(payload.mail.MailFrom.String(), 255),
+			payload.mail.TLS)
+		feeder <- vals
+		payload.savedNotify <- &saveStatus{nil, hash}
+
 	}
 	}
 }
 }
 
 
 func (c *redisClient) redisConnection(redisInterface string) (err error) {
 func (c *redisClient) redisConnection(redisInterface string) (err error) {
-
 	if c.isConnected == false {
 	if c.isConnected == false {
 		c.conn, err = redis.Dial("tcp", redisInterface)
 		c.conn, err = redis.Dial("tcp", redisInterface)
 		if err != nil {
 		if err != nil {
@@ -244,24 +425,18 @@ func (c *redisClient) redisConnection(redisInterface string) (err error) {
 		}
 		}
 		c.isConnected = true
 		c.isConnected = true
 	}
 	}
-
 	return nil
 	return nil
 }
 }
 
 
 // test database connection settings
 // test database connection settings
-func (g *GuerrillaDBAndRedisBackend) testDbConnections() (err error) {
-	db := autorc.New(
-		"tcp",
-		"",
-		g.config.MysqlHost,
-		g.config.MysqlUser,
-		g.config.MysqlPass,
-		g.config.MysqlDB)
-
-	if mysqlErr := db.Raw.Connect(); mysqlErr != nil {
-		err = fmt.Errorf("MySql cannot connect, check your settings: %s", mysqlErr)
+func (g *GuerrillaDBAndRedisBackend) testSettings() (err error) {
+
+	var db *sql.DB
+
+	if db, err = g.mysqlConnect(); err != nil {
+		err = fmt.Errorf("MySql cannot connect, check your settings: %s", err)
 	} else {
 	} else {
-		db.Raw.Close()
+		db.Close()
 	}
 	}
 
 
 	redisClient := &redisClient{}
 	redisClient := &redisClient{}

+ 33 - 0
backends/guerrilla_db_redis_test.go

@@ -0,0 +1,33 @@
+package backends
+
+import (
+	"bytes"
+	"compress/zlib"
+	"fmt"
+	"io"
+	"strings"
+	"testing"
+)
+
+func TestCompressedData(t *testing.T) {
+	var b bytes.Buffer
+	var out bytes.Buffer
+	str := "Hello Hello Hello Hello Hello Hello Hello!"
+	sbj := "Subject:hello\r\n"
+	b.WriteString(str)
+	cd := newCompressedData()
+	cd.set([]byte(sbj), &b)
+
+	// compress
+	fmt.Fprint(&out, cd)
+
+	// decompress
+	var result bytes.Buffer
+	zReader, _ := zlib.NewReader(bytes.NewReader(out.Bytes()))
+	io.Copy(&result, zReader)
+	expect := sbj + str
+	if delta := strings.Compare(expect, result.String()); delta != 0 {
+		t.Error(delta, "compression did match, expected", expect, "but got", result.String())
+	}
+
+}

+ 0 - 109
backends/util.go

@@ -4,16 +4,11 @@ import (
 	"bytes"
 	"bytes"
 	"compress/zlib"
 	"compress/zlib"
 	"crypto/md5"
 	"crypto/md5"
-	"encoding/base64"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
-	"io/ioutil"
 	"net/textproto"
 	"net/textproto"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
-
-	"github.com/sloonz/go-qprintable"
-	"gopkg.in/iconv.v1"
 )
 )
 
 
 // First capturing group is header name, second is header value.
 // First capturing group is header name, second is header value.
@@ -40,110 +35,6 @@ func ParseHeaders(mailData string) map[string]string {
 	return headers
 	return headers
 }
 }
 
 
-var mimeRegex, _ = regexp.Compile(`=\?(.+?)\?([QBqp])\?(.+?)\?=`)
-
-// Decode strings in Mime header format
-// eg. =?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=
-func MimeHeaderDecode(str string) string {
-
-	matched := mimeRegex.FindAllStringSubmatch(str, -1)
-	var charset, encoding, payload string
-	if matched != nil {
-		for i := 0; i < len(matched); i++ {
-			if len(matched[i]) > 2 {
-				charset = matched[i][1]
-				encoding = strings.ToUpper(matched[i][2])
-				payload = matched[i][3]
-				switch encoding {
-				case "B":
-					str = strings.Replace(
-						str,
-						matched[i][0],
-						MailTransportDecode(payload, "base64", charset),
-						1)
-				case "Q":
-					str = strings.Replace(
-						str,
-						matched[i][0],
-						MailTransportDecode(payload, "quoted-printable", charset),
-						1)
-				}
-			}
-		}
-	}
-	return str
-}
-
-// decode from 7bit to 8bit UTF-8
-// encodingType can be "base64" or "quoted-printable"
-func MailTransportDecode(str string, encodingType string, charset string) string {
-	if charset == "" {
-		charset = "UTF-8"
-	} else {
-		charset = strings.ToUpper(charset)
-	}
-	if encodingType == "base64" {
-		str = fromBase64(str)
-	} else if encodingType == "quoted-printable" {
-		str = fromQuotedP(str)
-	}
-
-	if charset != "UTF-8" {
-		charset = fixCharset(charset)
-		// TODO: remove dependency to os-dependent iconv library
-		if cd, err := iconv.Open("UTF-8", charset); err == nil {
-			defer func() {
-				cd.Close()
-				if r := recover(); r != nil {
-					//logln(1, fmt.Sprintf("Recovered in %v", r))
-				}
-			}()
-			// eg. charset can be "ISO-2022-JP"
-			return cd.ConvString(str)
-		}
-
-	}
-	return str
-}
-
-func fromBase64(data string) string {
-	buf := bytes.NewBufferString(data)
-	decoder := base64.NewDecoder(base64.StdEncoding, buf)
-	res, _ := ioutil.ReadAll(decoder)
-	return string(res)
-}
-
-func fromQuotedP(data string) string {
-	buf := bytes.NewBufferString(data)
-	decoder := qprintable.NewDecoder(qprintable.BinaryEncoding, buf)
-	res, _ := ioutil.ReadAll(decoder)
-	return string(res)
-}
-
-var charsetRegex, _ = regexp.Compile(`[_:.\/\\]`)
-
-func fixCharset(charset string) string {
-	fixed_charset := charsetRegex.ReplaceAllString(charset, "-")
-	// Fix charset
-	// borrowed from http://squirrelmail.svn.sourceforge.net/viewvc/squirrelmail/trunk/squirrelmail/include/languages.php?revision=13765&view=markup
-	// OE ks_c_5601_1987 > cp949
-	fixed_charset = strings.Replace(fixed_charset, "ks-c-5601-1987", "cp949", -1)
-	// Moz x-euc-tw > euc-tw
-	fixed_charset = strings.Replace(fixed_charset, "x-euc", "euc", -1)
-	// Moz x-windows-949 > cp949
-	fixed_charset = strings.Replace(fixed_charset, "x-windows_", "cp", -1)
-	// windows-125x and cp125x charsets
-	fixed_charset = strings.Replace(fixed_charset, "windows-", "cp", -1)
-	// ibm > cp
-	fixed_charset = strings.Replace(fixed_charset, "ibm", "cp", -1)
-	// iso-8859-8-i -> iso-8859-8
-	fixed_charset = strings.Replace(fixed_charset, "iso-8859-8-i", "iso-8859-8", -1)
-	if charset != fixed_charset {
-		return fixed_charset
-	}
-	return charset
-}
-
 // returns an md5 hash as string of hex characters
 // returns an md5 hash as string of hex characters
 func MD5Hex(stringArguments ...string) string {
 func MD5Hex(stringArguments ...string) string {
 	h := md5.New()
 	h := md5.New()

+ 120 - 52
client.go

@@ -2,8 +2,13 @@ package guerrilla
 
 
 import (
 import (
 	"bufio"
 	"bufio"
+	"bytes"
+	"crypto/tls"
+	"fmt"
+	"github.com/flashmob/go-guerrilla/envelope"
+	"github.com/flashmob/go-guerrilla/log"
 	"net"
 	"net"
-	"strings"
+	"net/textproto"
 	"sync"
 	"sync"
 	"time"
 	"time"
 )
 )
@@ -25,7 +30,7 @@ const (
 )
 )
 
 
 type client struct {
 type client struct {
-	*Envelope
+	*envelope.Envelope
 	ID          uint64
 	ID          uint64
 	ConnectedAt time.Time
 	ConnectedAt time.Time
 	KilledAt    time.Time
 	KilledAt    time.Time
@@ -34,99 +39,132 @@ type client struct {
 	state        ClientState
 	state        ClientState
 	messagesSent int
 	messagesSent int
 	// Response to be written to the client
 	// Response to be written to the client
-	response  string
-	conn      net.Conn
-	bufin     *smtpBufferedReader
-	bufout    *bufio.Writer
-	timeoutMu sync.Mutex
+	response   bytes.Buffer
+	conn       net.Conn
+	bufin      *smtpBufferedReader
+	bufout     *bufio.Writer
+	smtpReader *textproto.Reader
+	ar         *adjustableLimitedReader
+	// guards access to conn
+	connGuard sync.Mutex
+	log       log.Logger
 }
 }
 
 
-// Email represents a single SMTP message.
-type Envelope struct {
-	// Remote IP address
-	RemoteAddress string
-	// Message sent in EHLO command
-	Helo string
-	// Sender
-	MailFrom *EmailAddress
-	// Recipients
-	RcptTo  []EmailAddress
-	Data    string
-	Subject string
-	TLS     bool
-}
-
-func NewClient(conn net.Conn, clientID uint64) *client {
-	return &client{
+// Allocate a new client
+func NewClient(conn net.Conn, clientID uint64, logger log.Logger) *client {
+	c := &client{
 		conn: conn,
 		conn: conn,
-		Envelope: &Envelope{
-			RemoteAddress: conn.RemoteAddr().String(),
+		Envelope: &envelope.Envelope{
+			RemoteAddress: getRemoteAddr(conn),
 		},
 		},
 		ConnectedAt: time.Now(),
 		ConnectedAt: time.Now(),
 		bufin:       newSMTPBufferedReader(conn),
 		bufin:       newSMTPBufferedReader(conn),
 		bufout:      bufio.NewWriter(conn),
 		bufout:      bufio.NewWriter(conn),
 		ID:          clientID,
 		ID:          clientID,
+		log:         logger,
 	}
 	}
+	// used for reading the DATA state
+	c.smtpReader = textproto.NewReader(c.bufin.Reader)
+	return c
 }
 }
 
 
-func (c *client) responseAdd(r string) {
-	c.response = c.response + r + "\r\n"
+// setResponse adds a response to be written on the next turn
+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
+		c.response.Reset()
+	}
+	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())
+			}
+		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())
+			}
+		}
+	}
+	c.bufout.WriteString("\r\n")
+	if c.log.IsDebug() {
+		c.response.WriteString("\r\n")
+	}
 }
 }
 
 
+// resetTransaction resets the SMTP transaction, ready for the next email (doesn't disconnect)
+// Transaction ends on:
+// -HELO/EHLO/REST command
+// -End of DATA command
+// TLS handhsake
 func (c *client) resetTransaction() {
 func (c *client) resetTransaction() {
-	c.MailFrom = &EmailAddress{}
-	c.RcptTo = []EmailAddress{}
-	c.Data = ""
+	c.MailFrom = envelope.EmailAddress{}
+	c.RcptTo = []envelope.EmailAddress{}
+	c.Data.Reset()
 	c.Subject = ""
 	c.Subject = ""
+	c.Header = nil
 }
 }
 
 
+// isInTransaction returns true if the connection is inside a transaction.
+// A transaction starts after a MAIL command gets issued by the client.
+// Call resetTransaction to end the transaction
 func (c *client) isInTransaction() bool {
 func (c *client) isInTransaction() bool {
-	isMailFromEmpty := *c.MailFrom == (EmailAddress{})
+	isMailFromEmpty := c.MailFrom == (envelope.EmailAddress{})
 	if isMailFromEmpty {
 	if isMailFromEmpty {
 		return false
 		return false
 	}
 	}
 	return true
 	return true
 }
 }
 
 
+// kill flags the connection to close on the next turn
 func (c *client) kill() {
 func (c *client) kill() {
 	c.KilledAt = time.Now()
 	c.KilledAt = time.Now()
 }
 }
 
 
+// isAlive returns true if the client is to close on the next turn
 func (c *client) isAlive() bool {
 func (c *client) isAlive() bool {
 	return c.KilledAt.IsZero()
 	return c.KilledAt.IsZero()
 }
 }
 
 
-func (c *client) scanSubject(reply string) {
-	if c.Subject == "" && (len(reply) > 8) {
-		test := strings.ToUpper(reply[0:9])
-		if i := strings.Index(test, "SUBJECT: "); i == 0 {
-			// first line with \r\n
-			c.Subject = reply[9:]
-		}
-	} else if strings.HasSuffix(c.Subject, "\r\n") {
-		// chop off the \r\n
-		c.Subject = c.Subject[0 : len(c.Subject)-2]
-		if (strings.HasPrefix(reply, " ")) || (strings.HasPrefix(reply, "\t")) {
-			// subject is multi-line
-			c.Subject = c.Subject + reply[1:]
-		}
-	}
-}
-
+// setTimeout adjust the timeout on the connection, goroutine safe
 func (c *client) setTimeout(t time.Duration) {
 func (c *client) setTimeout(t time.Duration) {
-	defer c.timeoutMu.Unlock()
-	c.timeoutMu.Lock()
+	defer c.connGuard.Unlock()
+	c.connGuard.Lock()
 	if c.conn != nil {
 	if c.conn != nil {
 		c.conn.SetDeadline(time.Now().Add(t * time.Second))
 		c.conn.SetDeadline(time.Now().Add(t * time.Second))
 	}
 	}
+}
 
 
+// closeConn closes a client connection, , goroutine safe
+func (c *client) closeConn() {
+	defer c.connGuard.Unlock()
+	c.connGuard.Lock()
+	c.conn.Close()
+	c.conn = nil
 }
 }
 
 
+// init is called after the client is borrowed from the pool, to get it ready for the connection
 func (c *client) init(conn net.Conn, clientID uint64) {
 func (c *client) init(conn net.Conn, clientID uint64) {
 	c.conn = conn
 	c.conn = conn
 	// reset our reader & writer
 	// reset our reader & writer
 	c.bufout.Reset(conn)
 	c.bufout.Reset(conn)
 	c.bufin.Reset(conn)
 	c.bufin.Reset(conn)
+	// reset the data buffer, keep it allocated
+	c.Data.Reset()
 	// reset session data
 	// reset session data
 	c.state = 0
 	c.state = 0
 	c.KilledAt = time.Time{}
 	c.KilledAt = time.Time{}
@@ -134,10 +172,40 @@ func (c *client) init(conn net.Conn, clientID uint64) {
 	c.ID = clientID
 	c.ID = clientID
 	c.TLS = false
 	c.TLS = false
 	c.errors = 0
 	c.errors = 0
-	c.response = ""
 	c.Helo = ""
 	c.Helo = ""
+	c.Header = nil
+	c.RemoteAddress = getRemoteAddr(conn)
+
 }
 }
 
 
+// getID returns the client's unique ID
 func (c *client) getID() uint64 {
 func (c *client) getID() uint64 {
 	return c.ID
 	return c.ID
 }
 }
+
+// UpgradeToTLS upgrades a client connection to TLS
+func (client *client) upgradeToTLS(tlsConfig *tls.Config) error {
+	var tlsConn *tls.Conn
+	// load the config thread-safely
+	tlsConn = tls.Server(client.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
+	return err
+}
+
+func getRemoteAddr(conn net.Conn) string {
+	if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
+		// we just want the IP (not the port)
+		return addr.IP.String()
+	} else {
+		return conn.RemoteAddr().Network()
+	}
+}

+ 3 - 3
cmd/guerrillad/root.go

@@ -1,7 +1,7 @@
 package main
 package main
 
 
 import (
 import (
-	log "github.com/Sirupsen/logrus"
+	"github.com/Sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
@@ -23,9 +23,9 @@ func init() {
 		"print out more debug information")
 		"print out more debug information")
 	rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
 	rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
 		if verbose {
 		if verbose {
-			log.SetLevel(log.DebugLevel)
+			logrus.SetLevel(logrus.DebugLevel)
 		} else {
 		} else {
-			log.SetLevel(log.InfoLevel)
+			logrus.SetLevel(logrus.InfoLevel)
 		}
 		}
 	}
 	}
 }
 }

+ 129 - 58
cmd/guerrillad/serve.go

@@ -8,20 +8,23 @@ import (
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"os/signal"
 	"os/signal"
+	"reflect"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"syscall"
 	"syscall"
 	"time"
 	"time"
 
 
-	log "github.com/Sirupsen/logrus"
-	"github.com/spf13/cobra"
-
 	"github.com/flashmob/go-guerrilla"
 	"github.com/flashmob/go-guerrilla"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/spf13/cobra"
+)
+
+const (
+	defaultPidFile = "/var/run/go-guerrilla.pid"
 )
 )
 
 
 var (
 var (
-	iface      string
 	configPath string
 	configPath string
 	pidFile    string
 	pidFile    string
 
 
@@ -33,13 +36,20 @@ var (
 
 
 	cmdConfig     = CmdConfig{}
 	cmdConfig     = CmdConfig{}
 	signalChannel = make(chan os.Signal, 1) // for trapping SIG_HUP
 	signalChannel = make(chan os.Signal, 1) // for trapping SIG_HUP
+	mainlog       log.Logger
 )
 )
 
 
 func init() {
 func init() {
+	// log to stderr on startup
+	var logOpenError error
+	if mainlog, logOpenError = log.GetLogger(log.OutputStderr.String()); logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger to %s", log.OutputStderr)
+	}
 	serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
 	serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
 		"goguerrilla.conf", "Path to the configuration file")
 		"goguerrilla.conf", "Path to the configuration file")
-	serveCmd.PersistentFlags().StringVarP(&pidFile, "pid-file", "p",
-		"/var/run/go-guerrilla.pid", "Path to the pid file")
+	// intentionally didn't specify default pidFile; value from config is used if flag is empty
+	serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
+		"", "Path to the pid file")
 
 
 	rootCmd.AddCommand(serveCmd)
 	rootCmd.AddCommand(serveCmd)
 }
 }
@@ -49,32 +59,55 @@ func sigHandler(app guerrilla.Guerrilla) {
 	signal.Notify(signalChannel, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGKILL)
 	signal.Notify(signalChannel, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGKILL)
 
 
 	for sig := range signalChannel {
 	for sig := range signalChannel {
-
 		if sig == syscall.SIGHUP {
 		if sig == syscall.SIGHUP {
-			err := readConfig(configPath, verbose, &cmdConfig)
+			// save old config & load in new one
+			oldConfig := cmdConfig
+			newConfig := CmdConfig{}
+			err := readConfig(configPath, pidFile, &newConfig)
 			if err != nil {
 			if err != nil {
-				log.WithError(err).Error("Error while ReadConfig (reload)")
+				mainlog.WithError(err).Error("Error while ReadConfig (reload)")
 			} else {
 			} else {
-				log.Infof("Configuration is reloaded at %s", guerrilla.ConfigLoadTime)
+				cmdConfig = newConfig
+				mainlog.Infof("Configuration was reloaded at %s", guerrilla.ConfigLoadTime)
+				cmdConfig.emitChangeEvents(&oldConfig, app)
 			}
 			}
-			// TODO: reinitialize
 		} else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT {
 		} else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT {
-			log.Infof("Shutdown signal caught")
+			mainlog.Infof("Shutdown signal caught")
 			app.Shutdown()
 			app.Shutdown()
-			log.Infof("Shutdown completd, exiting.")
-			os.Exit(0)
+			mainlog.Infof("Shutdown completed, exiting.")
+			return
 		} else {
 		} else {
-			os.Exit(0)
+			mainlog.Infof("Shutdown, unknown signal caught")
+			return
 		}
 		}
 	}
 	}
 }
 }
 
 
+func subscribeBackendEvent(event string, backend backends.Backend, app guerrilla.Guerrilla) {
+
+	app.Subscribe(event, func(cmdConfig *CmdConfig) {
+		logger, _ := log.GetLogger(cmdConfig.LogFile)
+		var err error
+		if err = backend.Shutdown(); err != nil {
+			logger.WithError(err).Warn("Backend failed to shutdown")
+			return
+		}
+		backend, err = backends.New(cmdConfig.BackendName, cmdConfig.BackendConfig, logger)
+		if err != nil {
+			logger.WithError(err).Fatalf("Error while loading the backend %q",
+				cmdConfig.BackendName)
+		} else {
+			logger.Info("Backend started:", cmdConfig.BackendName)
+		}
+	})
+}
+
 func serve(cmd *cobra.Command, args []string) {
 func serve(cmd *cobra.Command, args []string) {
 	logVersion()
 	logVersion()
 
 
-	err := readConfig(configPath, verbose, &cmdConfig)
+	err := readConfig(configPath, pidFile, &cmdConfig)
 	if err != nil {
 	if err != nil {
-		log.WithError(err).Fatal("Error while reading config")
+		mainlog.WithError(err).Fatal("Error while reading config")
 	}
 	}
 
 
 	// Check that max clients is not greater than system open file limit.
 	// Check that max clients is not greater than system open file limit.
@@ -86,47 +119,46 @@ func serve(cmd *cobra.Command, args []string) {
 			maxClients += s.MaxClients
 			maxClients += s.MaxClients
 		}
 		}
 		if maxClients > fileLimit {
 		if maxClients > fileLimit {
-			log.Warnf("Combined max clients for all servers (%d) is greater than open file limit (%d). "+
+			mainlog.Warnf("Combined max clients for all servers (%d) is greater than open file limit (%d). "+
 				"Please increase your open file limit or decrease max clients.", maxClients, fileLimit)
 				"Please increase your open file limit or decrease max clients.", maxClients, fileLimit)
 		}
 		}
 	}
 	}
 
 
-	// Write out our PID
-	if len(pidFile) > 0 {
-		if f, err := os.Create(pidFile); err == nil {
-			defer f.Close()
-			if _, err := f.WriteString(fmt.Sprintf("%d", os.Getpid())); err == nil {
-				f.Sync()
-			} else {
-				log.WithError(err).Fatalf("Error while writing pidFile (%s)", pidFile)
-			}
-		} else {
-			log.WithError(err).Fatalf("Error while creating pidFile (%s)", pidFile)
-		}
+	// Backend setup
+	var backend backends.Backend
+	backend, err = backends.New(cmdConfig.BackendName, cmdConfig.BackendConfig, mainlog)
+	if err != nil {
+		mainlog.WithError(err).Fatalf("Error while loading the backend %q",
+			cmdConfig.BackendName)
 	}
 	}
-	var backend guerrilla.Backend
-	switch cmdConfig.BackendName {
-	case "dummy":
-		b := &backends.DummyBackend{}
-		b.Initialize(cmdConfig.BackendConfig)
-		backend = guerrilla.Backend(b)
-	case "guerrilla-db-redis":
-		b := &backends.GuerrillaDBAndRedisBackend{}
-		err = b.Initialize(cmdConfig.BackendConfig)
-		if err != nil {
-			log.WithError(err).Errorf("Initalization of %s backend failed", cmdConfig.BackendName)
-		}
 
 
-		backend = guerrilla.Backend(b)
-	default:
-		log.Fatalf("Unknown backend: %s", cmdConfig.BackendName)
+	app, err := guerrilla.New(&cmdConfig.AppConfig, backend, mainlog)
+	if err != nil {
+		mainlog.WithError(err).Error("Error(s) when creating new server(s)")
 	}
 	}
-	b := &backends.GuerrillaDBAndRedisBackend{}
-	err = b.Initialize(cmdConfig.BackendConfig)
 
 
-	app := guerrilla.New(&cmdConfig.AppConfig, &backend)
-	go app.Start()
+	// start the app
+	err = app.Start()
+	if err != nil {
+		mainlog.WithError(err).Error("Error(s) when starting server(s)")
+	}
+	subscribeBackendEvent("config_change:backend_config", backend, app)
+	subscribeBackendEvent("config_change:backend_name", backend, app)
+	// Write out our PID
+	writePid(cmdConfig.PidFile)
+	// ...and write out our pid whenever the file name changes in the config
+	app.Subscribe("config_change:pid_file", func(ac *guerrilla.AppConfig) {
+		writePid(ac.PidFile)
+	})
+	// change the logger from stdrerr to one from config
+	mainlog.Infof("main log configured to %s", cmdConfig.LogFile)
+	var logOpenError error
+	if mainlog, logOpenError = log.GetLogger(cmdConfig.LogFile); logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed changing to a custom logger [%s]", cmdConfig.LogFile)
+	}
+	app.SetLogger(mainlog)
 	sigHandler(app)
 	sigHandler(app)
+
 }
 }
 
 
 // Superset of `guerrilla.AppConfig` containing options specific
 // Superset of `guerrilla.AppConfig` containing options specific
@@ -134,26 +166,50 @@ func serve(cmd *cobra.Command, args []string) {
 type CmdConfig struct {
 type CmdConfig struct {
 	guerrilla.AppConfig
 	guerrilla.AppConfig
 	BackendName   string                 `json:"backend_name"`
 	BackendName   string                 `json:"backend_name"`
-	BackendConfig map[string]interface{} `json:"backend_config"`
+	BackendConfig backends.BackendConfig `json:"backend_config"`
+}
+
+func (c *CmdConfig) load(jsonBytes []byte) error {
+	c.AppConfig.Load(jsonBytes)
+	err := json.Unmarshal(jsonBytes, &c)
+	if err != nil {
+		return fmt.Errorf("Could not parse config file: %s", err.Error())
+	}
+	return nil
+}
+
+func (c *CmdConfig) emitChangeEvents(oldConfig *CmdConfig, app guerrilla.Guerrilla) {
+	// has backend changed?
+	if !reflect.DeepEqual((*c).BackendConfig, (*oldConfig).BackendConfig) {
+		app.Publish("config_change:backend_config", c)
+	}
+	if c.BackendName != oldConfig.BackendName {
+		app.Publish("config_change:backend_name", c)
+	}
+	// call other emitChangeEvents
+	c.AppConfig.EmitChangeEvents(&oldConfig.AppConfig, app)
 }
 }
 
 
 // ReadConfig which should be called at startup, or when a SIG_HUP is caught
 // ReadConfig which should be called at startup, or when a SIG_HUP is caught
-func readConfig(path string, verbose bool, config *CmdConfig) error {
+func readConfig(path string, pidFile string, config *CmdConfig) error {
 	// load in the config.
 	// load in the config.
 	data, err := ioutil.ReadFile(path)
 	data, err := ioutil.ReadFile(path)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not read config file: %s", err.Error())
 		return fmt.Errorf("Could not read config file: %s", err.Error())
 	}
 	}
-
-	err = json.Unmarshal(data, &config)
-	if err != nil {
-		return fmt.Errorf("Could not parse config file: %s", err.Error())
+	if err := config.load(data); err != nil {
+		return err
+	}
+	// override config pidFile with with flag from the command line
+	if len(pidFile) > 0 {
+		config.AppConfig.PidFile = pidFile
+	} else if len(config.AppConfig.PidFile) == 0 {
+		config.AppConfig.PidFile = defaultPidFile
 	}
 	}
 
 
 	if len(config.AllowedHosts) == 0 {
 	if len(config.AllowedHosts) == 0 {
 		return errors.New("Empty `allowed_hosts` is not allowed")
 		return errors.New("Empty `allowed_hosts` is not allowed")
 	}
 	}
-
 	guerrilla.ConfigLoadTime = time.Now()
 	guerrilla.ConfigLoadTime = time.Now()
 	return nil
 	return nil
 }
 }
@@ -164,11 +220,26 @@ func getFileLimit() int {
 	if err != nil {
 	if err != nil {
 		return -1
 		return -1
 	}
 	}
-
 	limit, err := strconv.Atoi(strings.TrimSpace(string(out)))
 	limit, err := strconv.Atoi(strings.TrimSpace(string(out)))
 	if err != nil {
 	if err != nil {
 		return -1
 		return -1
 	}
 	}
-
 	return limit
 	return limit
 }
 }
+
+func writePid(pidFile string) {
+	if len(pidFile) > 0 {
+		if f, err := os.Create(pidFile); err == nil {
+			defer f.Close()
+			pid := os.Getpid()
+			if _, err := f.WriteString(fmt.Sprintf("%d", pid)); err == nil {
+				f.Sync()
+				mainlog.Infof("pid_file (%s) written with pid:%v", pidFile, pid)
+			} else {
+				mainlog.WithError(err).Fatalf("Error while writing pidFile (%s)", pidFile)
+			}
+		} else {
+			mainlog.WithError(err).Fatalf("Error while creating pidFile (%s)", pidFile)
+		}
+	}
+}

+ 1046 - 0
cmd/guerrillad/serve_test.go

@@ -0,0 +1,1046 @@
+package main
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"runtime"
+	"strconv"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+	test "github.com/flashmob/go-guerrilla/tests"
+	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"github.com/spf13/cobra"
+)
+
+var configJsonA = `
+{
+    "log_file" : "../../tests/testlog",
+    "log_level" : "debug",
+    "pid_file" : "./pidfile.pid",
+    "allowed_hosts": [
+      "guerrillamail.com",
+      "guerrillamailblock.com",
+      "sharklasers.com",
+      "guerrillamail.net",
+      "guerrillamail.org"
+    ],
+    "backend_name": "dummy",
+    "backend_config": {
+        "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"
+        },
+        {
+            "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"
+        }
+    ]
+}
+`
+
+// backend config changed, log_received_mails is false
+var configJsonB = `
+{
+    "log_file" : "../../tests/testlog",
+    "log_level" : "debug",
+    "pid_file" : "./pidfile2.pid",
+    "allowed_hosts": [
+      "guerrillamail.com",
+      "guerrillamailblock.com",
+      "sharklasers.com",
+      "guerrillamail.net",
+      "guerrillamail.org"
+    ],
+    "backend_name": "dummy",
+    "backend_config": {
+        "log_received_mails": false
+    },
+    "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"
+        }
+    ]
+}
+`
+
+// backend_name changed, is guerrilla-redis-db + added a server
+var configJsonC = `
+{
+    "log_file" : "../../tests/testlog",
+    "log_level" : "debug",
+    "pid_file" : "./pidfile.pid",
+    "allowed_hosts": [
+      "guerrillamail.com",
+      "guerrillamailblock.com",
+      "sharklasers.com",
+      "guerrillamail.net",
+      "guerrillamail.org"
+    ],
+    "backend_name": "guerrilla-redis-db",
+    "backend_config" :
+        {
+            "mysql_db":"gmail_mail",
+            "mysql_host":"127.0.0.1:3306",
+            "mysql_pass":"ok",
+            "mysql_user":"root",
+            "mail_table":"new_mail",
+            "redis_interface" : "127.0.0.1:6379",
+            "redis_expire_seconds" : 7200,
+            "save_workers_size" : 3,
+            "primary_mail_host":"sharklasers.com"
+        },
+    "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"
+        },
+        {
+            "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"
+        }
+    ]
+}
+`
+
+// adds 127.0.0.1:4655, a secure server
+var configJsonD = `
+{
+    "log_file" : "../../tests/testlog",
+    "log_level" : "debug",
+    "pid_file" : "./pidfile.pid",
+    "allowed_hosts": [
+      "guerrillamail.com",
+      "guerrillamailblock.com",
+      "sharklasers.com",
+      "guerrillamail.net",
+      "guerrillamail.org"
+    ],
+    "backend_name": "dummy",
+    "backend_config": {
+        "log_received_mails": false
+    },
+    "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:2552",
+            "start_tls_on":true,
+            "tls_always_on":false,
+            "max_clients": 1000,
+            "log_file" : "../../tests/testlog"
+        },
+        {
+            "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"
+        }
+    ]
+}
+`
+
+const testPauseDuration = time.Millisecond * 600
+
+// reload config
+func sigHup() {
+	if data, err := ioutil.ReadFile("pidfile.pid"); err == nil {
+		mainlog.Infof("pid read is %s", data)
+		ecmd := exec.Command("kill", "-HUP", string(data))
+		_, err = ecmd.Output()
+		if err != nil {
+			mainlog.Infof("could not SIGHUP", err)
+		}
+	} else {
+		mainlog.WithError(err).Info("sighup - Could not read pidfle")
+	}
+
+}
+
+// shutdown after calling serve()
+func sigKill() {
+	if data, err := ioutil.ReadFile("pidfile.pid"); err == nil {
+		mainlog.Infof("pid read is %s", data)
+		ecmd := exec.Command("kill", string(data))
+		_, err = ecmd.Output()
+		if err != nil {
+			mainlog.Infof("could not sigkill", err)
+		}
+	} else {
+		mainlog.WithError(err).Info("sigKill - Could not read pidfle")
+	}
+
+}
+
+// make sure that we get all the config change events
+func TestCmdConfigChangeEvents(t *testing.T) {
+
+	oldconf := &CmdConfig{}
+	oldconf.load([]byte(configJsonA))
+
+	newconf := &CmdConfig{}
+	newconf.load([]byte(configJsonB))
+
+	newerconf := &CmdConfig{}
+	newerconf.load([]byte(configJsonC))
+
+	expectedEvents := map[string]bool{
+		"config_change:backend_config": false,
+		"config_change:backend_name":   false,
+		"server_change:new_server":     false,
+	}
+	mainlog, _ = log.GetLogger("off")
+
+	bcfg := backends.BackendConfig{"log_received_mails": true}
+	backend, err := backends.New("dummy", bcfg, mainlog)
+	app, err := guerrilla.New(&oldconf.AppConfig, backend, mainlog)
+	if err != nil {
+		//log.Info("Failed to create new app", err)
+	}
+	toUnsubscribe := map[string]func(c *CmdConfig){}
+	toUnsubscribeS := map[string]func(c *guerrilla.ServerConfig){}
+
+	for event := range expectedEvents {
+		// Put in anon func since range is overwriting event
+		func(e string) {
+
+			if strings.Index(e, "server_change") == 0 {
+				f := func(c *guerrilla.ServerConfig) {
+					expectedEvents[e] = true
+				}
+				app.Subscribe(event, f)
+				toUnsubscribeS[event] = f
+			} else {
+				f := func(c *CmdConfig) {
+					expectedEvents[e] = true
+				}
+				app.Subscribe(event, f)
+				toUnsubscribe[event] = f
+			}
+
+		}(event)
+	}
+
+	// emit events
+	newconf.emitChangeEvents(oldconf, app)
+	newerconf.emitChangeEvents(newconf, app)
+	// unsubscribe
+	for unevent, unfun := range toUnsubscribe {
+		app.Unsubscribe(unevent, unfun)
+	}
+
+	for event, val := range expectedEvents {
+		if val == false {
+			t.Error("Did not fire config change event:", event)
+			t.FailNow()
+			break
+		}
+	}
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+
+}
+
+// start server, change config, send SIG HUP, confirm that the pidfile changed & backend reloaded
+func TestServe(t *testing.T) {
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+
+	mainlog, _ = log.GetLogger("../../tests/testlog")
+
+	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
+	cmd := &cobra.Command{}
+	configPath = "configJsonA.json"
+	var serveWG sync.WaitGroup
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+
+	data, err := ioutil.ReadFile("pidfile.pid")
+	if err != nil {
+		t.Error("error reading pidfile.pid", err)
+		t.FailNow()
+	}
+	_, err = strconv.Atoi(string(data))
+	if err != nil {
+		t.Error("could not parse pidfile.pid", err)
+		t.FailNow()
+	}
+
+	// change the config file
+	ioutil.WriteFile("configJsonA.json", []byte(configJsonB), 0644)
+
+	// test SIGHUP via the kill command
+	// Would not work on windows as kill is not available.
+	// TODO: Implement an alternative test for windows.
+	if runtime.GOOS != "windows" {
+		ecmd := exec.Command("kill", "-HUP", string(data))
+		_, err = ecmd.Output()
+		if err != nil {
+			t.Error("could not SIGHUP", err)
+			t.FailNow()
+		}
+		time.Sleep(testPauseDuration) // allow sighup to do its job
+		// did the pidfile change as expected?
+		if _, err := os.Stat("./pidfile2.pid"); os.IsNotExist(err) {
+			t.Error("pidfile not changed after sighup SIGHUP", err)
+		}
+	}
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+
+	// did backend started as expected?
+	fd, err := os.Open("../../tests/testlog")
+	if err != nil {
+		t.Error(err)
+	}
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		if i := strings.Index(logOutput, "Backend started:dummy"); i < 0 {
+			t.Error("Dummy backend not restared")
+		}
+	}
+
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonA.json")
+	os.Remove("./pidfile.pid")
+	os.Remove("./pidfile2.pid")
+
+}
+
+// Start with configJsonA.json,
+// then add a new server to it (127.0.0.1:2526),
+// then SIGHUP (to reload config & trigger config update events),
+// then connect to it & HELO.
+func TestServerAddEvent(t *testing.T) {
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	mainlog, _ = log.GetLogger("../../tests/testlog")
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
+	cmd := &cobra.Command{}
+	configPath = "configJsonA.json"
+	var serveWG sync.WaitGroup
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration) // allow the server to start
+	// now change the config by adding a server
+	conf := &CmdConfig{}                                 // blank one
+	conf.load([]byte(configJsonA))                       // load configJsonA
+	newServer := conf.Servers[0]                         // copy the first server config
+	newServer.ListenInterface = "127.0.0.1:2526"         // change it
+	newConf := conf                                      // copy the cmdConfg
+	newConf.Servers = append(newConf.Servers, newServer) // add the new server
+	if jsonbytes, err := json.Marshal(newConf); err == nil {
+		//fmt.Println(string(jsonbytes))
+		ioutil.WriteFile("configJsonA.json", []byte(jsonbytes), 0644)
+	}
+	// send a sighup signal to the server
+	sigHup()
+	time.Sleep(testPauseDuration) // pause for config to reload
+
+	if conn, buffin, err := test.Connect(newServer, 20); err != nil {
+		t.Error("Could not connect to new server", newServer.ListenInterface)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 mail.test.com Hello"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			}
+		} else {
+			t.Error(err)
+		}
+	}
+
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "New server added [127.0.0.1:2526]"); i < 0 {
+			t.Error("Did not add [127.0.0.1:2526], most likely because Bus.Subscribe(\"server_change:new_server\" didnt fire")
+		}
+	}
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonA.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// Start with configJsonA.json,
+// then change the config to enable 127.0.0.1:2228,
+// then write the new config,
+// then SIGHUP (to reload config & trigger config update events),
+// then connect to 127.0.0.1:2228 & HELO.
+func TestServerStartEvent(t *testing.T) {
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	mainlog, _ = log.GetLogger("../../tests/testlog")
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
+	cmd := &cobra.Command{}
+	configPath = "configJsonA.json"
+	var serveWG sync.WaitGroup
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+	// now change the config by adding a server
+	conf := &CmdConfig{}           // blank one
+	conf.load([]byte(configJsonA)) // load configJsonA
+
+	newConf := conf // copy the cmdConfg
+	newConf.Servers[1].IsEnabled = true
+	if jsonbytes, err := json.Marshal(newConf); err == nil {
+		//fmt.Println(string(jsonbytes))
+		ioutil.WriteFile("configJsonA.json", []byte(jsonbytes), 0644)
+	} else {
+		t.Error(err)
+	}
+	// send a sighup signal to the server
+	sigHup()
+	time.Sleep(testPauseDuration) // pause for config to reload
+
+	if conn, buffin, err := test.Connect(newConf.Servers[1], 20); err != nil {
+		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 enable.test.com Hello"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			}
+		} else {
+			t.Error(err)
+		}
+	}
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "Starting server [127.0.0.1:2228]"); i < 0 {
+			t.Error("did not add [127.0.0.1:2228], most likely because Bus.Subscribe(\"server_change:start_server\" didnt fire")
+		}
+	}
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonA.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// Start with configJsonA.json,
+// then change the config to enable 127.0.0.1:2228,
+// then write the new config,
+// then SIGHUP (to reload config & trigger config update events),
+// then connect to 127.0.0.1:2228 & HELO.
+// then change the config to dsiable 127.0.0.1:2228,
+// then SIGHUP (to reload config & trigger config update events),
+// then connect to 127.0.0.1:2228 - it should not connect
+
+func TestServerStopEvent(t *testing.T) {
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	mainlog, _ = log.GetLogger("../../tests/testlog")
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
+	cmd := &cobra.Command{}
+	configPath = "configJsonA.json"
+	var serveWG sync.WaitGroup
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+	// now change the config by enabling a server
+	conf := &CmdConfig{}           // blank one
+	conf.load([]byte(configJsonA)) // load configJsonA
+
+	newConf := conf // copy the cmdConfg
+	newConf.Servers[1].IsEnabled = true
+	if jsonbytes, err := json.Marshal(newConf); err == nil {
+		//fmt.Println(string(jsonbytes))
+		ioutil.WriteFile("configJsonA.json", []byte(jsonbytes), 0644)
+	} else {
+		t.Error(err)
+	}
+	// send a sighup signal to the server
+	sigHup()
+	time.Sleep(testPauseDuration) // pause for config to reload
+
+	if conn, buffin, err := test.Connect(newConf.Servers[1], 20); err != nil {
+		t.Error("Could not connect to new server", newConf.Servers[1].ListenInterface)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 enable.test.com Hello"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			}
+		} else {
+			t.Error(err)
+		}
+		conn.Close()
+	}
+	// now disable the server
+	newerConf := newConf // copy the cmdConfg
+	newerConf.Servers[1].IsEnabled = false
+	if jsonbytes, err := json.Marshal(newerConf); err == nil {
+		//fmt.Println(string(jsonbytes))
+		ioutil.WriteFile("configJsonA.json", []byte(jsonbytes), 0644)
+	} else {
+		t.Error(err)
+	}
+	// send a sighup signal to the server
+	sigHup()
+	time.Sleep(testPauseDuration) // pause for config to reload
+
+	// it should not connect to the server
+	if _, _, err := test.Connect(newConf.Servers[1], 20); err == nil {
+		t.Error("127.0.0.1:2228 was disabled, but still accepting connections", newConf.Servers[1].ListenInterface)
+	}
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "Server [127.0.0.1:2228] stopped"); i < 0 {
+			t.Error("did not stop [127.0.0.1:2228], most likely because Bus.Subscribe(\"server_change:stop_server\" didnt fire")
+		}
+	}
+
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonA.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// 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
+// then write the new config,
+// then SIGHUP (to reload config & trigger config update events),
+// connect to 127.0.0.1:4655 & HELO & try RCPT TO, grr.la should work
+
+func TestAllowedHostsEvent(t *testing.T) {
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	mainlog, _ = log.GetLogger("../../tests/testlog")
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
+	conf := &CmdConfig{}           // blank one
+	conf.load([]byte(configJsonD)) // load configJsonD
+	cmd := &cobra.Command{}
+	configPath = "configJsonD.json"
+	var serveWG sync.WaitGroup
+	time.Sleep(testPauseDuration)
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+
+	// now connect and try RCPT TO with an invalid host
+	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[1], 20); err != nil {
+		t.Error("Could not connect to new server", conf.AppConfig.Servers[1].ListenInterface, err)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 secure.test.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 := "454 4.1.1 Error: Relay access denied: grr.la"
+					if strings.Index(result, expect) != 0 {
+						t.Error("Expected:", expect, "but got:", result)
+					}
+				}
+			}
+		}
+		conn.Close()
+	}
+
+	// now change the config by adding a host to allowed hosts
+
+	newConf := conf // copy the cmdConfg
+	newConf.AllowedHosts = append(newConf.AllowedHosts, "grr.la")
+	if jsonbytes, err := json.Marshal(newConf); err == nil {
+		ioutil.WriteFile("configJsonD.json", []byte(jsonbytes), 0644)
+	} else {
+		t.Error(err)
+	}
+	// send a sighup signal to the server to reload config
+	sigHup()
+	time.Sleep(testPauseDuration) // pause for config to reload
+
+	// now repeat the same conversion, RCPT TO should be accepted
+	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[1], 20); err != nil {
+		t.Error("Could not connect to new server", conf.AppConfig.Servers[1].ListenInterface, err)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 secure.test.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()
+	}
+
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "allowed_hosts config changed, a new list was set"); i < 0 {
+			t.Error("did not change allowed_hosts, most likely because Bus.Subscribe(\"config_change:allowed_hosts\" didnt fire")
+		}
+	}
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonD.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// Test TLS config change event
+// start with configJsonD
+// should be able to STARTTLS to 127.0.0.1:2525 with no problems
+// generate new certs & reload config
+// should get a new tls event & able to STARTTLS with no problem
+
+func TestTLSConfigEvent(t *testing.T) {
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	// pause for generated cert to output on slow machines
+	time.Sleep(testPauseDuration)
+	// did cert output?
+	if _, err := os.Stat("../../tests/mail2.guerrillamail.com.cert.pem"); err != nil {
+		t.Error("Did not create cert ", err)
+	}
+	mainlog, _ = log.GetLogger("../../tests/testlog")
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
+	conf := &CmdConfig{}           // blank one
+	conf.load([]byte(configJsonD)) // load configJsonD
+	cmd := &cobra.Command{}
+	configPath = "configJsonD.json"
+	var serveWG sync.WaitGroup
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+
+	// Test STARTTLS handshake
+	testTlsHandshake := func() {
+		if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
+			t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+		} else {
+			if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+				expect := "250 mail.test.com Hello"
+				if strings.Index(result, expect) != 0 {
+					t.Error("Expected", expect, "but got", result)
+				} else {
+					if result, err = test.Command(conn, buffin, "STARTTLS"); err == nil {
+						expect := "220 2.0.0 Ready to start TLS"
+						if strings.Index(result, expect) != 0 {
+							t.Error("Expected:", expect, "but got:", result)
+						} else {
+							tlsConn := tls.Client(conn, &tls.Config{
+								InsecureSkipVerify: true,
+								ServerName:         "127.0.0.1",
+							})
+							if err := tlsConn.Handshake(); err != nil {
+								t.Error("Failed to handshake", conf.AppConfig.Servers[0].ListenInterface)
+							} else {
+								conn = tlsConn
+								mainlog.Info("TLS Handshake succeeded")
+							}
+
+						}
+					}
+				}
+			}
+			conn.Close()
+		}
+	}
+	testTlsHandshake()
+
+	if err := os.Remove("../../tests/mail2.guerrillamail.com.cert.pem"); err != nil {
+		t.Error("could not remove cert", err)
+	}
+	if err := os.Remove("../../tests/mail2.guerrillamail.com.key.pem"); err != nil {
+		t.Error("could not remove key", err)
+	}
+
+	// generate a new cert
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	// pause for generated cert to output
+	time.Sleep(testPauseDuration)
+	// did cert output?
+	if _, err := os.Stat("../../tests/mail2.guerrillamail.com.cert.pem"); err != nil {
+		t.Error("Did not create cert ", err)
+	}
+
+	sigHup()
+
+	time.Sleep(testPauseDuration * 2) // pause for config to reload
+	testTlsHandshake()
+
+	//time.Sleep(testPauseDuration)
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "Server [127.0.0.1:2552] new TLS configuration loaded"); i < 0 {
+			t.Error("did not change tls, most likely because Bus.Subscribe(\"server_change:tls_config\" didnt fire")
+		}
+	}
+
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonD.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// Test for missing TLS certificate, when starting or config reload
+
+func TestBadTLS(t *testing.T) {
+	mainlog, _ = log.GetLogger("../../tests/testlog")
+	if err := os.Remove("./../../tests/mail2.guerrillamail.com.cert.pem"); err != nil {
+		mainlog.WithError(err).Error("could not remove ./../../tests/mail2.guerrillamail.com.cert.pem")
+	} else {
+		mainlog.Info("removed ./../../tests/mail2.guerrillamail.com.cert.pem")
+	}
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
+	conf := &CmdConfig{}           // blank one
+	conf.load([]byte(configJsonD)) // load configJsonD
+	conf.Servers[0].Timeout = 1
+	cmd := &cobra.Command{}
+	configPath = "configJsonD.json"
+	var serveWG sync.WaitGroup
+
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+
+	// Test STARTTLS handshake
+	testTlsHandshake := func() {
+		if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
+			t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+		} else {
+			conn.SetDeadline(time.Now().Add(time.Second))
+			if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+				expect := "250 mail.test.com Hello"
+				if strings.Index(result, expect) != 0 {
+					t.Error("Expected", expect, "but got", result)
+				} else {
+					if result, err = test.Command(conn, buffin, "STARTTLS"); err == nil {
+						expect := "220 2.0.0 Ready to start TLS"
+						if strings.Index(result, expect) != 0 {
+							t.Error("Expected:", expect, "but got:", result)
+						} else {
+							tlsConn := tls.Client(conn, &tls.Config{
+								InsecureSkipVerify: true,
+								ServerName:         "127.0.0.1",
+							})
+							if err := tlsConn.Handshake(); err != nil {
+								mainlog.Info("TLS Handshake failed")
+							} else {
+								t.Error("Handshake succeeded, expected it to fail", conf.AppConfig.Servers[0].ListenInterface)
+								conn = tlsConn
+
+							}
+
+						}
+					}
+				}
+			}
+			conn.Close()
+		}
+	}
+	testTlsHandshake()
+
+	// write some trash data
+	ioutil.WriteFile("./../../tests/mail2.guerrillamail.com.cert.pem", []byte("trash data"), 0664)
+	ioutil.WriteFile("./../../tests/mail2.guerrillamail.com.key.pem", []byte("trash data"), 0664)
+
+	// generate a new cert
+	//testcert.GenerateCert("mail2.guerrillamail.com", "", 365 * 24 * time.Hour, false, 2048, "P256", "../../tests/")
+	sigHup()
+
+	time.Sleep(testPauseDuration) // pause for config to reload
+	testTlsHandshake()
+
+	time.Sleep(testPauseDuration)
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "failed to load the new TLS configuration"); i < 0 {
+			t.Error("did not detect TLS load failure")
+		}
+	}
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonD.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// Test for when the server config Timeout value changes
+// Start with configJsonD.json
+
+func TestSetTimeoutEvent(t *testing.T) {
+	//mainlog, _ = log.GetLogger("../../tests/testlog")
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
+	conf := &CmdConfig{}           // blank one
+	conf.load([]byte(configJsonD)) // load configJsonD
+	cmd := &cobra.Command{}
+	configPath = "configJsonD.json"
+	var serveWG sync.WaitGroup
+
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+
+	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 mail.test.com Hello"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			}
+		}
+	}
+	// set the timeout to 1 second
+
+	newConf := conf // copy the cmdConfg
+	newConf.Servers[0].Timeout = 1
+	if jsonbytes, err := json.Marshal(newConf); err == nil {
+		ioutil.WriteFile("configJsonD.json", []byte(jsonbytes), 0644)
+	} else {
+		t.Error(err)
+	}
+	// send a sighup signal to the server to reload config
+	sigHup()
+	time.Sleep(time.Millisecond * 1200) // pause for connection to timeout
+
+	// so the connection we have opened should timeout by now
+
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "i/o timeout"); i < 0 {
+			t.Error("Connection to 127.0.0.1:2552 didn't timeout as expected")
+		}
+	}
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonD.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// Test debug level config change
+// Start in log_level = debug
+// Load config & start server
+func TestDebugLevelChange(t *testing.T) {
+	//mainlog, _ = log.GetLogger("../../tests/testlog")
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	// start the server by emulating the serve command
+	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
+	conf := &CmdConfig{}           // blank one
+	conf.load([]byte(configJsonD)) // load configJsonD
+	conf.LogLevel = "debug"
+	cmd := &cobra.Command{}
+	configPath = "configJsonD.json"
+	var serveWG sync.WaitGroup
+
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(testPauseDuration)
+
+	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+	} else {
+		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
+			expect := "250 mail.test.com Hello"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			}
+		}
+		conn.Close()
+	}
+	// set the log_level to info
+
+	newConf := conf // copy the cmdConfg
+	newConf.LogLevel = "info"
+	if jsonbytes, err := json.Marshal(newConf); err == nil {
+		ioutil.WriteFile("configJsonD.json", []byte(jsonbytes), 0644)
+	} else {
+		t.Error(err)
+	}
+	// send a sighup signal to the server to reload config
+	sigHup()
+	time.Sleep(testPauseDuration) // log to change
+
+	// connect again, this time we should see info
+	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+	} else {
+		if result, err := test.Command(conn, buffin, "NOOP"); err == nil {
+			expect := "200 2.0.0 OK"
+			if strings.Index(result, expect) != 0 {
+				t.Error("Expected", expect, "but got", result)
+			}
+		}
+		conn.Close()
+	}
+
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	// did backend started as expected?
+	fd, _ := os.Open("../../tests/testlog")
+	if read, err := ioutil.ReadAll(fd); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "log level changed to [info]"); i < 0 {
+			t.Error("Log level did not change to [info]")
+		}
+		// This should not be there:
+		if i := strings.Index(logOutput, "Client sent: NOOP"); i != -1 {
+			t.Error("Log level did not change to [info], we are still seeing debug messages")
+		}
+	}
+	// cleanup
+	os.Truncate("../../tests/testlog", 0)
+	os.Remove("configJsonD.json")
+	os.Remove("./pidfile.pid")
+
+}

+ 3 - 4
cmd/guerrillad/version.go

@@ -1,7 +1,6 @@
 package main
 package main
 
 
 import (
 import (
-	log "github.com/Sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 
 
 	guerrilla "github.com/flashmob/go-guerrilla"
 	guerrilla "github.com/flashmob/go-guerrilla"
@@ -21,7 +20,7 @@ func init() {
 }
 }
 
 
 func logVersion() {
 func logVersion() {
-	log.Infof("guerrillad %s", guerrilla.Version)
-	log.Debugf("Build Time: %s", guerrilla.BuildTime)
-	log.Debugf("Commit:     %s", guerrilla.Commit)
+	mainlog.Infof("guerrillad %s", guerrilla.Version)
+	mainlog.Debugf("Build Time: %s", guerrilla.BuildTime)
+	mainlog.Debugf("Commit:     %s", guerrilla.Commit)
 }
 }

+ 235 - 11
config.go

@@ -1,10 +1,22 @@
 package guerrilla
 package guerrilla
 
 
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"reflect"
+	"strings"
+)
+
 // AppConfig is the holder of the configuration of the app
 // AppConfig is the holder of the configuration of the app
 type AppConfig struct {
 type AppConfig struct {
 	Dashboard    DashboardConfig `json:"dashboard"`
 	Dashboard    DashboardConfig `json:"dashboard"`
 	Servers      []ServerConfig  `json:"servers"`
 	Servers      []ServerConfig  `json:"servers"`
 	AllowedHosts []string        `json:"allowed_hosts"`
 	AllowedHosts []string        `json:"allowed_hosts"`
+	PidFile      string          `json:"pid_file"`
+	LogFile      string          `json:"log_file,omitempty"`
+	LogLevel     string          `json:"log_level,omitempty"`
 }
 }
 
 
 type DashboardConfig struct {
 type DashboardConfig struct {
@@ -14,15 +26,227 @@ type DashboardConfig struct {
 
 
 // ServerConfig specifies config options for a single server
 // ServerConfig specifies config options for a single server
 type ServerConfig struct {
 type ServerConfig struct {
-	IsEnabled       bool     `json:"is_enabled"`
-	Hostname        string   `json:"host_name"`
-	AllowedHosts    []string `json:"allowed_hosts"`
-	MaxSize         int64    `json:"max_size"`
-	PrivateKeyFile  string   `json:"private_key_file"`
-	PublicKeyFile   string   `json:"public_key_file"`
-	Timeout         int      `json:"timeout"`
-	ListenInterface string   `json:"listen_interface"`
-	StartTLSOn      bool     `json:"start_tls_on,omitempty"`
-	TLSAlwaysOn     bool     `json:"tls_always_on,omitempty"`
-	MaxClients      int      `json:"max_clients"`
+	IsEnabled       bool   `json:"is_enabled"`
+	Hostname        string `json:"host_name"`
+	MaxSize         int64  `json:"max_size"`
+	PrivateKeyFile  string `json:"private_key_file"`
+	PublicKeyFile   string `json:"public_key_file"`
+	Timeout         int    `json:"timeout"`
+	ListenInterface string `json:"listen_interface"`
+	StartTLSOn      bool   `json:"start_tls_on,omitempty"`
+	TLSAlwaysOn     bool   `json:"tls_always_on,omitempty"`
+	MaxClients      int    `json:"max_clients"`
+	LogFile         string `json:"log_file,omitempty"`
+
+	_privateKeyFile_mtime int
+	_publicKeyFile_mtime  int
+}
+
+// Unmarshalls json data into AppConfig struct and any other initialization of the struct
+func (c *AppConfig) Load(jsonBytes []byte) error {
+	err := json.Unmarshal(jsonBytes, c)
+	if err != nil {
+		return fmt.Errorf("could not parse config file: %s", err)
+	}
+	if len(c.AllowedHosts) == 0 {
+		return errors.New("empty AllowedHosts is not allowed")
+	}
+
+	// read the timestamps for the ssl keys, to determine if they need to be reloaded
+	for i := 0; i < len(c.Servers); i++ {
+		if err := c.Servers[i].loadTlsKeyTimestamps(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Emits any configuration change events onto the event bus.
+func (c *AppConfig) EmitChangeEvents(oldConfig *AppConfig, app Guerrilla) {
+	// has 'allowed hosts' changed?
+	if !reflect.DeepEqual(oldConfig.AllowedHosts, c.AllowedHosts) {
+		app.Publish("config_change:allowed_hosts", c)
+	}
+	// has pid file changed?
+	if strings.Compare(oldConfig.PidFile, c.PidFile) != 0 {
+		app.Publish("config_change:pid_file", c)
+	}
+	// has mainlog log changed?
+	if strings.Compare(oldConfig.LogFile, c.LogFile) != 0 {
+		app.Publish("config_change:log_file", c)
+	} else {
+		// since config file has not changed, we reload it
+		app.Publish("config_change:reopen_log_file", c)
+	}
+	// has log level changed?
+	if strings.Compare(oldConfig.LogLevel, c.LogLevel) != 0 {
+		app.Publish("config_change:log_level", c)
+	}
+	// server config changes
+	oldServers := oldConfig.getServers()
+	for iface, newServer := range c.getServers() {
+		// is server is in both configs?
+		if oldServer, ok := oldServers[iface]; ok {
+			// since old server exists in the new config, we do not track it anymore
+			delete(oldServers, iface)
+			newServer.emitChangeEvents(oldServer, app)
+		} else {
+			// start new server
+			app.Publish("server_change:new_server", newServer)
+		}
+
+	}
+	// remove any servers that don't exist anymore
+	for _, oldserver := range oldServers {
+		app.Publish("server_change:remove_server", oldserver)
+	}
+}
+
+// gets the servers in a map (key by interface) for easy lookup
+func (c *AppConfig) getServers() map[string]*ServerConfig {
+	servers := make(map[string]*ServerConfig, len(c.Servers))
+	for i := 0; i < len(c.Servers); i++ {
+		servers[c.Servers[i].ListenInterface] = &c.Servers[i]
+	}
+	return servers
+}
+
+// Emits any configuration change events on the server.
+// All events are fired and run synchronously
+func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) {
+	// get a list of changes
+	changes := getDiff(
+		*oldServer,
+		*sc,
+	)
+	if len(changes) > 0 {
+		// something changed in the server config
+		app.Publish("server_change:update_config", sc)
+	}
+
+	// enable or disable?
+	if _, ok := changes["IsEnabled"]; ok {
+		if sc.IsEnabled {
+			app.Publish("server_change:start_server", sc)
+		} else {
+			app.Publish("server_change:stop_server", sc)
+		}
+		// do not emit any more events when IsEnabled changed
+		return
+	}
+	// log file change?
+	if _, ok := changes["LogFile"]; ok {
+		app.Publish("server_change:new_log_file", sc)
+	} else {
+		// since config file has not changed, we reload it
+		app.Publish("server_change:reopen_log_file", sc)
+	}
+	// timeout changed
+	if _, ok := changes["Timeout"]; ok {
+		app.Publish("server_change:timeout", sc)
+	}
+	// max_clients changed
+	if _, ok := changes["MaxClients"]; ok {
+		app.Publish("server_change:max_clients", 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 {
+		app.Publish("server_change:tls_config", sc)
+	}
+}
+
+// Loads in timestamps for the ssl keys
+func (sc *ServerConfig) loadTlsKeyTimestamps() error {
+	var statErr = func(iface string, err error) error {
+		return errors.New(
+			fmt.Sprintf(
+				"could not stat key for server [%s], %s",
+				iface,
+				err.Error()))
+	}
+	if info, err := os.Stat(sc.PrivateKeyFile); err == nil {
+		sc._privateKeyFile_mtime = info.ModTime().Second()
+	} else {
+		return statErr(sc.ListenInterface, err)
+	}
+	if info, err := os.Stat(sc.PublicKeyFile); err == nil {
+		sc._publicKeyFile_mtime = info.ModTime().Second()
+	} 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
+}
+
+// Returns a diff 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{} {
+	ret := make(map[string]interface{}, 5)
+	compareWith := structtomap(b)
+	for key, val := range structtomap(a) {
+		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()
+			if t1 != t3 {
+				ret["PrivateKeyFile"] = newServer.PrivateKeyFile
+			}
+			if t2 != t4 {
+				ret["PublicKeyFile"] = newServer.PublicKeyFile
+			}
+		}
+	}
+	return ret
+}
+
+// Convert fields of a struct to a map
+// only able to convert int, bool and string; not recursive
+func structtomap(obj interface{}) map[string]interface{} {
+	ret := make(map[string]interface{}, 0)
+	v := reflect.ValueOf(obj)
+	t := v.Type()
+	for index := 0; index < v.NumField(); index++ {
+		vField := v.Field(index)
+		fName := t.Field(index).Name
+
+		switch vField.Kind() {
+		case reflect.Int:
+			value := vField.Int()
+			ret[fName] = value
+		case reflect.String:
+			value := vField.String()
+			ret[fName] = value
+		case reflect.Bool:
+			value := vField.Bool()
+			ret[fName] = value
+		}
+	}
+	return ret
 }
 }

+ 271 - 0
config_test.go

@@ -0,0 +1,271 @@
+package guerrilla
+
+import (
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+	"time"
+)
+
+func init() {
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "./tests/")
+}
+
+// a configuration file with a dummy backend
+
+//
+var configJsonA = `
+{
+    "log_file" : "./tests/testlog",
+    "log_level" : "debug",
+    "pid_file" : "/var/run/go-guerrilla.pid",
+    "allowed_hosts": ["spam4.me","grr.la"],
+    "backend_name" : "dummy",
+    "backend_config" :
+        {
+            "log_received_mails" : true
+        },
+    "servers" : [
+        {
+            "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":true,
+            "tls_always_on":false,
+            "max_clients": 2
+        },
+
+        {
+            "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
+        },
+
+        {
+            "is_enabled" : true,
+            "host_name":"mail.stopme.com",
+            "max_size": 100017,
+            "private_key_file":"config_test.go",
+            "public_key_file":"config_test.go",
+            "timeout":160,
+            "listen_interface":"127.0.0.1:9999",
+            "start_tls_on":true,
+            "tls_always_on":false,
+            "max_clients": 2
+        },
+
+        {
+            "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":true,
+            "tls_always_on":false,
+            "max_clients": 2
+        }
+
+
+    ]
+}
+`
+
+// B is A's configuration with different values from B
+// 127.0.0.1:4654 will be added
+// A's 127.0.0.1:3333 is disabled
+// B's 127.0.0.1:9999 is removed
+
+var configJsonB = `
+{
+    "log_file" : "./tests/testlog",
+    "log_level" : "debug",
+    "pid_file" : "/var/run/different-go-guerrilla.pid",
+    "allowed_hosts": ["spam4.me","grr.la","newhost.com"],
+    "backend_name" : "dummy",
+    "backend_config" :
+        {
+            "log_received_mails" : true
+        },
+    "servers" : [
+        {
+            "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
+        },
+        {
+            "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,
+            "max_clients": 2
+        },
+
+        {
+            "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":true,
+            "max_clients":1
+        },
+
+        {
+            "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
+        }
+    ]
+}
+`
+
+func TestConfigLoad(t *testing.T) {
+	ac := &AppConfig{}
+	if err := ac.Load([]byte(configJsonA)); err != nil {
+		t.Error("Cannot load config |", err)
+		t.SkipNow()
+	}
+	expectedLen := 4
+	if len(ac.Servers) != expectedLen {
+		t.Error("len(ac.Servers), expected", expectedLen, "got", len(ac.Servers))
+		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)
+	}
+}
+
+// Test the sample config to make sure a valid one is given!
+func TestSampleConfig(t *testing.T) {
+	fileName := "goguerrilla.conf.sample"
+	if jsonBytes, err := ioutil.ReadFile(fileName); err == nil {
+		ac := &AppConfig{}
+		if err := ac.Load(jsonBytes); err != nil {
+			// sample config can have broken tls certs
+			if strings.Index(err.Error(), "could not stat key") != 0 {
+				t.Error("Cannot load config", fileName, "|", err)
+				t.FailNow()
+			}
+		}
+	} else {
+		t.Error("Error reading", fileName, "|", err)
+	}
+}
+
+// make sure that we get all the config change events
+func TestConfigChangeEvents(t *testing.T) {
+
+	oldconf := &AppConfig{}
+	oldconf.Load([]byte(configJsonA))
+	logger, _ := log.GetLogger(oldconf.LogFile)
+	bcfg := backends.BackendConfig{"log_received_mails": true}
+	backend, _ := backends.New("dummy", bcfg, logger)
+	app, _ := New(oldconf, backend, logger)
+	// 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())
+	newconf := &AppConfig{}
+	newconf.Load([]byte(configJsonB))
+	newconf.Servers[0].LogFile = "/dev/stderr" // test for log file change
+	newconf.LogLevel = "off"
+	newconf.LogFile = "off"
+	expectedEvents := map[string]bool{
+		"config_change:pid_file":        false,
+		"config_change:log_file":        false,
+		"config_change:log_level":       false,
+		"config_change:allowed_hosts":   false,
+		"server_change:new_server":      false, // 127.0.0.1:4654 will be added
+		"server_change:remove_server":   false, // 127.0.0.1:9999 server removed
+		"server_change:stop_server":     false, // 127.0.0.1:3333: server (disabled)
+		"server_change:new_log_file":    false, // 127.0.0.1:2526
+		"server_change:reopen_log_file": false, // 127.0.0.1:2527
+		"server_change:timeout":         false, // 127.0.0.1:2526 timeout
+		//"server_change:tls_config":    false, // 127.0.0.1:2526
+		"server_change:max_clients": false, // 127.0.0.1:2526
+		"server_change:tls_config":  false, // 127.0.0.1:2527 timestamp changed on certificates
+	}
+	toUnsubscribe := map[string]func(c *AppConfig){}
+	toUnsubscribeS := map[string]func(c *ServerConfig){}
+
+	for event := range expectedEvents {
+		// Put in anon func since range is overwriting event
+		func(e string) {
+			if strings.Index(e, "config_change") != -1 {
+				f := func(c *AppConfig) {
+					expectedEvents[e] = true
+				}
+				app.Subscribe(event, f)
+				toUnsubscribe[event] = f
+			} else {
+				// must be a server config change then
+				f := func(c *ServerConfig) {
+					expectedEvents[e] = true
+				}
+				app.Subscribe(event, f)
+				toUnsubscribeS[event] = f
+			}
+
+		}(event)
+	}
+
+	// emit events
+	newconf.EmitChangeEvents(oldconf, app)
+	// unsubscribe
+	for unevent, unfun := range toUnsubscribe {
+		app.Unsubscribe(unevent, unfun)
+	}
+	for unevent, unfun := range toUnsubscribeS {
+		app.Unsubscribe(unevent, unfun)
+	}
+	for event, val := range expectedEvents {
+		if val == false {
+			t.Error("Did not fire config change event:", event)
+			t.FailNow()
+			break
+		}
+	}
+
+	// don't forget to reset
+	os.Truncate(oldconf.LogFile, 0)
+}

+ 186 - 0
envelope/envelope.go

@@ -0,0 +1,186 @@
+package envelope
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"github.com/sloonz/go-qprintable"
+	"gopkg.in/iconv.v1"
+	"io/ioutil"
+	"net/textproto"
+	"regexp"
+	"strings"
+)
+
+// EmailAddress encodes an email address of the form `<user@host>`
+type EmailAddress struct {
+	User string
+	Host string
+}
+
+func (ep *EmailAddress) String() string {
+	return fmt.Sprintf("%s@%s", ep.User, ep.Host)
+}
+
+func (ep *EmailAddress) IsEmpty() bool {
+	return ep.User == "" && ep.Host == ""
+}
+
+// Email represents a single SMTP message.
+type Envelope struct {
+	// Remote IP address
+	RemoteAddress string
+	// Message sent in EHLO command
+	Helo string
+	// Sender
+	MailFrom EmailAddress
+	// Recipients
+	RcptTo []EmailAddress
+	// Data stores the header and message body
+	Data bytes.Buffer
+	// Subject stores the subject of the email, extracted and decoded after calling ParseHeaders()
+	Subject string
+	// TLS is true if the email was received using a TLS connection
+	TLS bool
+	// Header stores the results from ParseHeaders()
+	Header textproto.MIMEHeader
+}
+
+// ParseHeaders parses the headers into Header field of the Envelope struct.
+// Data buffer must be full before calling.
+// It assumes that at most 30kb of email data can be a header
+// Decoding of encoding to UTF is only done on the Subject, where the result is assigned to the Subject field
+func (e *Envelope) ParseHeaders() error {
+	var err error
+	if e.Header != nil {
+		return errors.New("Headers already parsed")
+	}
+	all := e.Data.Bytes()
+
+	// find where the header ends, assuming that over 30 kb would be max
+	max := 1024 * 30
+	if len(all) < max {
+		max = len(all) - 1
+	}
+	headerEnd := bytes.Index(all[:max], []byte("\n\n"))
+
+	if headerEnd > -1 {
+		headerReader := textproto.NewReader(bufio.NewReader(bytes.NewBuffer(all[0:headerEnd])))
+		e.Header, err = headerReader.ReadMIMEHeader()
+		if err != nil {
+			// decode the subject
+			if subject, ok := e.Header["Subject"]; ok {
+				e.Subject = MimeHeaderDecode(subject[0])
+			}
+		}
+	} else {
+		err = errors.New("header not found")
+	}
+	return err
+}
+
+var mimeRegex, _ = regexp.Compile(`=\?(.+?)\?([QBqp])\?(.+?)\?=`)
+
+// Decode strings in Mime header format
+// eg. =?ISO-2022-JP?B?GyRCIVo9dztSOWJAOCVBJWMbKEI=?=
+func MimeHeaderDecode(str string) string {
+
+	matched := mimeRegex.FindAllStringSubmatch(str, -1)
+	var charset, encoding, payload string
+	if matched != nil {
+		for i := 0; i < len(matched); i++ {
+			if len(matched[i]) > 2 {
+				charset = matched[i][1]
+				encoding = strings.ToUpper(matched[i][2])
+				payload = matched[i][3]
+				switch encoding {
+				case "B":
+					str = strings.Replace(
+						str,
+						matched[i][0],
+						MailTransportDecode(payload, "base64", charset),
+						1)
+				case "Q":
+					str = strings.Replace(
+						str,
+						matched[i][0],
+						MailTransportDecode(payload, "quoted-printable", charset),
+						1)
+				}
+			}
+		}
+	}
+	return str
+}
+
+// decode from 7bit to 8bit UTF-8
+// encodingType can be "base64" or "quoted-printable"
+func MailTransportDecode(str string, encodingType string, charset string) string {
+	if charset == "" {
+		charset = "UTF-8"
+	} else {
+		charset = strings.ToUpper(charset)
+	}
+	if encodingType == "base64" {
+		str = fromBase64(str)
+	} else if encodingType == "quoted-printable" {
+		str = fromQuotedP(str)
+	}
+
+	if charset != "UTF-8" {
+		charset = fixCharset(charset)
+		// iconv is pretty good at what it does
+		if cd, err := iconv.Open("UTF-8", charset); err == nil {
+			defer func() {
+				cd.Close()
+				if r := recover(); r != nil {
+					//logln(1, fmt.Sprintf("Recovered in %v", r))
+				}
+			}()
+			// eg. charset can be "ISO-2022-JP"
+			return cd.ConvString(str)
+		}
+
+	}
+	return str
+}
+
+func fromBase64(data string) string {
+	buf := bytes.NewBufferString(data)
+	decoder := base64.NewDecoder(base64.StdEncoding, buf)
+	res, _ := ioutil.ReadAll(decoder)
+	return string(res)
+}
+
+func fromQuotedP(data string) string {
+	buf := bytes.NewBufferString(data)
+	decoder := qprintable.NewDecoder(qprintable.BinaryEncoding, buf)
+	res, _ := ioutil.ReadAll(decoder)
+	return string(res)
+}
+
+var charsetRegex, _ = regexp.Compile(`[_:.\/\\]`)
+
+func fixCharset(charset string) string {
+	fixed_charset := charsetRegex.ReplaceAllString(charset, "-")
+	// Fix charset
+	// borrowed from http://squirrelmail.svn.sourceforge.net/viewvc/squirrelmail/trunk/squirrelmail/include/languages.php?revision=13765&view=markup
+	// OE ks_c_5601_1987 > cp949
+	fixed_charset = strings.Replace(fixed_charset, "ks-c-5601-1987", "cp949", -1)
+	// Moz x-euc-tw > euc-tw
+	fixed_charset = strings.Replace(fixed_charset, "x-euc", "euc", -1)
+	// Moz x-windows-949 > cp949
+	fixed_charset = strings.Replace(fixed_charset, "x-windows_", "cp", -1)
+	// windows-125x and cp125x charsets
+	fixed_charset = strings.Replace(fixed_charset, "windows-", "cp", -1)
+	// ibm > cp
+	fixed_charset = strings.Replace(fixed_charset, "ibm", "cp", -1)
+	// iso-8859-8-i -> iso-8859-8
+	fixed_charset = strings.Replace(fixed_charset, "iso-8859-8-i", "iso-8859-8", -1)
+	if charset != fixed_charset {
+		return fixed_charset
+	}
+	return charset
+}

+ 0 - 42
glide.lock

@@ -1,42 +0,0 @@
-hash: ce60d2a4a0a6b12d61788d65a3c64a31f0174e91ab796544260398dd0aa43114
-updated: 2017-01-24T11:26:52.507726508-08:00
-imports:
-- name: github.com/garyburd/redigo
-  version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
-  subpackages:
-  - internal
-  - redis
-- name: github.com/gorilla/context
-  version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
-- name: github.com/gorilla/mux
-  version: 392c28fe23e1c45ddba891b0320b3b5df220beea
-- name: github.com/gorilla/websocket
-  version: 0674c7c7968d9fac5f0f678325161ec31df406af
-- name: github.com/inconshreveable/mousetrap
-  version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
-- name: github.com/rakyll/statik
-  version: 274df120e9065bdd08eb1120e0375e3dc1ae8465
-  subpackages:
-  - fs
-- name: github.com/Sirupsen/logrus
-  version: d26492970760ca5d33129d2d799e34be5c4782eb
-- name: github.com/sloonz/go-qprintable
-  version: 775b3a4592d5bfc47b0ba398ec0d4dba018e5926
-- name: github.com/spf13/cobra
-  version: 0f056af21f5f368e5b0646079d0094a2c64150f7
-- name: github.com/spf13/pflag
-  version: a232f6d9f87afaaa08bafaff5da685f974b83313
-- name: github.com/ziutek/mymysql
-  version: e08c2f35356576b3c3690c252fe5dca728ae9292
-  subpackages:
-  - autorc
-  - godrv
-  - mysql
-  - native
-- name: golang.org/x/sys
-  version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea
-  subpackages:
-  - unix
-- name: gopkg.in/iconv.v1
-  version: 16a760eb7e186ae0e3aedda00d4a1daa4d0701d8
-testImports: []

+ 4 - 0
glide.yaml

@@ -19,3 +19,7 @@ import:
   version: ~0.1.0
   version: ~0.1.0
   subpackages:
   subpackages:
   - fs
   - fs
+- package: github.com/asaskevich/EventBus
+  version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2
+- package: github.com/go-sql-driver/mysql
+  version: ^1.3.0

+ 8 - 4
goguerrilla.conf.sample

@@ -1,12 +1,14 @@
 {
 {
+    "log_file" : "stderr",
+    "log_level" : "info",
     "allowed_hosts": [
     "allowed_hosts": [
       "guerrillamail.com",
       "guerrillamail.com",
-      "guerrillamailblock.com"
+      "guerrillamailblock.com",
       "sharklasers.com",
       "sharklasers.com",
       "guerrillamail.net",
       "guerrillamail.net",
       "guerrillamail.org"
       "guerrillamail.org"
     ],
     ],
-    "primary_mail_host": "sharklasers.com",
+    "pid_file" : "/var/run/go-guerrilla.pid",
     "backend_name": "dummy",
     "backend_name": "dummy",
     "backend_config": {
     "backend_config": {
         "log_received_mails": true
         "log_received_mails": true
@@ -22,7 +24,8 @@
             "listen_interface":"127.0.0.1:25",
             "listen_interface":"127.0.0.1:25",
             "start_tls_on":true,
             "start_tls_on":true,
             "tls_always_on":false,
             "tls_always_on":false,
-            "max_clients": 1000
+            "max_clients": 1000,
+            "log_file" : "stderr"
         },
         },
         {
         {
             "is_enabled" : true,
             "is_enabled" : true,
@@ -34,7 +37,8 @@
             "listen_interface":"127.0.0.1:465",
             "listen_interface":"127.0.0.1:465",
             "start_tls_on":false,
             "start_tls_on":false,
             "tls_always_on":true,
             "tls_always_on":true,
-            "max_clients":500
+            "max_clients":500,
+            "log_file" : "stderr"
         }
         }
     ]
     ]
 }
 }

+ 350 - 30
guerrilla.go

@@ -3,65 +3,349 @@ package guerrilla
 import (
 import (
 	"errors"
 	"errors"
 	"sync"
 	"sync"
+	"sync/atomic"
 
 
-	log "github.com/Sirupsen/logrus"
+	evbus "github.com/asaskevich/EventBus"
+	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/dashboard"
 	"github.com/flashmob/go-guerrilla/dashboard"
+	"github.com/flashmob/go-guerrilla/log"
 )
 )
 
 
-func init() {
-	log.AddHook(dashboard.LogHook)
+const (
+	// server has just been created
+	GuerrillaStateNew = iota
+	// Server has been started and is running
+	GuerrillaStateStarted
+	// Server has just been stopped
+	GuerrillaStateStopped
+)
+
+type Errors []error
+
+// implement the Error interface
+func (e Errors) Error() string {
+	if len(e) == 1 {
+		return e[0].Error()
+	}
+	// multiple errors
+	msg := ""
+	for _, err := range e {
+		msg += "\n" + err.Error()
+	}
+	return msg
 }
 }
 
 
 type Guerrilla interface {
 type Guerrilla interface {
-	Start() (startErrors []error)
+	Start() error
 	Shutdown()
 	Shutdown()
+	Subscribe(topic string, fn interface{}) error
+	Publish(topic string, args ...interface{})
+	Unsubscribe(topic string, handler interface{}) error
+	SetLogger(log.Logger)
 }
 }
 
 
 type guerrilla struct {
 type guerrilla struct {
-	Config  *AppConfig
-	servers []server
-	backend *Backend
+	Config  AppConfig
+	servers map[string]*server
+	backend backends.Backend
+	// guard controls access to g.servers
+	guard   sync.Mutex
+	state   int8
+	bus     *evbus.EventBus
+	mainlog logStore
+}
+
+type logStore struct {
+	atomic.Value
+}
+
+// Get loads the log.logger in an atomic operation. Returns a stderr logger if not able to load
+func (ls *logStore) Get() log.Logger {
+	if v, ok := ls.Load().(log.Logger); ok {
+		return v
+	}
+	l, _ := log.GetLogger(log.OutputStderr.String())
+	return l
 }
 }
 
 
 // Returns a new instance of Guerrilla with the given config, not yet running.
 // Returns a new instance of Guerrilla with the given config, not yet running.
-func New(ac *AppConfig, b *Backend) Guerrilla {
-	g := &guerrilla{ac, []server{}, b}
-	// Instantiate servers
-	for _, sc := range ac.Servers {
-		if !sc.IsEnabled {
+func New(ac *AppConfig, b backends.Backend, l log.Logger) (Guerrilla, error) {
+	g := &guerrilla{
+		Config:  *ac, // take a local copy
+		servers: make(map[string]*server, len(ac.Servers)),
+		backend: b,
+		bus:     evbus.New(),
+	}
+	g.mainlog.Store(l)
+
+	if ac.LogLevel != "" {
+		g.mainlog.Get().SetLevel(ac.LogLevel)
+	}
+
+	g.state = GuerrillaStateNew
+	err := g.makeServers()
+
+	// subscribe for any events that may come in while running
+	g.subscribeEvents()
+	return g, err
+}
+
+// Instantiate servers
+func (g *guerrilla) makeServers() error {
+	g.mainlog.Get().Debug("making servers")
+	var errs Errors
+	for _, sc := range g.Config.Servers {
+		if _, ok := g.servers[sc.ListenInterface]; ok {
+			// server already instantiated
 			continue
 			continue
 		}
 		}
-		// Add relevant app-wide config options to each server
-		sc.AllowedHosts = ac.AllowedHosts
-		server, err := newServer(sc, b)
+		server, err := newServer(&sc, g.backend, g.mainlog.Get())
 		if err != nil {
 		if err != nil {
-			log.WithError(err).Error("Failed to create server")
-		} else {
-			g.servers = append(g.servers, *server)
+			g.mainlog.Get().WithError(err).Errorf("Failed to create server [%s]", sc.ListenInterface)
+			errs = append(errs, err)
+		}
+		if server != nil {
+			g.servers[sc.ListenInterface] = server
+			server.setAllowedHosts(g.Config.AllowedHosts)
+		}
+
+	}
+	if len(g.servers) == 0 {
+		errs = append(errs, errors.New("There are no servers that can start, please check your config"))
+	}
+	if len(errs) == 0 {
+		return nil
+	}
+	return errs
+}
+
+// find a server by interface, retuning the index of the config and instance of server
+func (g *guerrilla) findServer(iface string) (int, *server) {
+	g.guard.Lock()
+	defer g.guard.Unlock()
+	ret := -1
+	for i := range g.Config.Servers {
+		if g.Config.Servers[i].ListenInterface == iface {
+			server := g.servers[iface]
+			ret = i
+			return ret, server
 		}
 		}
 	}
 	}
-	return g
+	return ret, nil
+}
+
+func (g *guerrilla) removeServer(serverConfigIndex int, iface string) {
+	g.guard.Lock()
+	defer g.guard.Unlock()
+	delete(g.servers, iface)
+	// cut out from the slice
+	g.Config.Servers = append(g.Config.Servers[:serverConfigIndex], g.Config.Servers[1:]...)
+}
+
+func (g *guerrilla) addServer(sc *ServerConfig) {
+	g.guard.Lock()
+	defer g.guard.Unlock()
+	g.Config.Servers = append(g.Config.Servers, *sc)
+	g.makeServers()
+}
+
+func (g *guerrilla) setConfig(i int, sc *ServerConfig) {
+	g.guard.Lock()
+	defer g.guard.Unlock()
+	g.Config.Servers[i] = *sc
+	g.servers[sc.ListenInterface].setConfig(sc)
+}
+
+// mapServers calls a callback on each server in g.servers map
+// It locks the g.servers map before mapping
+func (g *guerrilla) mapServers(callback func(*server)) map[string]*server {
+	defer g.guard.Unlock()
+	g.guard.Lock()
+	for _, server := range g.servers {
+		callback(server)
+	}
+	return g.servers
+}
+
+// subscribeEvents subscribes event handlers for configuration change events
+func (g *guerrilla) subscribeEvents() {
+
+	// allowed_hosts changed, set for all servers
+	g.Subscribe("config_change:allowed_hosts", func(c *AppConfig) {
+		g.mapServers(func(server *server) {
+			server.setAllowedHosts(c.AllowedHosts)
+		})
+		g.mainlog.Get().Infof("allowed_hosts config changed, a new list was set")
+	})
+
+	// the main log file changed
+	g.Subscribe("config_change:log_file", func(c *AppConfig) {
+		var err error
+		var l log.Logger
+		if l, err = log.GetLogger(c.LogFile); err == nil {
+			g.mainlog.Store(l)
+			g.mapServers(func(server *server) {
+				server.mainlogStore.Store(l) // it will change to hl on the next accepted client
+			})
+			g.mainlog.Get().Infof("main log for new clients changed to to [%s]", c.LogFile)
+		} else {
+			g.mainlog.Get().WithError(err).Errorf("main logging change failed [%s]", c.LogFile)
+		}
+
+	})
+
+	// re-open the main log file (file not changed)
+	g.Subscribe("config_change:reopen_log_file", func(c *AppConfig) {
+		g.mainlog.Get().Reopen()
+		g.mainlog.Get().Infof("re-opened main log file [%s]", c.LogFile)
+	})
+
+	// when log level changes, apply to mainlog and server logs
+	g.Subscribe("config_change:log_level", func(c *AppConfig) {
+		g.mainlog.Get().SetLevel(c.LogLevel)
+		g.mapServers(func(server *server) {
+			server.log.SetLevel(c.LogLevel)
+		})
+		g.mainlog.Get().Infof("log level changed to [%s]", c.LogLevel)
+	})
+
+	// server config was updated
+	g.Subscribe("server_change:update_config", func(sc *ServerConfig) {
+		if i, _ := g.findServer(sc.ListenInterface); i != -1 {
+			g.setConfig(i, sc)
+		}
+	})
+
+	// add a new server to the config & start
+	g.Subscribe("server_change:new_server", func(sc *ServerConfig) {
+		if i, _ := g.findServer(sc.ListenInterface); i == -1 {
+			// not found, lets add it
+			g.addServer(sc)
+			g.mainlog.Get().Infof("New server added [%s]", sc.ListenInterface)
+			if g.state == GuerrillaStateStarted {
+				err := g.Start()
+				if err != nil {
+					g.mainlog.Get().WithError(err).Info("Event server_change:new_server returned errors when starting")
+				}
+			}
+		}
+	})
+	// start a server that already exists in the config and has been instantiated
+	g.Subscribe("server_change:start_server", func(sc *ServerConfig) {
+		if i, server := g.findServer(sc.ListenInterface); i != -1 {
+			if server.state == ServerStateStopped || server.state == ServerStateNew {
+				g.mainlog.Get().Infof("Starting server [%s]", server.listenInterface)
+				err := g.Start()
+				if err != nil {
+					g.mainlog.Get().WithError(err).Info("Event server_change:start_server returned errors when starting")
+				}
+			}
+		}
+	})
+	// stop running a server
+	g.Subscribe("server_change:stop_server", func(sc *ServerConfig) {
+		if i, server := g.findServer(sc.ListenInterface); i != -1 {
+			if server.state == ServerStateRunning {
+				server.Shutdown()
+				g.mainlog.Get().Infof("Server [%s] stopped.", sc.ListenInterface)
+			}
+		}
+	})
+	// server was removed from config
+	g.Subscribe("server_change:remove_server", func(sc *ServerConfig) {
+		if i, server := g.findServer(sc.ListenInterface); i != -1 {
+			server.Shutdown()
+			g.removeServer(i, sc.ListenInterface)
+			g.mainlog.Get().Infof("Server [%s] removed from config, stopped it.", sc.ListenInterface)
+		}
+	})
+
+	// TLS changes
+	g.Subscribe("server_change:tls_config", func(sc *ServerConfig) {
+		if i, server := g.findServer(sc.ListenInterface); i != -1 {
+			if err := server.configureSSL(); err == nil {
+				g.mainlog.Get().Infof("Server [%s] new TLS configuration loaded", sc.ListenInterface)
+			} else {
+				g.mainlog.Get().WithError(err).Errorf("Server [%s] failed to load the new TLS configuration", sc.ListenInterface)
+			}
+		}
+	})
+	// when server's timeout change.
+	g.Subscribe("server_change:timeout", func(sc *ServerConfig) {
+		g.mapServers(func(server *server) {
+			server.setTimeout(sc.Timeout)
+		})
+	})
+	// when server's max clients change.
+	g.Subscribe("server_change:max_clients", func(sc *ServerConfig) {
+		g.mapServers(func(server *server) {
+			// TODO resize the pool somehow
+		})
+	})
+	// when a server's log file changes
+	g.Subscribe("server_change:new_log_file", func(sc *ServerConfig) {
+		if i, server := g.findServer(sc.ListenInterface); i != -1 {
+			var err error
+			var l log.Logger
+			if l, err = log.GetLogger(sc.LogFile); err == nil {
+				g.mainlog.Store(l)
+				server.logStore.Store(l) // it will change to l on the next accepted client
+				g.mainlog.Get().Infof("Server [%s] changed, new clients will log to: [%s]",
+					sc.ListenInterface,
+					sc.LogFile,
+				)
+			} else {
+				g.mainlog.Get().WithError(err).Errorf(
+					"Server [%s] log change failed to: [%s]",
+					sc.ListenInterface,
+					sc.LogFile,
+				)
+			}
+		}
+	})
+	// when the daemon caught a sighup
+	g.Subscribe("server_change:reopen_log_file", func(sc *ServerConfig) {
+		if i, server := g.findServer(sc.ListenInterface); i != -1 {
+			server.log.Reopen()
+			g.mainlog.Get().Infof("Server [%s] re-opened log file [%s]", sc.ListenInterface, sc.LogFile)
+		}
+	})
+
 }
 }
 
 
 // Entry point for the application. Starts all servers.
 // Entry point for the application. Starts all servers.
-func (g *guerrilla) Start() (startErrors []error) {
+func (g *guerrilla) Start() error {
+	var startErrors Errors
+	g.guard.Lock()
+	defer func() {
+		g.state = GuerrillaStateStarted
+		g.guard.Unlock()
+	}()
 	if len(g.servers) == 0 {
 	if len(g.servers) == 0 {
 		return append(startErrors, errors.New("No servers to start, please check the config"))
 		return append(startErrors, errors.New("No servers to start, please check the config"))
 	}
 	}
 	// channel for reading errors
 	// channel for reading errors
 	errs := make(chan error, len(g.servers))
 	errs := make(chan error, len(g.servers))
 	var startWG sync.WaitGroup
 	var startWG sync.WaitGroup
-	startWG.Add(len(g.servers))
+
 	// start servers, send any errors back to errs channel
 	// start servers, send any errors back to errs channel
-	for i := 0; i < len(g.servers); i++ {
+	for ListenInterface := range g.servers {
+		if !g.servers[ListenInterface].isEnabled() {
+			// not enabled
+			continue
+		}
+		if g.servers[ListenInterface].state != ServerStateNew &&
+			g.servers[ListenInterface].state != ServerStateStopped {
+			continue
+		}
+		startWG.Add(1)
 		go func(s *server) {
 		go func(s *server) {
 			if err := s.Start(&startWG); err != nil {
 			if err := s.Start(&startWG); err != nil {
 				errs <- err
 				errs <- err
-				startWG.Done()
 			}
 			}
-		}(&g.servers[i])
+		}(g.servers[ListenInterface])
 	}
 	}
-	// wait for all servers to start
+	// wait for all servers to start (or fail)
 	startWG.Wait()
 	startWG.Wait()
 
 
 	if g.Config.Dashboard.Enabled {
 	if g.Config.Dashboard.Enabled {
@@ -77,13 +361,49 @@ func (g *guerrilla) Start() (startErrors []error) {
 			startErrors = append(startErrors, err)
 			startErrors = append(startErrors, err)
 		}
 		}
 	}
 	}
-	return startErrors
+	if len(startErrors) > 0 {
+		return startErrors
+	} else {
+		if gw, ok := g.backend.(*backends.BackendGateway); ok {
+			if gw.State == backends.BackendStateShuttered {
+				_ = gw.Reinitialize()
+			}
+		}
+	}
+	return nil
 }
 }
 
 
 func (g *guerrilla) Shutdown() {
 func (g *guerrilla) Shutdown() {
-	for _, s := range g.servers {
-		s.Shutdown()
-		log.Infof("shutdown completed for [%s]", s.config.ListenInterface)
+	g.guard.Lock()
+	defer func() {
+		g.state = GuerrillaStateStopped
+		defer g.guard.Unlock()
+	}()
+	for ListenInterface, s := range g.servers {
+		if s.state == ServerStateRunning {
+			s.Shutdown()
+			g.mainlog.Get().Infof("shutdown completed for [%s]", ListenInterface)
+		}
+	}
+	if err := g.backend.Shutdown(); err != nil {
+		g.mainlog.Get().WithError(err).Warn("Backend failed to shutdown")
+	} else {
+		g.mainlog.Get().Infof("Backend shutdown completed")
 	}
 	}
-	log.Infof("Backend shutdown completed")
+}
+
+func (g *guerrilla) Subscribe(topic string, fn interface{}) error {
+	return g.bus.Subscribe(topic, fn)
+}
+
+func (g *guerrilla) Publish(topic string, args ...interface{}) {
+	g.bus.Publish(topic, args...)
+}
+
+func (g *guerrilla) Unsubscribe(topic string, handler interface{}) error {
+	return g.bus.Unsubscribe(topic, handler)
+}
+
+func (g *guerrilla) SetLogger(l log.Logger) {
+	g.mainlog.Store(l)
 }
 }

+ 335 - 0
log/log.go

@@ -0,0 +1,335 @@
+package log
+
+import (
+	"bufio"
+	"io"
+	"io/ioutil"
+	"net"
+	"os"
+	"strings"
+	"sync"
+
+	log "github.com/Sirupsen/logrus"
+
+	"github.com/flashmob/go-guerrilla/dashboard"
+)
+
+type Logger interface {
+	log.FieldLogger
+	WithConn(conn net.Conn) *log.Entry
+	Reopen() error
+	GetLogDest() string
+	SetLevel(level string)
+	GetLevel() string
+	IsDebug() bool
+	AddHook(h log.Hook)
+}
+
+// Implements the Logger interface
+// It's a logrus logger wrapper that contains an instance of our LoggerHook
+type HookedLogger struct {
+
+	// satisfy the log.FieldLogger interface
+	*log.Logger
+
+	h LoggerHook
+}
+
+type loggerCache map[string]Logger
+
+// loggers store the cached loggers created by NewLogger
+var loggers struct {
+	cache loggerCache
+	// mutex guards the cache
+	sync.Mutex
+}
+
+// GetLogger returns a struct that implements Logger (i.e HookedLogger) with a custom hook.
+// It may be new or already created, (ie. singleton factory pattern)
+// The hook has been initialized with dest
+// dest can can be a path to a file, or the following string values:
+// "off" - disable any log output
+// "stdout" - write to standard output
+// "stderr" - write to standard error
+// If the file doesn't exists, a new file will be created. Otherwise it will be appended
+// Each Logger returned is cached on dest, subsequent call will get the cached logger if dest matches
+// If there was an error, the log will revert to stderr instead of using a custom hook
+
+func GetLogger(dest string) (Logger, error) {
+	loggers.Lock()
+	defer loggers.Unlock()
+	if loggers.cache == nil {
+		loggers.cache = make(loggerCache, 1)
+	} else {
+		if l, ok := loggers.cache[dest]; ok {
+			// return the one we found in the cache
+			return l, nil
+		}
+	}
+	logrus := log.New()
+	// we'll use the hook to output instead
+	logrus.Out = ioutil.Discard
+
+	l := &HookedLogger{}
+	l.Logger = logrus
+
+	// cache it
+	loggers.cache[dest] = l
+
+	// setup the hook
+	if h, err := NewLogrusHook(dest); err != nil {
+		// revert back to stderr
+		logrus.Out = os.Stderr
+		return l, err
+	} else {
+		logrus.Hooks.Add(h)
+		l.h = h
+	}
+
+	// add the dashboard hook
+	logrus.Hooks.Add(dashboard.LogHook)
+
+	return l, nil
+
+}
+
+// AddHook adds a new logrus hook
+func (l *HookedLogger) AddHook(h log.Hook) {
+	log.AddHook(h)
+}
+
+func (l *HookedLogger) IsDebug() bool {
+	return l.GetLevel() == log.DebugLevel.String()
+}
+
+// SetLevel sets a log level, one of the LogLevels
+func (l *HookedLogger) SetLevel(level string) {
+	var logLevel log.Level
+	var err error
+	if logLevel, err = log.ParseLevel(level); err != nil {
+		return
+	}
+	l.Level = logLevel
+	log.SetLevel(logLevel)
+}
+
+// GetLevel gets the current log level
+func (l *HookedLogger) GetLevel() string {
+	return l.Level.String()
+}
+
+// Reopen closes the log file and re-opens it
+func (l *HookedLogger) Reopen() error {
+	return l.h.Reopen()
+}
+
+// Fgetname Gets the file name
+func (l *HookedLogger) GetLogDest() string {
+	return l.h.GetLogDest()
+}
+
+// WithConn extends logrus to be able to log with a net.Conn
+func (l *HookedLogger) WithConn(conn net.Conn) *log.Entry {
+	var addr string = "unknown"
+
+	if conn != nil {
+		addr = conn.RemoteAddr().String()
+	}
+	return l.WithField("addr", addr)
+}
+
+// custom logrus hook
+
+// hookMu ensures all io operations are synced. Always on exported functions
+var hookMu sync.Mutex
+
+// LoggerHook extends the log.Hook interface by adding Reopen() and Rename()
+type LoggerHook interface {
+	log.Hook
+	Reopen() error
+	GetLogDest() string
+}
+type LogrusHook struct {
+	w io.Writer
+	// file descriptor, can be re-opened
+	fd *os.File
+	// filename to the file descriptor
+	fname string
+	// txtFormatter that doesn't use colors
+	plainTxtFormatter *log.TextFormatter
+
+	mu sync.Mutex
+}
+
+// newLogrusHook creates a new hook. dest can be a file name or one of the following strings:
+// "stderr" - log to stderr, lines will be written to os.Stdout
+// "stdout" - log to stdout, lines will be written to os.Stdout
+// "off" - no log, lines will be written to ioutil.Discard
+func NewLogrusHook(dest string) (LoggerHook, error) {
+	hookMu.Lock()
+	defer hookMu.Unlock()
+	hook := LogrusHook{fname: dest}
+	err := hook.setup(dest)
+	return &hook, err
+}
+
+type OutputOption int
+
+const (
+	OutputStderr OutputOption = 1 + iota
+	OutputStdout
+	OutputOff
+	OutputNull
+	OutputFile
+)
+
+var outputOptions = [...]string{
+	"stderr",
+	"stdout",
+	"off",
+	"",
+	"file",
+}
+
+func (o OutputOption) String() string {
+	return outputOptions[o-1]
+}
+
+func parseOutputOption(str string) OutputOption {
+	switch str {
+	case "stderr":
+		return OutputStderr
+	case "stdout":
+		return OutputStdout
+	case "off":
+		return OutputOff
+	case "":
+		return OutputNull
+	}
+	return OutputFile
+}
+
+// Setup sets the hook's writer w and file descriptor fd
+// assumes the hook.fd is closed and nil
+func (hook *LogrusHook) setup(dest string) error {
+
+	out := parseOutputOption(dest)
+	if out == OutputNull || out == OutputStderr {
+		hook.w = os.Stderr
+	} else if out == OutputStdout {
+		hook.w = os.Stdout
+	} else if out == OutputOff {
+		hook.w = ioutil.Discard
+	} else {
+		if _, err := os.Stat(dest); err == nil {
+			// file exists open the file for appending
+			if err := hook.openAppend(dest); err != nil {
+				return err
+			}
+		} else {
+			// create the file
+			if err := hook.openCreate(dest); err != nil {
+				return err
+			}
+		}
+	}
+	// disable colors when writing to file
+	if hook.fd != nil {
+		hook.plainTxtFormatter = &log.TextFormatter{DisableColors: true}
+	}
+	return nil
+}
+
+// openAppend opens the dest file for appending. Default to os.Stderr if it can't open dest
+func (hook *LogrusHook) openAppend(dest string) (err error) {
+	fd, err := os.OpenFile(dest, os.O_APPEND|os.O_WRONLY, 0644)
+	if err != nil {
+		log.WithError(err).Error("Could not open log file for appending")
+		hook.w = os.Stderr
+		hook.fd = nil
+		return
+	}
+	hook.w = bufio.NewWriter(fd)
+	hook.fd = fd
+	return
+}
+
+// openCreate creates a new dest file for appending. Default to os.Stderr if it can't open dest
+func (hook *LogrusHook) openCreate(dest string) (err error) {
+	fd, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
+	if err != nil {
+		log.WithError(err).Error("Could not create log file")
+		hook.w = os.Stderr
+		hook.fd = nil
+		return
+	}
+	hook.w = bufio.NewWriter(fd)
+	hook.fd = fd
+	return
+}
+
+// Fire implements the logrus Hook interface. It disables color text formatting if writing to a file
+func (hook *LogrusHook) Fire(entry *log.Entry) error {
+	hookMu.Lock()
+	defer hookMu.Unlock()
+	if hook.fd != nil {
+		// save the old hook
+		oldhook := entry.Logger.Formatter
+		defer func() {
+			// set the back to the old hook after we're done
+			entry.Logger.Formatter = oldhook
+		}()
+		// use the plain text hook
+		entry.Logger.Formatter = hook.plainTxtFormatter
+	}
+	if line, err := entry.String(); err == nil {
+		r := strings.NewReader(line)
+		if _, err = io.Copy(hook.w, r); err != nil {
+			return err
+		}
+		if wb, ok := hook.w.(*bufio.Writer); ok {
+			if err := wb.Flush(); err != nil {
+				return err
+			}
+			if hook.fd != nil {
+				hook.fd.Sync()
+			}
+		}
+		return err
+	} else {
+		return err
+	}
+}
+
+// GetLogDest returns the destination of the log as a string
+func (hook *LogrusHook) GetLogDest() string {
+	hookMu.Lock()
+	defer hookMu.Unlock()
+	return hook.fname
+}
+
+// Levels implements the logrus Hook interface
+func (hook *LogrusHook) Levels() []log.Level {
+	return log.AllLevels
+}
+
+// Reopen closes and re-open log file descriptor, which is a special feature of this hook
+func (hook *LogrusHook) Reopen() error {
+	hookMu.Lock()
+	defer hookMu.Unlock()
+	var err error
+	if hook.fd != nil {
+		if err = hook.fd.Close(); err != nil {
+			return err
+		}
+		// The file could have been re-named by an external program such as logrotate(8)
+		if _, err := os.Stat(hook.fname); err != nil {
+			// The file doesn't exist,create a new one.
+			return hook.openCreate(hook.fname)
+		} else {
+			return hook.openAppend(hook.fname)
+		}
+	}
+	return err
+
+}

+ 1 - 1
mocks/client.go

@@ -1,4 +1,4 @@
-package main
+package mocks
 
 
 import (
 import (
 	"fmt"
 	"fmt"

+ 101 - 0
mocks/conn_mock.go

@@ -0,0 +1,101 @@
+package mocks
+
+import (
+	"io"
+	"net"
+	"time"
+)
+
+// Mocks a net.Conn - server and client sides.
+// See server_test.go for usage examples
+// Taken from https://github.com/jordwest/mock-conn
+// This great answer http://stackoverflow.com/questions/1976950/simulate-a-tcp-connection-in-go
+
+// Addr is a fake network interface which implements the net.Addr interface
+type Addr struct {
+	NetworkString string
+	AddrString    string
+}
+
+func (a Addr) Network() string {
+	return a.NetworkString
+}
+
+func (a Addr) String() string {
+	return a.AddrString
+}
+
+// End is one 'end' of a simulated connection.
+type End struct {
+	Reader *io.PipeReader
+	Writer *io.PipeWriter
+}
+
+func (c End) Close() error {
+	if err := c.Writer.Close(); err != nil {
+		return err
+	}
+	if err := c.Reader.Close(); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (e End) Read(data []byte) (n int, err error)  { return e.Reader.Read(data) }
+func (e End) Write(data []byte) (n int, err error) { return e.Writer.Write(data) }
+
+func (e End) LocalAddr() net.Addr {
+	return Addr{
+		NetworkString: "tcp",
+		AddrString:    "127.0.0.1",
+	}
+}
+
+func (e End) RemoteAddr() net.Addr {
+	return Addr{
+		NetworkString: "tcp",
+		AddrString:    "127.0.0.1",
+	}
+}
+
+func (e End) SetDeadline(t time.Time) error      { return nil }
+func (e End) SetReadDeadline(t time.Time) error  { return nil }
+func (e End) SetWriteDeadline(t time.Time) error { return nil }
+
+// MockConn facilitates testing by providing two connected ReadWriteClosers
+// each of which can be used in place of a net.Conn
+type Conn struct {
+	Server *End
+	Client *End
+}
+
+func NewConn() *Conn {
+	// A connection consists of two pipes:
+	// Client      |      Server
+	//   writes   ===>  reads
+	//    reads  <===   writes
+
+	serverRead, clientWrite := io.Pipe()
+	clientRead, serverWrite := io.Pipe()
+
+	return &Conn{
+		Server: &End{
+			Reader: serverRead,
+			Writer: serverWrite,
+		},
+		Client: &End{
+			Reader: clientRead,
+			Writer: clientWrite,
+		},
+	}
+}
+
+func (c *Conn) Close() error {
+	if err := c.Server.Close(); err != nil {
+		return err
+	}
+	if err := c.Client.Close(); err != nil {
+		return err
+	}
+	return nil
+}

+ 0 - 60
models.go

@@ -3,10 +3,7 @@ package guerrilla
 import (
 import (
 	"bufio"
 	"bufio"
 	"errors"
 	"errors"
-	"fmt"
 	"io"
 	"io"
-	"strconv"
-	"strings"
 )
 )
 
 
 var (
 var (
@@ -14,63 +11,6 @@ var (
 	MessageSizeExceeded = errors.New("Maximum message size exceeded")
 	MessageSizeExceeded = errors.New("Maximum message size exceeded")
 )
 )
 
 
-// Backends process received mail. Depending on the implementation, that can
-// be storing in a database, retransmitting to another server, etc.
-// Must return an SMTP message (i.e. "250 OK") and a boolean indicating
-// whether the message was processed successfully.
-type Backend interface {
-	Process(*Envelope) BackendResult
-	Shutdown() error
-}
-
-// BackendResult represents a response to an SMTP client after receiving DATA.
-// The String method should return an SMTP message ready to send back to the
-// client, for example `250 OK: Message received`.
-type BackendResult interface {
-	fmt.Stringer
-	// Code should return the SMTP code associated with this response, ie. `250`
-	Code() int
-}
-
-// Internal implementation of BackendResult for use by backend implementations.
-type backendResult string
-
-func (br backendResult) String() string {
-	return string(br)
-}
-
-// Parses the SMTP code from the first 3 characters of the SMTP message.
-// Returns 554 if code cannot be parsed.
-func (br backendResult) Code() int {
-	trimmed := strings.TrimSpace(string(br))
-	if len(trimmed) < 3 {
-		return 554
-	}
-	code, err := strconv.Atoi(trimmed[:3])
-	if err != nil {
-		return 554
-	}
-	return code
-}
-
-func NewBackendResult(message string) BackendResult {
-	return backendResult(message)
-}
-
-// EmailAddress encodes an email address of the form `<user@host>`
-type EmailAddress struct {
-	User string
-	Host string
-}
-
-func (ep *EmailAddress) String() string {
-	return fmt.Sprintf("%s@%s", ep.User, ep.Host)
-}
-
-func (ep *EmailAddress) isEmpty() bool {
-	return ep.User == "" && ep.Host == ""
-}
-
 // we need to adjust the limit, so we embed io.LimitedReader
 // we need to adjust the limit, so we embed io.LimitedReader
 type adjustableLimitedReader struct {
 type adjustableLimitedReader struct {
 	R *io.LimitedReader
 	R *io.LimitedReader

+ 33 - 20
pool.go

@@ -2,6 +2,7 @@ package guerrilla
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"github.com/flashmob/go-guerrilla/log"
 	"net"
 	"net"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
@@ -41,6 +42,22 @@ type lentClients struct {
 	wg sync.WaitGroup
 	wg sync.WaitGroup
 }
 }
 
 
+// maps the callback on all lentClients
+func (c *lentClients) mapAll(callback func(p Poolable)) {
+	defer c.mu.Unlock()
+	c.mu.Lock()
+	for _, item := range c.m {
+		callback(item)
+	}
+}
+
+// operation performs an operation on a Poolable item using the callback
+func (c *lentClients) operation(callback func(p Poolable), item Poolable) {
+	defer c.mu.Unlock()
+	c.mu.Lock()
+	callback(item)
+}
+
 // NewPool creates a new pool of Clients.
 // NewPool creates a new pool of Clients.
 func NewPool(poolSize int) *Pool {
 func NewPool(poolSize int) *Pool {
 	return &Pool{
 	return &Pool{
@@ -65,10 +82,9 @@ func (p *Pool) ShutdownState() {
 	p.ShutdownChan <- 1             // release any waiting p.sem
 	p.ShutdownChan <- 1             // release any waiting p.sem
 
 
 	// set a low timeout
 	// set a low timeout
-	var c Poolable
-	for _, c = range p.activeClients.m {
-		c.setTimeout(time.Duration(int64(aVeryLowTimeout)))
-	}
+	p.activeClients.mapAll(func(p Poolable) {
+		p.setTimeout(time.Duration(int64(aVeryLowTimeout)))
+	})
 
 
 }
 }
 
 
@@ -93,12 +109,9 @@ func (p *Pool) IsShuttingDown() bool {
 
 
 // set a timeout for all lent clients
 // set a timeout for all lent clients
 func (p *Pool) SetTimeout(duration time.Duration) {
 func (p *Pool) SetTimeout(duration time.Duration) {
-	var client Poolable
-	p.activeClients.mu.Lock()
-	defer p.activeClients.mu.Unlock()
-	for _, client = range p.activeClients.m {
-		client.setTimeout(duration)
-	}
+	p.activeClients.mapAll(func(p Poolable) {
+		p.setTimeout(duration)
+	})
 }
 }
 
 
 // Gets the number of active clients that are currently
 // Gets the number of active clients that are currently
@@ -108,7 +121,7 @@ func (p *Pool) GetActiveClientsCount() int {
 }
 }
 
 
 // Borrow a Client from the pool. Will block if len(activeClients) > maxClients
 // Borrow a Client from the pool. Will block if len(activeClients) > maxClients
-func (p *Pool) Borrow(conn net.Conn, clientID uint64) (Poolable, error) {
+func (p *Pool) Borrow(conn net.Conn, clientID uint64, logger log.Logger) (Poolable, error) {
 	p.poolGuard.Lock()
 	p.poolGuard.Lock()
 	defer p.poolGuard.Unlock()
 	defer p.poolGuard.Unlock()
 
 
@@ -123,7 +136,7 @@ func (p *Pool) Borrow(conn net.Conn, clientID uint64) (Poolable, error) {
 		case c = <-p.pool:
 		case c = <-p.pool:
 			c.init(conn, clientID)
 			c.init(conn, clientID)
 		default:
 		default:
-			c = NewClient(conn, clientID)
+			c = NewClient(conn, clientID, logger)
 		}
 		}
 		p.activeClientsAdd(c)
 		p.activeClientsAdd(c)
 
 
@@ -146,15 +159,15 @@ func (p *Pool) Return(c Poolable) {
 }
 }
 
 
 func (p *Pool) activeClientsAdd(c Poolable) {
 func (p *Pool) activeClientsAdd(c Poolable) {
-	p.activeClients.mu.Lock()
-	p.activeClients.wg.Add(1)
-	p.activeClients.m[c.getID()] = c
-	p.activeClients.mu.Unlock()
+	p.activeClients.operation(func(item Poolable) {
+		p.activeClients.wg.Add(1)
+		p.activeClients.m[c.getID()] = item
+	}, c)
 }
 }
 
 
 func (p *Pool) activeClientsRemove(c Poolable) {
 func (p *Pool) activeClientsRemove(c Poolable) {
-	p.activeClients.mu.Lock()
-	p.activeClients.wg.Done()
-	delete(p.activeClients.m, c.getID())
-	p.activeClients.mu.Unlock()
+	p.activeClients.operation(func(item Poolable) {
+		delete(p.activeClients.m, item.getID())
+		p.activeClients.wg.Done()
+	}, c)
 }
 }

+ 454 - 0
response/enhanced.go

@@ -0,0 +1,454 @@
+package response
+
+import (
+	"fmt"
+)
+
+const (
+	// ClassSuccess specifies that the DSN is reporting a positive delivery
+	// action.  Detail sub-codes may provide notification of
+	// transformations required for delivery.
+	ClassSuccess = 2
+	// ClassTransientFailure - a persistent transient failure is one in which the message as
+	// sent is valid, but persistence of some temporary condition has
+	// caused abandonment or delay of attempts to send the message.
+	// If this code accompanies a delivery failure report, sending in
+	// the future may be successful.
+	ClassTransientFailure = 4
+	// ClassPermanentFailure - a permanent failure is one which is not likely to be resolved
+	// by resending the message in the current form.  Some change to
+	// the message or the destination must be made for successful
+	// delivery.
+	ClassPermanentFailure = 5
+)
+
+// class is a type for ClassSuccess, ClassTransientFailure and ClassPermanentFailure constants
+type class int
+
+// String implements stringer for the class type
+func (c class) String() string {
+	return fmt.Sprintf("%c00", c)
+}
+
+// codeMap for mapping Enhanced Status Code to Basic Code
+// Mapping according to https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xml
+// This might not be entirely useful
+var codeMap = struct {
+	m map[EnhancedStatusCode]int
+}{m: map[EnhancedStatusCode]int{
+
+	EnhancedStatusCode{ClassSuccess, OtherAddressStatus}:               250,
+	EnhancedStatusCode{ClassSuccess, DestinationMailboxAddressValid}:   250,
+	EnhancedStatusCode{ClassSuccess, OtherOrUndefinedMailSystemStatus}: 250,
+	EnhancedStatusCode{ClassSuccess, OtherOrUndefinedProtocolStatus}:   250,
+	EnhancedStatusCode{ClassSuccess, ConversionWithLossPerformed}:      250,
+	EnhancedStatusCode{ClassSuccess, ".6.8"}:                           252,
+	EnhancedStatusCode{ClassSuccess, ".7.0"}:                           220,
+
+	EnhancedStatusCode{ClassTransientFailure, BadDestinationMailboxAddress}:      451,
+	EnhancedStatusCode{ClassTransientFailure, BadSendersSystemAddress}:           451,
+	EnhancedStatusCode{ClassTransientFailure, MailingListExpansionProblem}:       450,
+	EnhancedStatusCode{ClassTransientFailure, OtherOrUndefinedMailSystemStatus}:  421,
+	EnhancedStatusCode{ClassTransientFailure, MailSystemFull}:                    452,
+	EnhancedStatusCode{ClassTransientFailure, SystemNotAcceptingNetworkMessages}: 453,
+	EnhancedStatusCode{ClassTransientFailure, NoAnswerFromHost}:                  451,
+	EnhancedStatusCode{ClassTransientFailure, BadConnection}:                     421,
+	EnhancedStatusCode{ClassTransientFailure, RoutingServerFailure}:              451,
+	EnhancedStatusCode{ClassTransientFailure, NetworkCongestion}:                 451,
+	EnhancedStatusCode{ClassTransientFailure, OtherOrUndefinedProtocolStatus}:    451,
+	EnhancedStatusCode{ClassTransientFailure, InvalidCommand}:                    430,
+	EnhancedStatusCode{ClassTransientFailure, TooManyRecipients}:                 452,
+	EnhancedStatusCode{ClassTransientFailure, InvalidCommandArguments}:           451,
+	EnhancedStatusCode{ClassTransientFailure, ".7.0"}:                            450,
+	EnhancedStatusCode{ClassTransientFailure, ".7.1"}:                            451,
+	EnhancedStatusCode{ClassTransientFailure, ".7.12"}:                           422,
+	EnhancedStatusCode{ClassTransientFailure, ".7.15"}:                           450,
+	EnhancedStatusCode{ClassTransientFailure, ".7.24"}:                           451,
+
+	EnhancedStatusCode{ClassPermanentFailure, BadDestinationMailboxAddress}:            550,
+	EnhancedStatusCode{ClassPermanentFailure, BadDestinationMailboxAddressSyntax}:      501,
+	EnhancedStatusCode{ClassPermanentFailure, BadSendersSystemAddress}:                 501,
+	EnhancedStatusCode{ClassPermanentFailure, ".1.10"}:                                 556,
+	EnhancedStatusCode{ClassPermanentFailure, MailboxFull}:                             552,
+	EnhancedStatusCode{ClassPermanentFailure, MessageLengthExceedsAdministrativeLimit}: 552,
+	EnhancedStatusCode{ClassPermanentFailure, OtherOrUndefinedMailSystemStatus}:        550,
+	EnhancedStatusCode{ClassPermanentFailure, MessageTooBigForSystem}:                  552,
+	EnhancedStatusCode{ClassPermanentFailure, RoutingServerFailure}:                    550,
+	EnhancedStatusCode{ClassPermanentFailure, OtherOrUndefinedProtocolStatus}:          501,
+	EnhancedStatusCode{ClassPermanentFailure, InvalidCommand}:                          500,
+	EnhancedStatusCode{ClassPermanentFailure, SyntaxError}:                             500,
+	EnhancedStatusCode{ClassPermanentFailure, InvalidCommandArguments}:                 501,
+	EnhancedStatusCode{ClassPermanentFailure, ".5.6"}:                                  500,
+	EnhancedStatusCode{ClassPermanentFailure, ConversionRequiredButNotSupported}:       554,
+	EnhancedStatusCode{ClassPermanentFailure, ".6.6"}:                                  554,
+	EnhancedStatusCode{ClassPermanentFailure, ".6.7"}:                                  553,
+	EnhancedStatusCode{ClassPermanentFailure, ".6.8"}:                                  550,
+	EnhancedStatusCode{ClassPermanentFailure, ".6.9"}:                                  550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.0"}:                                  550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.1"}:                                  551,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.2"}:                                  550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.4"}:                                  504,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.8"}:                                  554,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.9"}:                                  534,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.10"}:                                 523,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.11"}:                                 524,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.13"}:                                 525,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.14"}:                                 535,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.15"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.16"}:                                 552,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.17"}:                                 500,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.18"}:                                 500,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.19"}:                                 500,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.20"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.21"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.22"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.23"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.24"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.25"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.26"}:                                 550,
+	EnhancedStatusCode{ClassPermanentFailure, ".7.27"}:                                 550,
+}}
+
+var (
+	// Canned is to be read-only, except in the init() function
+	Canned Responses
+)
+
+// Responses has some already pre-constructed responses
+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
+
+	// The 400's
+	ErrorTooManyRecipients string
+	ErrorRelayDenied       string
+	ErrorShutdown          string
+
+	// The 200's
+	SuccessMailCmd       string
+	SuccessRcptCmd       string
+	SuccessResetCmd      string
+	SuccessVerifyCmd     string
+	SuccessNoopCmd       string
+	SuccessQuitCmd       string
+	SuccessDataCmd       string
+	SuccessStartTLSCmd   string
+	SuccessMessageQueued string
+}
+
+// Called automatically during package load to build up the Responses struct
+func init() {
+
+	// There's even a Wikipedia page for canned responses: https://en.wikipedia.org/wiki/Canned_response
+	Canned = Responses{}
+
+	Canned.FailLineTooLong = (&Response{
+		EnhancedCode: InvalidCommand,
+		BasicCode:    554,
+		Class:        ClassPermanentFailure,
+		Comment:      "Line too long.",
+	}).String()
+
+	Canned.FailNestedMailCmd = (&Response{
+		EnhancedCode: InvalidCommand,
+		BasicCode:    503,
+		Class:        ClassPermanentFailure,
+		Comment:      "Error: nested MAIL command",
+	}).String()
+
+	Canned.SuccessMailCmd = (&Response{
+		EnhancedCode: OtherAddressStatus,
+		Class:        ClassSuccess,
+	}).String()
+
+	Canned.SuccessRcptCmd = (&Response{
+		EnhancedCode: DestinationMailboxAddressValid,
+		Class:        ClassSuccess,
+	}).String()
+
+	Canned.SuccessResetCmd = Canned.SuccessMailCmd
+	Canned.SuccessNoopCmd = (&Response{
+		EnhancedCode: OtherStatus,
+		Class:        ClassSuccess,
+	}).String()
+
+	Canned.SuccessVerifyCmd = (&Response{
+		EnhancedCode: OtherOrUndefinedProtocolStatus,
+		BasicCode:    252,
+		Class:        ClassSuccess,
+		Comment:      "Cannot verify user",
+	}).String()
+
+	Canned.ErrorTooManyRecipients = (&Response{
+		EnhancedCode: TooManyRecipients,
+		BasicCode:    452,
+		Class:        ClassTransientFailure,
+		Comment:      "Too many recipients",
+	}).String()
+
+	Canned.ErrorRelayDenied = (&Response{
+		EnhancedCode: BadDestinationMailboxAddress,
+		BasicCode:    454,
+		Class:        ClassTransientFailure,
+		Comment:      "Error: Relay access denied: ",
+	}).String()
+
+	Canned.SuccessQuitCmd = (&Response{
+		EnhancedCode: OtherStatus,
+		BasicCode:    221,
+		Class:        ClassSuccess,
+		Comment:      "Bye",
+	}).String()
+
+	Canned.FailNoSenderDataCmd = (&Response{
+		EnhancedCode: InvalidCommand,
+		BasicCode:    503,
+		Class:        ClassPermanentFailure,
+		Comment:      "Error: No sender",
+	}).String()
+
+	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.SuccessStartTLSCmd = (&Response{
+		EnhancedCode: OtherStatus,
+		BasicCode:    220,
+		Class:        ClassSuccess,
+		Comment:      "Ready to start TLS",
+	}).String()
+
+	Canned.FailUnrecognizedCmd = (&Response{
+		EnhancedCode: InvalidCommand,
+		BasicCode:    554,
+		Class:        ClassPermanentFailure,
+		Comment:      "Unrecognized command",
+	}).String()
+
+	Canned.FailMaxUnrecognizedCmd = (&Response{
+		EnhancedCode: InvalidCommand,
+		BasicCode:    554,
+		Class:        ClassPermanentFailure,
+		Comment:      "Too many unrecognized commands",
+	}).String()
+
+	Canned.ErrorShutdown = (&Response{
+		EnhancedCode: OtherOrUndefinedMailSystemStatus,
+		BasicCode:    421,
+		Class:        ClassTransientFailure,
+		Comment:      "Server is shutting down. Please try again later. Sayonara!",
+	}).String()
+
+	Canned.FailReadLimitExceededDataCmd = (&Response{
+		EnhancedCode: SyntaxError,
+		BasicCode:    550,
+		Class:        ClassPermanentFailure,
+		Comment:      "Error: ",
+	}).String()
+
+	Canned.FailMessageSizeExceeded = (&Response{
+		EnhancedCode: SyntaxError,
+		BasicCode:    550,
+		Class:        ClassPermanentFailure,
+		Comment:      "Error: ",
+	}).String()
+
+	Canned.FailReadErrorDataCmd = (&Response{
+		EnhancedCode: OtherOrUndefinedMailSystemStatus,
+		BasicCode:    451,
+		Class:        ClassTransientFailure,
+		Comment:      "Error: ",
+	}).String()
+
+	Canned.FailPathTooLong = (&Response{
+		EnhancedCode: InvalidCommandArguments,
+		BasicCode:    550,
+		Class:        ClassPermanentFailure,
+		Comment:      "Path too long",
+	}).String()
+
+	Canned.FailInvalidAddress = (&Response{
+		EnhancedCode: InvalidCommandArguments,
+		BasicCode:    501,
+		Class:        ClassPermanentFailure,
+		Comment:      "Invalid address",
+	}).String()
+
+	Canned.FailLocalPartTooLong = (&Response{
+		EnhancedCode: InvalidCommandArguments,
+		BasicCode:    550,
+		Class:        ClassPermanentFailure,
+		Comment:      "Local part too long, cannot exceed 64 characters",
+	}).String()
+
+	Canned.FailDomainTooLong = (&Response{
+		EnhancedCode: InvalidCommandArguments,
+		BasicCode:    550,
+		Class:        ClassPermanentFailure,
+		Comment:      "Domain cannot exceed 255 characters",
+	}).String()
+
+	Canned.FailBackendNotRunning = (&Response{
+		EnhancedCode: OtherOrUndefinedProtocolStatus,
+		BasicCode:    554,
+		Class:        ClassPermanentFailure,
+		Comment:      "Transaction failed - backend not running ",
+	}).String()
+
+	Canned.FailBackendTransaction = (&Response{
+		EnhancedCode: OtherOrUndefinedProtocolStatus,
+		BasicCode:    554,
+		Class:        ClassPermanentFailure,
+		Comment:      "Error: ",
+	}).String()
+
+	Canned.SuccessMessageQueued = (&Response{
+		EnhancedCode: OtherStatus,
+		BasicCode:    250,
+		Class:        ClassSuccess,
+		Comment:      "OK : queued as ",
+	}).String()
+
+	Canned.FailBackendTimeout = (&Response{
+		EnhancedCode: OtherOrUndefinedProtocolStatus,
+		BasicCode:    554,
+		Class:        ClassPermanentFailure,
+		Comment:      "Error: transaction timeout",
+	}).String()
+
+}
+
+// DefaultMap contains defined default codes (RfC 3463)
+const (
+	OtherStatus                             = ".0.0"
+	OtherAddressStatus                      = ".1.0"
+	BadDestinationMailboxAddress            = ".1.1"
+	BadDestinationSystemAddress             = ".1.2"
+	BadDestinationMailboxAddressSyntax      = ".1.3"
+	DestinationMailboxAddressAmbiguous      = ".1.4"
+	DestinationMailboxAddressValid          = ".1.5"
+	MailboxHasMoved                         = ".1.6"
+	BadSendersMailboxAddressSyntax          = ".1.7"
+	BadSendersSystemAddress                 = ".1.8"
+	OtherOrUndefinedMailboxStatus           = ".2.0"
+	MailboxDisabled                         = ".2.1"
+	MailboxFull                             = ".2.2"
+	MessageLengthExceedsAdministrativeLimit = ".2.3"
+	MailingListExpansionProblem             = ".2.4"
+	OtherOrUndefinedMailSystemStatus        = ".3.0"
+	MailSystemFull                          = ".3.1"
+	SystemNotAcceptingNetworkMessages       = ".3.2"
+	SystemNotCapableOfSelectedFeatures      = ".3.3"
+	MessageTooBigForSystem                  = ".3.4"
+	OtherOrUndefinedNetworkOrRoutingStatus  = ".4.0"
+	NoAnswerFromHost                        = ".4.1"
+	BadConnection                           = ".4.2"
+	RoutingServerFailure                    = ".4.3"
+	UnableToRoute                           = ".4.4"
+	NetworkCongestion                       = ".4.5"
+	RoutingLoopDetected                     = ".4.6"
+	DeliveryTimeExpired                     = ".4.7"
+	OtherOrUndefinedProtocolStatus          = ".5.0"
+	InvalidCommand                          = ".5.1"
+	SyntaxError                             = ".5.2"
+	TooManyRecipients                       = ".5.3"
+	InvalidCommandArguments                 = ".5.4"
+	WrongProtocolVersion                    = ".5.5"
+	OtherOrUndefinedMediaError              = ".6.0"
+	MediaNotSupported                       = ".6.1"
+	ConversionRequiredAndProhibited         = ".6.2"
+	ConversionRequiredButNotSupported       = ".6.3"
+	ConversionWithLossPerformed             = ".6.4"
+	ConversionFailed                        = ".6.5"
+)
+
+var defaultTexts = struct {
+	m map[EnhancedStatusCode]string
+}{m: map[EnhancedStatusCode]string{
+	EnhancedStatusCode{ClassSuccess, ".0.0"}:          "OK",
+	EnhancedStatusCode{ClassSuccess, ".1.0"}:          "OK",
+	EnhancedStatusCode{ClassSuccess, ".1.5"}:          "OK",
+	EnhancedStatusCode{ClassSuccess, ".5.0"}:          "OK",
+	EnhancedStatusCode{ClassTransientFailure, ".5.3"}: "Too many recipients",
+	EnhancedStatusCode{ClassTransientFailure, ".5.4"}: "Relay access denied",
+	EnhancedStatusCode{ClassPermanentFailure, ".5.1"}: "Invalid command",
+}}
+
+// Response type for Stringer interface
+type Response struct {
+	EnhancedCode subjectDetail
+	BasicCode    int
+	Class        class
+	// Comment is optional
+	Comment string
+}
+
+// it looks like this ".5.4"
+type subjectDetail string
+
+// EnhancedStatus are the ones that look like 2.1.0
+type EnhancedStatusCode struct {
+	Class             class
+	SubjectDetailCode subjectDetail
+}
+
+// String returns a string representation of EnhancedStatus
+func (e EnhancedStatusCode) String() string {
+	return fmt.Sprintf("%d%s", e.Class, e.SubjectDetailCode)
+}
+
+// String returns a custom Response as a string
+func (r *Response) String() string {
+
+	basicCode := r.BasicCode
+	comment := r.Comment
+	if len(comment) == 0 && r.BasicCode == 0 {
+		var ok bool
+		if comment, ok = defaultTexts.m[EnhancedStatusCode{r.Class, r.EnhancedCode}]; !ok {
+			switch r.Class {
+			case 2:
+				comment = "OK"
+			case 4:
+				comment = "Temporary failure."
+			case 5:
+				comment = "Permanent failure."
+			}
+		}
+	}
+	e := EnhancedStatusCode{r.Class, r.EnhancedCode}
+	if r.BasicCode == 0 {
+		basicCode = getBasicStatusCode(e)
+	}
+
+	return fmt.Sprintf("%d %s %s", basicCode, e.String(), comment)
+}
+
+// getBasicStatusCode gets the basic status code from codeMap, or fallback code if not mapped
+func getBasicStatusCode(e EnhancedStatusCode) int {
+	if val, ok := codeMap.m[e]; ok {
+		return val
+	}
+	// Fallback if code is not defined
+	return int(e.Class) * 100
+}

+ 63 - 0
response/enhanced_test.go

@@ -0,0 +1,63 @@
+package response
+
+import (
+	"testing"
+)
+
+func TestClass(t *testing.T) {
+	if ClassPermanentFailure != 5 {
+		t.Error("ClassPermanentFailure is not 5")
+	}
+	if ClassTransientFailure != 4 {
+		t.Error("ClassTransientFailure is not 4")
+	}
+	if ClassSuccess != 2 {
+		t.Error("ClassSuccess is not 2")
+	}
+}
+
+func TestGetBasicStatusCode(t *testing.T) {
+	// Known status code
+	a := getBasicStatusCode(EnhancedStatusCode{2, OtherOrUndefinedProtocolStatus})
+	if a != 250 {
+		t.Errorf("getBasicStatusCode. Int \"%d\" not expected.", a)
+	}
+
+	// Unknown status code
+	b := getBasicStatusCode(EnhancedStatusCode{2, OtherStatus})
+	if b != 200 {
+		t.Errorf("getBasicStatusCode. Int \"%d\" not expected.", b)
+	}
+}
+
+// TestString for the String function
+func TestCustomString(t *testing.T) {
+	// Basic testing
+	resp := &Response{
+		EnhancedCode: OtherStatus,
+		BasicCode:    200,
+		Class:        ClassSuccess,
+		Comment:      "Test",
+	}
+
+	if resp.String() != "200 2.0.0 Test" {
+		t.Errorf("CustomString failed. String \"%s\" not expected.", resp)
+	}
+
+	// Default String
+	resp2 := &Response{
+		EnhancedCode: OtherStatus,
+		Class:        ClassSuccess,
+	}
+	if resp2.String() != "200 2.0.0 OK" {
+		t.Errorf("String failed. String \"%s\" not expected.", resp2)
+	}
+}
+
+func TestBuildEnhancedResponseFromDefaultStatus(t *testing.T) {
+	//a := buildEnhancedResponseFromDefaultStatus(ClassPermanentFailure, InvalidCommand)
+	a := EnhancedStatusCode{ClassPermanentFailure, InvalidCommand}.String()
+	if a != "5.5.1" {
+		t.Errorf("buildEnhancedResponseFromDefaultStatus failed. String \"%s\" not expected.", a)
+	}
+}

+ 160 - 0
response/quote.go

@@ -0,0 +1,160 @@
+package response
+
+import (
+	"fmt"
+	"math/rand"
+	"time"
+)
+
+// This is an easter egg
+
+const CRLF = "\r\n"
+
+var quotes = struct {
+	m map[int]string
+}{m: map[int]string{
+	0: "214 Maude Lebowski: He's a good man....and thorough.",
+	1: "214 The Dude: I had a rough night and I hate the f***ing Eagles, man.",
+	2: "214 Walter Sobchak: The chinaman is not the issue here... also dude, Asian American please",
+	3: "214 The Dude: Walter, the chinamen who peed on my rug I can't give him a bill, so what the f**k are you talking about?",
+	4: "214 The Dude: Hey, I know that guy, he's a nihilist. Karl Hungus.",
+	5: "214-Malibu Police Chief: Mr. Treehorn tells us that he had to eject you from his garden party; that you were drunk and abusive." + CRLF +
+		"214 The Dude: Mr. Treehorn treats objects like women, man.",
+	6: "214 Walter Sobchak: Shut the f**k up, Donny!",
+	7: "214-Donny: Shut the f**k up, Donny!" + CRLF +
+		"214 Walter Sobchak: Shut the f**k up, Donny!",
+	8:  "214 The Dude: It really tied the room together.",
+	9:  "214 Walter Sobchak: Is this your homework, Larry?",
+	10: "214 The Dude: Who the f**k are the Knutsens?",
+	11: "214 The Dude: Yeah,well, that's just, like, your opinion, man.",
+	12: "214-Walter Sobchak: Am I the only one who gives a s**t about the rules?!" + CRLF +
+		"214 Walter Sobchak: Am I the only one who gives a s**t about the rules?",
+	13: "214-Walter Sobchak: Am I wrong?" + CRLF +
+		"214-The Dude: No, you're not wrong Walter, you're just an ass-hole." +
+		"214 Walter Sobchak: Okay then.",
+	14: "214-Private Snoop: you see what happens lebowski?" + CRLF +
+		"214-The Dude: nobody calls me lebowski, you got the wrong guy, I'm the the dude, man." + CRLF +
+		"214-Private Snoop: Your name's Lebowski, Lebowski. Your wife is Bunny." + CRLF +
+		"214-The Dude: My wife? Bunny? Do you see a wedding ring on my finger? " + CRLF +
+		"214 Does this place look like I'm f**kin married? The toilet seat's up man!",
+	15: "214-The Dude: Yeah man. it really tied the room together." + CRLF +
+		"214-Donny: What tied the room together dude?" + CRLF +
+		"214-The Dude: My rug." + CRLF +
+		"214-Walter Sobchak: Were you listening to the Dude's story, Donny?" + CRLF +
+		"214-Donny: I was bowling." + CRLF +
+		"214-Walter Sobchak: So then you have no frame of reference here, Donny, " + CRLF +
+		"214 You're like a child who wonders in the middle of movie.",
+	16: "214-The Dude: She probably kidnapped herself." + CRLF +
+		"214-Donny: What do you mean dude?" + CRLF +
+		"214-The Dude: Rug Peers did not do this. look at it. " + CRLF +
+		"214-A young trophy wife, marries this guy for his money, she figures he " + CRLF +
+		"214-hasn't given her enough, she owes money all over town." + CRLF +
+		"214 Walter Sobchak: That f**kin bitch.",
+	17: "214 Walter Sobchak: Forget it, Donny, you're out of your element!",
+	18: "214-Walter Sobchak: You want a toe? I can get you a toe, believe me." + CRLF +
+		"214-There are ways, Dude. You don't wanna know about it, believe me. " + CRLF +
+		"214-The Dude: Yeah, but Walter." +
+		"214 Walter Sobchak: Hell, I can get you a toe by 3 o'clock this afternoon with nail polish.",
+	19: "214 Walter Sobchak: Calmer then you are.",
+	20: "214 Walter Sobchak: You are entering a world of pain",
+	21: "214 The Dude: This aggression will not stand man.",
+	22: "214 The Dude: His dudeness, duder, or el dudorino",
+	23: "214 Walter Sobchak: Has the whole world gone crazy!",
+	24: "214 Walter Sobchak: Calm down your being very undude.",
+	25: "214-Donny: Are these the Nazis, Walter?" + CRLF +
+		"214 Walter Sobchak: No Donny, these men are nihilists. There's nothing to be afraid of.",
+	26: "214 Walter Sobchak: Well, it was parked in the handicapped zone. Perhaps they towed it.",
+	27: "214-Private Snoop: I'm a brother shamus!" + CRLF +
+		"214 The Dude: Brother Seamus? Like an Irish monk?",
+	28: "214 Walter Sobchak: Have you ever of Vietnam? You're about to enter a world of pain!",
+	29: "214-Donny: What's a pederast, Walter?" + CRLF +
+		"214 Walter Sobchak: Shut the f**k up, Donny.",
+	30: "214 The Dude: Hey, careful, man, there's a beverage here!",
+	31: "214 The Stranger: Sometimes you eat the bar and sometimes, well, the bar eats you.",
+	32: "214 Walter Sobchak: Goodnight, sweet prince.",
+	33: "214 Jackie Treehorn: People forget the brain is the biggest erogenous zone.",
+	34: "214-The Big Lebowski: What makes a man? Is it doing the right thing?" + CRLF +
+		"214 The Dude: Sure, that and a pair of testicles.",
+	35: "214 The Dude: At least I'm housebroken.",
+	36: "214-Walter Sobchak: Three thousand years of beautiful tradition, from Moses to Sandy Koufax." + CRLF +
+		"214 You're goddamn right I'm living in the f**king past!",
+	37: "214-The Stranger: There's just one thing, dude." + CRLF +
+		"214-The Dude: What's that?" + CRLF +
+		"214-The Stranger: Do you have to use so many cuss words?" + CRLF +
+		"214 The Dude: What the f**k you talkin' about?",
+	38: "214-Walter Sobchak: I mean, say what you want about the tenets of National Socialism, " + CRLF +
+		"214 Dude, at least it's an ethos.",
+	39: "214 The Dude: My only hope is that the Big Lebowski kills me before the Germans can cut my d**k off.",
+	40: "214 The Dude: You human paraquat!",
+	41: "214 The Dude: Strikes and gutters, ups and downs.",
+	42: "214 The Dude: Sooner or later you are going to have to face the fact that your a moron.",
+	43: "214-The Dude: The fixes the cable?" + CRLF +
+		"214 Maude Lebowski: Don't be fatuous Jerry.",
+	44: "214 The Dude: Yeah, well, that's just, like, your opinion, man.",
+	45: "214 The Dude: I don't need your sympathy, I need my Johnson.",
+	46: "214 Donny: I am the walrus.",
+	47: "214 The Dude: We f**ked it up!",
+	48: "214 Jesus Quintana: You got that right, NO ONE f**ks with the jesus.",
+	49: "214 Walter Sobchak: You can say what you want about the tenets of national socialism but at least it's an ethos.",
+	50: "214-Walter Sobchak: f**king Germans. Nothing changes. f**king Nazis." + CRLF +
+		"214-Donny: They were Nazis, Dude?" + CRLF +
+		"214 Walter Sobchak: Oh, come on Donny, they were threatening castration! Are we gonna split hairs here? Am I wrong?",
+	51: "214 Walter Sobchak: [pulls out a gun] Smokey, my friend, you are entering a world of pain.",
+	52: "214 Walter Sobchak: This is what happens when you f**k a stranger in the ass!",
+	53: "214-The Dude: We dropped off the money." + CRLF +
+		"214-The Big Lebowski: *We*!?" + CRLF +
+		"214 The Dude: *I*; the royal we.",
+	54: "214 Walter Sobchak: You see what happens larry when you f**k a stranger in the ass.",
+	55: "214 The Dude: The Dude abides.",
+	56: "214 Walter Sobchak: f**k it dude, lets go bowling.",
+	57: "214 The Dude: I can't be worrying about that s**t. Life goes on, man.",
+	58: "214 Walter Sobchak: The ringer cannot look empty.",
+	59: "214-Malibu Police Chief: I don't like your jerk-off name, I don't like your jerk-off face," + CRLF +
+		"214 I don't like your j3rk-off behavior, and I don't like you... j3rk-off.",
+	60: "214-Walter Sobchak: Has the whole world gone CRAZY? Am I the only one around here who gives" + CRLF +
+		"214 a s**t about the rules? You think I'm f**kin' around, MARK IT ZERO!",
+	61: "214 Walter Sobchak: Look, Larry. Have you ever heard of Vietnam?",
+	62: "214 The Dude: Ha hey, this is a private residence man.",
+	63: "214 The Dude: Obviously you're not a golfer.",
+	64: "214 Walter Sobchak: You know, Dude, I myself dabbled in pacifism once. Not in Nam, of course.",
+	65: "214 Walter Sobchak: Donny, you're out of your element!",
+	66: "214 The Dude: Another caucasian, Gary.",
+	67: "214-Bunny Lebowski: I'll s**k your c**k for a thousand dollars." + CRLF +
+		"214-Brandt: Ah... Ha... ha... HA! Yes, we're all very fond of her." + CRLF +
+		"214-Bunny Lebowski: Brandt can't watch, though. Or it's an extra hundred." + CRLF +
+		"214 The Dude: Okay... just give me a minute. I gotta go find a cash machine...",
+	68: "214 Nihilist: Ve vont ze mawney Lebowski!",
+	69: "214 Walter Sobchak: Eight-year-olds, dude.",
+	70: "214-The Dude: They peed on my rug, man!" + CRLF +
+		"214-Walter Sobchak: f**king Nazis." + CRLF +
+		"214-Donny: I don't know if they were Nazis, Walter..." + CRLF +
+		"214 Walter Sobchak: Shut the f**k up, Donny. They were threatening castration!",
+	71: "214 Jesus Quintana: I don't f**king care, it don't matter to Jesus.",
+	72: "214-The Dude: Where's my car?" + CRLF +
+		"214 Walter Sobchak: It was parked in a handicap zone, perhaps they towed it.",
+	73: "214-Bunny Lebowski: Uli doesn't care about anything. He's a Nihilist!" + CRLF +
+		"214 The Dude: Ah, that must be exhausting!",
+	74: "214 Walter Sobchak: Smoky this is not Nam this is Bowling there are rules.",
+	75: "214 Maude Lebowski: Vagina.",
+	76: "214-Jesus Quintana: Let me tell you something pendejo. You pull any of your crazy s**t with us." + CRLF +
+		"214-You flash your piece out on the lanes. I'll take it away from you and stick up your alps" + CRLF +
+		"214-and pull the f**king trigger 'til it goes click." + CRLF +
+		"214-The Dude: ...Jesus" + CRLF +
+		"214 Jesus Quintana: You said it man, nobody f**ks with the Jesus.",
+	77: "214-The Dude: You brought a f**king Pomeranian bowling?" + CRLF +
+		"214-Walter Sobchak: Bought it bowling? I didn't rent it shoes. " + CRLF +
+		"214 I'm not buying it a f**king beer. It's not taking your f**king turn, Dude.",
+	78: "214 Walter Sobchak: Mark it as a zero.",
+	79: "214-The Stranger: The Dude abides. I don't know about you, but I take comfort in that. " + CRLF +
+		"214 It's good knowing he's out there, the Dude, takin' 'er easy for all us sinners.",
+	80: "214 Walter Sobchak: Aw, f**k it Dude. Let's go bowling.",
+	81: "214 Walter Sobchak: Life does not stop and start at your convenience you miserable piece of s**t.",
+	82: "214 Walter Sobchak: I told that kraut a f**king thousand times that I don't roll on Shabbos!",
+	83: "214 Walter Sobchak: This is what happens when you find a stranger in the alps!",
+}}
+
+// GetQuote returns a random quote from The big Lebowski
+func GetQuote() string {
+	rand.Seed(time.Now().UnixNano())
+	return fmt.Sprintf("%s", quotes.m[rand.Intn(len(quotes.m))])
+}

+ 254 - 164
server.go

@@ -6,14 +6,16 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net"
 	"net"
+	"runtime"
 	"strings"
 	"strings"
+	"sync"
+	"sync/atomic"
 	"time"
 	"time"
 
 
-	"runtime"
-
-	log "github.com/Sirupsen/logrus"
-
-	"sync"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/envelope"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/response"
 )
 )
 
 
 const (
 const (
@@ -31,72 +33,171 @@ const (
 	RFC2821LimitRecipients = 100
 	RFC2821LimitRecipients = 100
 )
 )
 
 
+const (
+	// server has just been created
+	ServerStateNew = iota
+	// Server has just been stopped
+	ServerStateStopped
+	// Server has been started and is running
+	ServerStateRunning
+	// Server could not start due to an error
+	ServerStateStartError
+)
+
 // Server listens for SMTP clients on the port specified in its config
 // Server listens for SMTP clients on the port specified in its config
 type server struct {
 type server struct {
-	config         *ServerConfig
-	backend        Backend
-	tlsConfig      *tls.Config
-	maxSize        int64
-	timeout        time.Duration
-	clientPool     *Pool
-	wg             sync.WaitGroup // for waiting to shutdown
-	listener       net.Listener
-	closedListener chan (bool)
+	configStore     atomic.Value // stores guerrilla.ServerConfig
+	backend         backends.Backend
+	tlsConfigStore  atomic.Value
+	timeout         atomic.Value // stores time.Duration
+	listenInterface string
+	clientPool      *Pool
+	wg              sync.WaitGroup // for waiting to shutdown
+	listener        net.Listener
+	closedListener  chan (bool)
+	hosts           allowedHosts // stores map[string]bool for faster lookup
+	state           int
+	mainlog         log.Logger
+	log             log.Logger
+	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
+	logStore     atomic.Value
+	mainlogStore atomic.Value
+}
+
+type allowedHosts struct {
+	table map[string]bool // host lookup table
+	m     sync.Mutex      // guard access to the map
 }
 }
 
 
 // Creates and returns a new ready-to-run Server from a configuration
 // Creates and returns a new ready-to-run Server from a configuration
-func newServer(sc ServerConfig, b *Backend) (*server, error) {
+func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, error) {
 	server := &server{
 	server := &server{
-		config:         &sc,
-		backend:        *b,
-		maxSize:        sc.MaxSize,
-		timeout:        time.Duration(sc.Timeout),
-		clientPool:     NewPool(sc.MaxClients),
-		closedListener: make(chan (bool), 1),
+		backend:         b,
+		clientPool:      NewPool(sc.MaxClients),
+		closedListener:  make(chan (bool), 1),
+		listenInterface: sc.ListenInterface,
+		state:           ServerStateNew,
+		mainlog:         l,
+	}
+	var logOpenError error
+	if sc.LogFile == "" {
+		// none set, use the same log file as mainlog
+		server.log, logOpenError = log.GetLogger(server.mainlog.GetLogDest())
+	} else {
+		server.log, logOpenError = log.GetLogger(sc.LogFile)
+	}
+	if logOpenError != nil {
+		server.log.WithError(logOpenError).Errorf("Failed creating a logger for server [%s]", sc.ListenInterface)
 	}
 	}
-	if server.config.TLSAlwaysOn || server.config.StartTLSOn {
-		cert, err := tls.LoadX509KeyPair(server.config.PublicKeyFile, server.config.PrivateKeyFile)
+
+	// set to same level
+	server.log.SetLevel(server.mainlog.GetLevel())
+
+	server.setConfig(sc)
+	server.setTimeout(sc.Timeout)
+	if err := server.configureSSL(); err != nil {
+		return server, err
+	}
+	return server, nil
+}
+
+func (s *server) configureSSL() error {
+	sConfig := s.configStore.Load().(ServerConfig)
+	if sConfig.TLSAlwaysOn || sConfig.StartTLSOn {
+		cert, err := tls.LoadX509KeyPair(sConfig.PublicKeyFile, sConfig.PrivateKeyFile)
 		if err != nil {
 		if err != nil {
-			return nil, fmt.Errorf("Error loading TLS certificate: %s", err.Error())
+			return fmt.Errorf("error while loading the certificate: %s", err)
 		}
 		}
-		server.tlsConfig = &tls.Config{
+		tlsConfig := &tls.Config{
 			Certificates: []tls.Certificate{cert},
 			Certificates: []tls.Certificate{cert},
 			ClientAuth:   tls.VerifyClientCertIfGiven,
 			ClientAuth:   tls.VerifyClientCertIfGiven,
-			ServerName:   server.config.Hostname,
-			Rand:         rand.Reader,
+			ServerName:   sConfig.Hostname,
 		}
 		}
+		tlsConfig.Rand = rand.Reader
+		s.tlsConfigStore.Store(tlsConfig)
 	}
 	}
-	return server, nil
+	return nil
+}
+
+// configureLog checks to see if there is a new logger, so that the server.log can be safely changed
+// this function is not gorotine safe, although it'll read the new value safely
+func (s *server) configureLog() {
+	// when log changed
+	if l, ok := s.logStore.Load().(log.Logger); ok {
+		if l != s.log {
+			s.log = l
+		}
+	}
+	// when mainlog changed
+	if ml, ok := s.mainlogStore.Load().(log.Logger); ok {
+		if ml != s.mainlog {
+			s.mainlog = ml
+		}
+	}
+}
+
+// Set the timeout for the server and all clients
+func (server *server) setTimeout(seconds int) {
+	duration := time.Duration(int64(seconds))
+	server.clientPool.SetTimeout(duration)
+	server.timeout.Store(duration)
+}
+
+// goroutine safe config store
+func (server *server) setConfig(sc *ServerConfig) {
+	server.configStore.Store(*sc)
 }
 }
 
 
-// Begin accepting SMTP clients
+// goroutine safe
+func (server *server) isEnabled() bool {
+	sc := server.configStore.Load().(ServerConfig)
+	return sc.IsEnabled
+}
+
+// Set the allowed hosts for the server
+func (server *server) setAllowedHosts(allowedHosts []string) {
+	defer server.hosts.m.Unlock()
+	server.hosts.m.Lock()
+	server.hosts.table = make(map[string]bool, len(allowedHosts))
+	for _, h := range allowedHosts {
+		server.hosts.table[strings.ToLower(h)] = true
+	}
+}
+
+// Begin accepting SMTP clients. Will block unless there is an error or server.Shutdown() is called
 func (server *server) Start(startWG *sync.WaitGroup) error {
 func (server *server) Start(startWG *sync.WaitGroup) error {
 	var clientID uint64
 	var clientID uint64
 	clientID = 0
 	clientID = 0
 
 
-	listener, err := net.Listen("tcp", server.config.ListenInterface)
+	listener, err := net.Listen("tcp", server.listenInterface)
 	server.listener = listener
 	server.listener = listener
 	if err != nil {
 	if err != nil {
-		return fmt.Errorf("[%s] Cannot listen on port: %s ", server.config.ListenInterface, err.Error())
+		startWG.Done() // don't wait for me
+		server.state = ServerStateStartError
+		return fmt.Errorf("[%s] Cannot listen on port: %s ", server.listenInterface, err.Error())
 	}
 	}
 
 
-	log.Infof("Listening on TCP %s", server.config.ListenInterface)
-	startWG.Done() // start successful
+	server.log.Infof("Listening on TCP %s", server.listenInterface)
+	server.state = ServerStateRunning
+	startWG.Done() // start successful, don't wait for me
 
 
 	for {
 	for {
-		log.Debugf("[%s] Waiting for a new client. Next Client ID: %d", server.config.ListenInterface, clientID+1)
+		server.log.Debugf("[%s] Waiting for a new client. Next Client ID: %d", server.listenInterface, clientID+1)
 		conn, err := listener.Accept()
 		conn, err := listener.Accept()
+		server.configureLog()
 		clientID++
 		clientID++
 		if err != nil {
 		if err != nil {
 			if e, ok := err.(net.Error); ok && !e.Temporary() {
 			if e, ok := err.(net.Error); ok && !e.Temporary() {
-				log.Infof("Server [%s] has stopped accepting new clients", server.config.ListenInterface)
+				server.log.Infof("Server [%s] has stopped accepting new clients", server.listenInterface)
 				// the listener has been closed, wait for clients to exit
 				// the listener has been closed, wait for clients to exit
-				log.Infof("shutting down pool [%s]", server.config.ListenInterface)
+				server.log.Infof("shutting down pool [%s]", server.listenInterface)
+				server.clientPool.ShutdownState()
 				server.clientPool.ShutdownWait()
 				server.clientPool.ShutdownWait()
+				server.state = ServerStateStopped
 				server.closedListener <- true
 				server.closedListener <- true
 				return nil
 				return nil
 			}
 			}
-			log.WithError(err).Info("Temporary error accepting client")
+			server.mainlog.WithError(err).Info("Temporary error accepting client")
 			continue
 			continue
 		}
 		}
 		go func(p Poolable, borrow_err error) {
 		go func(p Poolable, borrow_err error) {
@@ -105,28 +206,30 @@ func (server *server) Start(startWG *sync.WaitGroup) error {
 				server.handleClient(c)
 				server.handleClient(c)
 				server.clientPool.Return(c)
 				server.clientPool.Return(c)
 			} else {
 			} else {
-				log.WithError(borrow_err).Info("couldn't borrow a new client")
+				server.log.WithError(borrow_err).Info("couldn't borrow a new client")
 				// we could not get a client, so close the connection.
 				// we could not get a client, so close the connection.
 				conn.Close()
 				conn.Close()
 
 
 			}
 			}
 			// intentionally placed Borrow in args so that it's called in the
 			// intentionally placed Borrow in args so that it's called in the
 			// same main goroutine.
 			// same main goroutine.
-		}(server.clientPool.Borrow(conn, clientID))
+		}(server.clientPool.Borrow(conn, clientID, server.log))
 
 
 	}
 	}
 }
 }
 
 
 func (server *server) Shutdown() {
 func (server *server) Shutdown() {
-	server.clientPool.ShutdownState()
 	if server.listener != nil {
 	if server.listener != nil {
+		// This will cause Start function to return, by causing an error on listener.Accept
 		server.listener.Close()
 		server.listener.Close()
-		// wait for the listener to close.
+		// wait for the listener to listener.Accept
 		<-server.closedListener
 		<-server.closedListener
 		// At this point Start will exit and close down the pool
 		// At this point Start will exit and close down the pool
 	} else {
 	} else {
+		server.clientPool.ShutdownState()
 		// listener already closed, wait for clients to exit
 		// listener already closed, wait for clients to exit
 		server.clientPool.ShutdownWait()
 		server.clientPool.ShutdownWait()
+		server.state = ServerStateStopped
 	}
 	}
 }
 }
 
 
@@ -136,91 +239,41 @@ func (server *server) GetActiveClientsCount() int {
 
 
 // Verifies that the host is a valid recipient.
 // Verifies that the host is a valid recipient.
 func (server *server) allowsHost(host string) bool {
 func (server *server) allowsHost(host string) bool {
-	for _, allowed := range server.config.AllowedHosts {
-		if host == allowed {
-			return true
-		}
+	defer server.hosts.m.Unlock()
+	server.hosts.m.Lock()
+	if _, ok := server.hosts.table[strings.ToLower(host)]; ok {
+		return true
 	}
 	}
 	return false
 	return false
 }
 }
 
 
-// Upgrades a client connection to TLS
-func (server *server) upgradeToTLS(client *client) bool {
-	tlsConn := tls.Server(client.conn, server.tlsConfig)
-	err := tlsConn.Handshake()
-	if err != nil {
-		log.WithError(err).Warn("[%s] Failed TLS handshake", client.RemoteAddress)
-		return false
-	}
-	client.conn = net.Conn(tlsConn)
-	client.bufout.Reset(client.conn)
-	client.bufin.Reset(client.conn)
-	client.TLS = true
-
-	return true
-}
-
-// Closes a client connection
-func (server *server) closeConn(client *client) {
-	client.conn.Close()
-	client.conn = nil
-	log.WithFields(map[string]interface{}{
-		"event":   "disconnect",
-		"address": client.RemoteAddress,
-		"helo":    client.Helo,
-		"id":      client.ID,
-	}).Info("Close client")
-}
-
 // Reads from the client until a terminating sequence is encountered,
 // Reads from the client until a terminating sequence is encountered,
 // or until a timeout occurs.
 // or until a timeout occurs.
-func (server *server) read(client *client) (string, error) {
+func (server *server) readCommand(client *client, maxSize int64) (string, error) {
 	var input, reply string
 	var input, reply string
 	var err error
 	var err error
-
 	// In command state, stop reading at line breaks
 	// In command state, stop reading at line breaks
 	suffix := "\r\n"
 	suffix := "\r\n"
-	if client.state == ClientData {
-		// In data state, stop reading at solo periods
-		suffix = "\r\n.\r\n"
-	}
-
 	for {
 	for {
-		client.setTimeout(server.timeout)
+		client.setTimeout(server.timeout.Load().(time.Duration))
 		reply, err = client.bufin.ReadString('\n')
 		reply, err = client.bufin.ReadString('\n')
 		input = input + reply
 		input = input + reply
-		if err == nil && client.state == ClientData {
-			if reply != "" {
-				// Extract the subject while we're at it
-				client.scanSubject(reply)
-			}
-			if int64(len(input)) > server.config.MaxSize {
-				return input, fmt.Errorf("Maximum DATA size exceeded (%d)", server.config.MaxSize)
-			}
-		}
 		if err != nil {
 		if err != nil {
 			break
 			break
 		}
 		}
 		if strings.HasSuffix(input, suffix) {
 		if strings.HasSuffix(input, suffix) {
+			// discard the suffix and stop reading
+			input = input[0 : len(input)-len(suffix)]
 			break
 			break
 		}
 		}
 	}
 	}
 	return input, err
 	return input, err
 }
 }
 
 
-// Writes a response to the client.
-func (server *server) writeResponse(client *client) error {
-	client.setTimeout(server.timeout)
-	size, err := client.bufout.WriteString(client.response)
-	if err != nil {
-		return err
-	}
-	err = client.bufout.Flush()
-	if err != nil {
-		return err
-	}
-	client.response = client.response[size:]
-	return nil
+// flushResponse a response to the client. Flushes the client.bufout buffer to the connection
+func (server *server) flushResponse(client *client) error {
+	client.setTimeout(server.timeout.Load().(time.Duration))
+	return client.bufout.Flush()
 }
 }
 
 
 func (server *server) isShuttingDown() bool {
 func (server *server) isShuttingDown() bool {
@@ -229,8 +282,9 @@ func (server *server) isShuttingDown() bool {
 
 
 // Handles an entire client SMTP exchange
 // Handles an entire client SMTP exchange
 func (server *server) handleClient(client *client) {
 func (server *server) handleClient(client *client) {
-	defer server.closeConn(client)
-	log.WithFields(map[string]interface{}{
+	defer client.closeConn()
+	sc := server.configStore.Load().(ServerConfig)
+	server.log.WithFields(map[string]interface{}{
 		"event":   "connect",
 		"event":   "connect",
 		"address": client.RemoteAddress,
 		"address": client.RemoteAddress,
 		"helo":    client.Helo,
 		"helo":    client.Helo,
@@ -239,26 +293,35 @@ func (server *server) handleClient(client *client) {
 
 
 	// Initial greeting
 	// Initial greeting
 	greeting := fmt.Sprintf("220 %s SMTP Guerrilla(%s) #%d (%d) %s gr:%d",
 	greeting := fmt.Sprintf("220 %s SMTP Guerrilla(%s) #%d (%d) %s gr:%d",
-		server.config.Hostname, Version, client.ID,
+		sc.Hostname, Version, client.ID,
 		server.clientPool.GetActiveClientsCount(), time.Now().Format(time.RFC3339), runtime.NumGoroutine())
 		server.clientPool.GetActiveClientsCount(), time.Now().Format(time.RFC3339), runtime.NumGoroutine())
 
 
-	helo := fmt.Sprintf("250 %s Hello", server.config.Hostname)
-	ehlo := fmt.Sprintf("250-%s Hello", server.config.Hostname)
+	helo := fmt.Sprintf("250 %s Hello", sc.Hostname)
+	// ehlo is a multi-line reply and need additional \r\n at the end
+	ehlo := fmt.Sprintf("250-%s Hello\r\n", sc.Hostname)
 
 
 	// Extended feature advertisements
 	// Extended feature advertisements
-	messageSize := fmt.Sprintf("250-SIZE %d\r\n", server.config.MaxSize)
+	messageSize := fmt.Sprintf("250-SIZE %d\r\n", sc.MaxSize)
 	pipelining := "250-PIPELINING\r\n"
 	pipelining := "250-PIPELINING\r\n"
 	advertiseTLS := "250-STARTTLS\r\n"
 	advertiseTLS := "250-STARTTLS\r\n"
+	advertiseEnhancedStatusCodes := "250-ENHANCEDSTATUSCODES\r\n"
+	// The last line doesn't need \r\n since string will be printed as a new line.
+	// Also, Last line has no dash -
 	help := "250 HELP"
 	help := "250 HELP"
 
 
-	if server.config.TLSAlwaysOn {
-		success := server.upgradeToTLS(client)
-		if !success {
+	if sc.TLSAlwaysOn {
+		tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
+		if !ok {
+			server.mainlog.Error("Failed to load *tls.Config")
+		} else if err := client.upgradeToTLS(tlsConfig); err == nil {
+			advertiseTLS = ""
+		} else {
+			server.log.WithError(err).Warnf("[%s] Failed TLS handshake", client.RemoteAddress)
+			// server requires TLS, but can't handshake
 			client.kill()
 			client.kill()
 		}
 		}
-		advertiseTLS = ""
 	}
 	}
-	if !server.config.StartTLSOn {
+	if !sc.StartTLSOn {
 		// STARTTLS turned off, don't advertise it
 		// STARTTLS turned off, don't advertise it
 		advertiseTLS = ""
 		advertiseTLS = ""
 	}
 	}
@@ -266,24 +329,24 @@ func (server *server) handleClient(client *client) {
 	for client.isAlive() {
 	for client.isAlive() {
 		switch client.state {
 		switch client.state {
 		case ClientGreeting:
 		case ClientGreeting:
-			client.responseAdd(greeting)
+			client.sendResponse(greeting)
 			client.state = ClientCmd
 			client.state = ClientCmd
 		case ClientCmd:
 		case ClientCmd:
 			client.bufin.setLimit(CommandLineMaxLength)
 			client.bufin.setLimit(CommandLineMaxLength)
-			input, err := server.read(client)
-			log.Debugf("Client sent: %s", input)
+			input, err := server.readCommand(client, sc.MaxSize)
+			server.log.Debugf("Client sent: %s", input)
 			if err == io.EOF {
 			if err == io.EOF {
-				log.WithError(err).Warnf("Client closed the connection: %s", client.RemoteAddress)
+				server.log.WithError(err).Warnf("Client closed the connection: %s", client.RemoteAddress)
 				return
 				return
 			} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
 			} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
-				log.WithError(err).Warnf("Timeout: %s", client.RemoteAddress)
+				server.log.WithError(err).Warnf("Timeout: %s", client.RemoteAddress)
 				return
 				return
 			} else if err == LineLimitExceeded {
 			} else if err == LineLimitExceeded {
-				client.responseAdd("500 Line too long.")
+				client.sendResponse(response.Canned.FailLineTooLong)
 				client.kill()
 				client.kill()
 				break
 				break
 			} else if err != nil {
 			} else if err != nil {
-				log.WithError(err).Warnf("Read error: %s", client.RemoteAddress)
+				server.log.WithError(err).Warnf("Read error: %s", client.RemoteAddress)
 				client.kill()
 				client.kill()
 				break
 				break
 			}
 			}
@@ -298,120 +361,139 @@ func (server *server) handleClient(client *client) {
 				cmdLen = CommandVerbMaxLength
 				cmdLen = CommandVerbMaxLength
 			}
 			}
 			cmd := strings.ToUpper(input[:cmdLen])
 			cmd := strings.ToUpper(input[:cmdLen])
-
 			switch {
 			switch {
 			case strings.Index(cmd, "HELO") == 0:
 			case strings.Index(cmd, "HELO") == 0:
 				client.Helo = strings.Trim(input[4:], " ")
 				client.Helo = strings.Trim(input[4:], " ")
 				client.resetTransaction()
 				client.resetTransaction()
-				client.responseAdd(helo)
+				client.sendResponse(helo)
 
 
 			case strings.Index(cmd, "EHLO") == 0:
 			case strings.Index(cmd, "EHLO") == 0:
 				client.Helo = strings.Trim(input[4:], " ")
 				client.Helo = strings.Trim(input[4:], " ")
 				client.resetTransaction()
 				client.resetTransaction()
-				client.responseAdd(ehlo + messageSize + pipelining + advertiseTLS + help)
+				client.sendResponse(ehlo,
+					messageSize,
+					pipelining,
+					advertiseTLS,
+					advertiseEnhancedStatusCodes,
+					help)
 
 
 			case strings.Index(cmd, "HELP") == 0:
 			case strings.Index(cmd, "HELP") == 0:
-				client.responseAdd("214 OK\r\n" + messageSize + pipelining + advertiseTLS + help)
+				quote := response.GetQuote()
+				client.sendResponse("214-OK\r\n" + quote)
 
 
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 				if client.isInTransaction() {
 				if client.isInTransaction() {
-					client.responseAdd("503 Error: nested MAIL command")
+					client.sendResponse(response.Canned.FailNestedMailCmd)
 					break
 					break
 				}
 				}
-				from, err := extractEmail(input[10:])
+				mail := input[10:]
+				from := envelope.EmailAddress{}
+
+				if !(strings.Index(mail, "<>") == 0) &&
+					!(strings.Index(mail, " <>") == 0) {
+					// Not Bounce, extract mail.
+					from, err = extractEmail(mail)
+				}
+
 				if err != nil {
 				if err != nil {
-					client.responseAdd(err.Error())
+					client.sendResponse(err)
 				} else {
 				} else {
 					client.MailFrom = from
 					client.MailFrom = from
-					client.responseAdd("250 OK")
+					client.sendResponse(response.Canned.SuccessMailCmd)
 				}
 				}
 
 
 			case strings.Index(cmd, "RCPT TO:") == 0:
 			case strings.Index(cmd, "RCPT TO:") == 0:
 				if len(client.RcptTo) > RFC2821LimitRecipients {
 				if len(client.RcptTo) > RFC2821LimitRecipients {
-					client.responseAdd("452 Too many recipients")
+					client.sendResponse(response.Canned.ErrorTooManyRecipients)
 					break
 					break
 				}
 				}
 				to, err := extractEmail(input[8:])
 				to, err := extractEmail(input[8:])
 				if err != nil {
 				if err != nil {
-					client.responseAdd(err.Error())
+					client.sendResponse(err.Error())
 				} else {
 				} else {
 					if !server.allowsHost(to.Host) {
 					if !server.allowsHost(to.Host) {
-						client.responseAdd("454 Error: Relay access denied: " + to.Host)
+						client.sendResponse(response.Canned.ErrorRelayDenied, to.Host)
 					} else {
 					} else {
-						client.RcptTo = append(client.RcptTo, *to)
-						client.responseAdd("250 OK")
+						client.RcptTo = append(client.RcptTo, to)
+						client.sendResponse(response.Canned.SuccessRcptCmd)
 					}
 					}
 				}
 				}
 
 
 			case strings.Index(cmd, "RSET") == 0:
 			case strings.Index(cmd, "RSET") == 0:
 				client.resetTransaction()
 				client.resetTransaction()
-				client.responseAdd("250 OK")
+				client.sendResponse(response.Canned.SuccessResetCmd)
 
 
 			case strings.Index(cmd, "VRFY") == 0:
 			case strings.Index(cmd, "VRFY") == 0:
-				client.responseAdd("252 Cannot verify user")
+				client.sendResponse(response.Canned.SuccessVerifyCmd)
 
 
 			case strings.Index(cmd, "NOOP") == 0:
 			case strings.Index(cmd, "NOOP") == 0:
-				client.responseAdd("250 OK")
+				client.sendResponse(response.Canned.SuccessNoopCmd)
 
 
 			case strings.Index(cmd, "QUIT") == 0:
 			case strings.Index(cmd, "QUIT") == 0:
-				client.responseAdd("221 Bye")
+				client.sendResponse(response.Canned.SuccessQuitCmd)
 				client.kill()
 				client.kill()
 
 
 			case strings.Index(cmd, "DATA") == 0:
 			case strings.Index(cmd, "DATA") == 0:
-				if client.MailFrom.isEmpty() {
-					client.responseAdd("503 Error: No sender")
+				if client.MailFrom.IsEmpty() {
+					client.sendResponse(response.Canned.FailNoSenderDataCmd)
 					break
 					break
 				}
 				}
 				if len(client.RcptTo) == 0 {
 				if len(client.RcptTo) == 0 {
-					client.responseAdd("503 Error: No recipients")
+					client.sendResponse(response.Canned.FailNoRecipientsDataCmd)
 					break
 					break
 				}
 				}
-				client.responseAdd("354 Enter message, ending with '.' on a line by itself")
+				client.sendResponse(response.Canned.SuccessDataCmd)
 				client.state = ClientData
 				client.state = ClientData
 
 
-			case server.config.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
-				client.responseAdd("220 Ready to start TLS")
+			case sc.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
+
+				client.sendResponse(response.Canned.SuccessStartTLSCmd)
 				client.state = ClientStartTLS
 				client.state = ClientStartTLS
 			default:
 			default:
-
-				client.responseAdd("500 Unrecognized command: " + cmd)
 				client.errors++
 				client.errors++
-				if client.errors > MaxUnrecognizedCommands {
-					client.responseAdd("554 Too many unrecognized commands")
+				if client.errors >= MaxUnrecognizedCommands {
+					client.sendResponse(response.Canned.FailMaxUnrecognizedCmd)
 					client.kill()
 					client.kill()
+				} else {
+					client.sendResponse(response.Canned.FailUnrecognizedCmd)
 				}
 				}
 			}
 			}
 
 
 		case ClientData:
 		case ClientData:
-			var err error
 
 
-			client.bufin.setLimit(server.config.MaxSize)
-			client.Data, err = server.read(client)
+			// intentionally placed the limit 1MB above so that reading does not return with an error
+			// if the client goes a little over. Anything above will err
+			client.bufin.setLimit(int64(sc.MaxSize) + 1024000) // This a hard limit.
+
+			n, err := client.Data.ReadFrom(client.smtpReader.DotReader())
+			if n > sc.MaxSize {
+				err = fmt.Errorf("Maximum DATA size exceeded (%d)", sc.MaxSize)
+			}
 			if err != nil {
 			if err != nil {
 				if err == LineLimitExceeded {
 				if err == LineLimitExceeded {
-					client.responseAdd("550 Error: " + LineLimitExceeded.Error())
+					client.sendResponse(response.Canned.FailReadLimitExceededDataCmd, LineLimitExceeded.Error())
 					client.kill()
 					client.kill()
 				} else if err == MessageSizeExceeded {
 				} else if err == MessageSizeExceeded {
-					client.responseAdd("550 Error: " + MessageSizeExceeded.Error())
+					client.sendResponse(response.Canned.FailMessageSizeExceeded, MessageSizeExceeded.Error())
 					client.kill()
 					client.kill()
 				} else {
 				} else {
+					client.sendResponse(response.Canned.FailReadErrorDataCmd, err.Error())
 					client.kill()
 					client.kill()
-					client.responseAdd("451 Error: " + err.Error())
 				}
 				}
-				log.WithError(err).Warn("Error reading data")
+				server.log.WithError(err).Warn("Error reading data")
 				break
 				break
 			}
 			}
 
 
 			res := server.backend.Process(client.Envelope)
 			res := server.backend.Process(client.Envelope)
 			if res.Code() < 300 {
 			if res.Code() < 300 {
 				client.messagesSent++
 				client.messagesSent++
-				log.WithFields(map[string]interface{}{
+				server.log.WithFields(map[string]interface{}{
 					"helo":          client.Helo,
 					"helo":          client.Helo,
 					"remoteAddress": client.RemoteAddress,
 					"remoteAddress": client.RemoteAddress,
 					"success":       true,
 					"success":       true,
 				}).Info("Received message")
 				}).Info("Received message")
 			}
 			}
-			client.responseAdd(res.String())
+			client.sendResponse(res.String())
 			client.state = ClientCmd
 			client.state = ClientCmd
 			if server.isShuttingDown() {
 			if server.isShuttingDown() {
 				client.state = ClientShutdown
 				client.state = ClientShutdown
@@ -419,25 +501,33 @@ func (server *server) handleClient(client *client) {
 			client.resetTransaction()
 			client.resetTransaction()
 
 
 		case ClientStartTLS:
 		case ClientStartTLS:
-			if !client.TLS && server.config.StartTLSOn {
-				if server.upgradeToTLS(client) {
+			if !client.TLS && sc.StartTLSOn {
+				tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
+				if !ok {
+					server.mainlog.Error("Failed to load *tls.Config")
+				} else if err := client.upgradeToTLS(tlsConfig); err == nil {
 					advertiseTLS = ""
 					advertiseTLS = ""
 					client.resetTransaction()
 					client.resetTransaction()
+				} else {
+					server.log.WithError(err).Warnf("[%s] Failed TLS handshake", client.RemoteAddress)
+					// Don't disconnect, let the client decide if it wants to continue
 				}
 				}
 			}
 			}
 			// change to command state
 			// change to command state
 			client.state = ClientCmd
 			client.state = ClientCmd
 		case ClientShutdown:
 		case ClientShutdown:
 			// shutdown state
 			// shutdown state
-			client.responseAdd("421 Server is shutting down. Please try again later. Sayonara!")
+			client.sendResponse(response.Canned.ErrorShutdown)
 			client.kill()
 			client.kill()
 		}
 		}
 
 
-		if len(client.response) > 0 {
-			log.Debugf("Writing response to client: \n%s", client.response)
-			err := server.writeResponse(client)
+		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 {
 			if err != nil {
-				log.WithError(err).Debug("Error writing response")
+				server.log.WithError(err).Debug("Error writing response")
 				return
 				return
 			}
 			}
 		}
 		}

+ 102 - 0
server_test.go

@@ -0,0 +1,102 @@
+package guerrilla
+
+import (
+	"testing"
+
+	"bufio"
+	"fmt"
+	"net/textproto"
+	"strings"
+	"sync"
+
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/mocks"
+)
+
+// 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",
+		Timeout:         5,
+		ListenInterface: "127.0.0.1:2529",
+		StartTLSOn:      true,
+		TLSAlwaysOn:     false,
+		MaxClients:      30, // not tested here
+		LogFile:         "./tests/testlog",
+	}
+	return sc
+}
+
+// getMockServerConn gets a new server using sc. Server will be using a mocked TCP connection
+// using the dummy backend
+// RCP TO command only allows test.com host
+func getMockServerConn(sc *ServerConfig, t *testing.T) (*mocks.Conn, *server) {
+	var logOpenError error
+	var mainlog log.Logger
+	mainlog, logOpenError = log.GetLogger(sc.LogFile)
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	backend, err := backends.New("dummy", backends.BackendConfig{"log_received_mails": true}, mainlog)
+	if err != nil {
+		t.Error("new dummy backend failed because:", err)
+	}
+	server, err := newServer(sc, backend, mainlog)
+	if err != nil {
+		//t.Error("new server failed because:", err)
+	} else {
+		server.setAllowedHosts([]string{"test.com"})
+	}
+	conn := mocks.NewConn()
+	return conn, server
+}
+
+func TestHandleClient(t *testing.T) {
+	var mainlog log.Logger
+	var logOpenError error
+	sc := getMockServerConfig()
+	mainlog, logOpenError = log.GetLogger(sc.LogFile)
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	conn, server := getMockServerConn(sc, t)
+	// call the serve.handleClient() func in a goroutine.
+	client := NewClient(conn.Server, 1, mainlog)
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		server.handleClient(client)
+		wg.Done()
+	}()
+	// Wait for the greeting from the server
+	r := textproto.NewReader(bufio.NewReader(conn.Client))
+	line, _ := r.ReadLine()
+	fmt.Println(line)
+	w := textproto.NewWriter(bufio.NewWriter(conn.Client))
+	w.PrintfLine("HELO test.test.com")
+	line, _ = r.ReadLine()
+	fmt.Println(line)
+	w.PrintfLine("QUIT")
+	line, _ = r.ReadLine()
+	fmt.Println("line is:", line)
+	expected := "221 2.0.0 Bye"
+	if strings.Index(line, expected) != 0 {
+		t.Error("expected", expected, "but got:", line)
+	}
+	wg.Wait() // wait for handleClient to exit
+}
+
+// TODO
+// - test github issue #44 and #42
+// - test other commands
+
+// also, could test
+// - test allowsHost() and allowsHost()
+// - test isInTransaction() (make sure it returns true after MAIL command, but false after HELO/EHLO/RSET/end of DATA
+// - test to make sure client envelope
+// - perhaps anything else that can be tested in server_test.go

+ 47 - 0
tests/client.go

@@ -0,0 +1,47 @@
+package test
+
+import (
+	"bufio"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"github.com/flashmob/go-guerrilla"
+	"net"
+	"time"
+)
+
+func Connect(serverConfig guerrilla.ServerConfig, deadline time.Duration) (net.Conn, *bufio.Reader, error) {
+	var bufin *bufio.Reader
+	var conn net.Conn
+	var err error
+	if serverConfig.TLSAlwaysOn {
+		// start tls automatically
+		conn, err = tls.Dial("tcp", serverConfig.ListenInterface, &tls.Config{
+			InsecureSkipVerify: true,
+			ServerName:         "127.0.0.1",
+		})
+	} else {
+		conn, err = net.Dial("tcp", serverConfig.ListenInterface)
+	}
+
+	if err != nil {
+		// handle error
+		//t.Error("Cannot dial server", config.Servers[0].ListenInterface)
+		return conn, bufin, errors.New("Cannot dial server: " + serverConfig.ListenInterface + "," + err.Error())
+	}
+	bufin = bufio.NewReader(conn)
+
+	// should be ample time to complete the test
+	conn.SetDeadline(time.Now().Add(time.Duration(time.Second * deadline)))
+	// read greeting, ignore it
+	_, err = bufin.ReadString('\n')
+	return conn, bufin, err
+}
+
+func Command(conn net.Conn, bufin *bufio.Reader, command string) (reply string, err error) {
+	_, err = fmt.Fprintln(conn, command+"\r")
+	if err == nil {
+		return bufin.ReadString('\n')
+	}
+	return "", err
+}

File diff suppressed because it is too large
+ 520 - 166
tests/guerrilla_test.go


+ 6 - 6
tests/generate_cert.go → tests/testcert/generate_cert.go

@@ -1,9 +1,8 @@
 // adopted from https://golang.org/src/crypto/tls/generate_cert.go?m=text
 // adopted from https://golang.org/src/crypto/tls/generate_cert.go?m=text
 
 
-// Generate a self-signed X.509 certificate for a TLS server. Outputs to
-// 'cert.pem' and 'key.pem' and will overwrite existing files.
+// Generate a self-signed X.509 certificate for a TLS server.
 
 
-package test
+package testcert
 
 
 import (
 import (
 	"crypto/ecdsa"
 	"crypto/ecdsa"
@@ -63,7 +62,7 @@ func pemBlockForKey(priv interface{}) *pem.Block {
 
 
 // validFrom - Creation date formatted as Jan 1 15:04:05 2011 or ""
 // validFrom - Creation date formatted as Jan 1 15:04:05 2011 or ""
 
 
-func generateCert(host string, validFrom string, validFor time.Duration, isCA bool, rsaBits int, ecdsaCurve string) {
+func GenerateCert(host string, validFrom string, validFor time.Duration, isCA bool, rsaBits int, ecdsaCurve string, dirPrefix string) {
 
 
 	if len(host) == 0 {
 	if len(host) == 0 {
 		log.Fatalf("Missing required --host parameter")
 		log.Fatalf("Missing required --host parameter")
@@ -141,19 +140,20 @@ func generateCert(host string, validFrom string, validFor time.Duration, isCA bo
 		log.Fatalf("Failed to create certificate: %s", err)
 		log.Fatalf("Failed to create certificate: %s", err)
 	}
 	}
 
 
-	certOut, err := os.Create("./" + host + ".cert.pem")
+	certOut, err := os.Create(dirPrefix + host + ".cert.pem")
 	if err != nil {
 	if err != nil {
 		log.Fatalf("failed to open cert.pem for writing: %s", err)
 		log.Fatalf("failed to open cert.pem for writing: %s", err)
 	}
 	}
 	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
 	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
 	certOut.Close()
 	certOut.Close()
 
 
-	keyOut, err := os.OpenFile("./"+host+".key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	keyOut, err := os.OpenFile(dirPrefix+host+".key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
 	if err != nil {
 	if err != nil {
 		log.Print("failed to open key.pem for writing:", err)
 		log.Print("failed to open key.pem for writing:", err)
 		return
 		return
 	}
 	}
 	pem.Encode(keyOut, pemBlockForKey(priv))
 	pem.Encode(keyOut, pemBlockForKey(priv))
+	keyOut.Sync()
 	keyOut.Close()
 	keyOut.Close()
 
 
 }
 }

+ 9 - 6
util.go

@@ -4,15 +4,18 @@ import (
 	"errors"
 	"errors"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
+
+	"github.com/flashmob/go-guerrilla/envelope"
+	"github.com/flashmob/go-guerrilla/response"
 )
 )
 
 
 var extractEmailRegex, _ = regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk!
 var extractEmailRegex, _ = regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk!
 
 
-func extractEmail(str string) (*EmailAddress, error) {
-	email := &EmailAddress{}
+func extractEmail(str string) (envelope.EmailAddress, error) {
+	email := envelope.EmailAddress{}
 	var err error
 	var err error
 	if len(str) > RFC2821LimitPath {
 	if len(str) > RFC2821LimitPath {
-		return email, errors.New("501 Path too long")
+		return email, errors.New(response.Canned.FailPathTooLong)
 	}
 	}
 	if matched := extractEmailRegex.FindStringSubmatch(str); len(matched) > 2 {
 	if matched := extractEmailRegex.FindStringSubmatch(str); len(matched) > 2 {
 		email.User = matched[1]
 		email.User = matched[1]
@@ -23,11 +26,11 @@ func extractEmail(str string) (*EmailAddress, error) {
 	}
 	}
 	err = nil
 	err = nil
 	if email.User == "" || email.Host == "" {
 	if email.User == "" || email.Host == "" {
-		err = errors.New("501 Invalid address")
+		err = errors.New(response.Canned.FailInvalidAddress)
 	} else if len(email.User) > RFC2832LimitLocalPart {
 	} else if len(email.User) > RFC2832LimitLocalPart {
-		err = errors.New("501 Local part too long, cannot exceed 64 characters")
+		err = errors.New(response.Canned.FailLocalPartTooLong)
 	} else if len(email.Host) > RFC2821LimitDomain {
 	} else if len(email.Host) > RFC2821LimitDomain {
-		err = errors.New("501 Domain cannot exceed 255 characters")
+		err = errors.New(response.Canned.FailDomainTooLong)
 	}
 	}
 	return email, err
 	return email, err
 }
 }

Some files were not shown because too many files changed in this diff