소스 검색

Hot config reload, part 2 + refactor backends structure (#33)

Fixes #18 

* Fixed a bug where pidFile from config was ignored and replaced with the default
* removed unused 'verbose' variable
* guerrilla struct now holds servers as a map instead of slice
* API change: guerrilla.New() now returns error
* Refactor TLS config into a separate function
* server's serverConfig is now kept in an atomic value, ready for hot config reload
* Slightly raised the hard limit on DATA state, to avoid potential errors
* Refactored allowedHosts to use a map for lookup instead of doing O(n)
*  Discard ending .\r\n - fixes #24
* Split the Envelope struct into its own package, as both backend and guerrilla depend on it
* update tests after merging latest tests from master
* Removed case statement in serve.go to make backends dynamically loaded. Structural changes so that that backends support worker pools & can re-use code.
* Future groundwork for decorator pattern
* pass saveMailChan via function instead of embedding (simplify)
going to save decorator pattern for another day, needs more thinking
* added event bus package to use for hot config reload
* emit config change events on config file changes when SIGHUP gets caught
* config tests, including events and sample config
* add pid_file to config sample
* add tests for backend config changes
* plug in test to travis
* generate cert before test
* tests for serve.go config reload
* add tests to Makefile
* pass server config as pointer
* events for adding, starting, stopping and removing a server
* fix server would hang on Accept error
* New Errors type for guerrilla package to hold multiple errors
* Start() and New() return the new Errors type
* Behavior change of serve: does not exit if servers could not be started
* Tests for server_change:start_server fired when server was enabled after reloading config - TestServerStartEvent
* refactoring to client to prevent a race condition on setTimeout function
bug found during this build https://travis-ci.org/flashmob/go-guerrilla/jobs/191348662
* Server stop event now working, added test TestServerStopEvent
* Backend proxy: added states & shutdown is protected by mutex
* Behaviour change: sigHandler will return rather than exit on kill / sigterm signals
* Backend errors: revert to return error as error rather than custom type...
* fix TLS handshake error formatting
*  "allowed hosts" config setting moved back to root level. Servers still keep their own table in a map, which may be customized in the future.
* "allowed hosts" now supports config reload event, will reload for all servers when SIGHUP is caught.
* test for allowed_hosts config reload
* TLS certificate config reload
* config reload for server's Timeout setting + test
* leave max_clients for another issue since it requires pool resizing
* embedded the event bus in the guerrilla struct and be as part of the Guerrilla interface. This should make the event bus easy to swap out should it be changed to a different implementation. Users of the API can also have easier access to the server events
* update readme
Guerrilla Mail 8 년 전
부모
커밋
2c302900b9
24개의 변경된 파일2682개의 추가작업 그리고 511개의 파일을 삭제
  1. 3 1
      .travis.yml
  2. 3 1
      Makefile
  3. 18 7
      README.md
  4. 155 0
      backends/abstract.go
  5. 189 0
      backends/backend.go
  6. 27 27
      backends/dummy.go
  7. 24 81
      backends/guerrilla_db_redis.go
  8. 42 27
      client.go
  9. 109 52
      cmd/guerrillad/serve.go
  10. 1034 0
      cmd/guerrillad/serve_test.go
  11. 222 11
      config.go
  12. 284 0
      config_test.go
  13. 32 0
      envelope/envelope.go
  14. 4 2
      glide.lock
  15. 2 0
      glide.yaml
  16. 1 1
      goguerrilla.conf.sample
  17. 259 27
      guerrilla.go
  18. 0 60
      models.go
  19. 30 18
      pool.go
  20. 135 83
      server.go
  21. 47 0
      tests/client.go
  22. 54 105
      tests/guerrilla_test.go
  23. 5 6
      tests/testcert/generate_cert.go
  24. 3 2
      util.go

+ 3 - 1
.travis.yml

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

+ 3 - 1
Makefile

@@ -25,4 +25,6 @@ guerrillad: *.go */*.go */*/*.go
 	$(GO_VARS) $(GO) build -o="guerrillad" -ldflags="$(LD_FLAGS)" $(ROOT)/cmd/guerrillad
 
 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

+ 18 - 7
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.
 Show the top source clients by: IP, by domain & by HELO message.
 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)
 
-- 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 
 (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
 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 
-github.
+github. (Flame graph maybe? https://github.com/uber/go-torch 
+Please open an issue before to discuss scope)
 (0.25 BTC)
 
 - Looking for someone to do a code review & possibly fix any tidbits,
@@ -178,13 +187,13 @@ config := &guerrilla.AppConfig{
   AllowedHosts: []string{...}
 }
 backend := &CustomBackend{...}
-app := guerrilla.New(config, backend)
+app, err := guerrilla.New(config, backend)
 ```
 
 ## Start the app.
 `Start` is non-blocking, so make sure the main goroutine is kept busy
 ```go
-app.Start() (startErrors []error)
+startErrors := app.Start()
 ```
 
 ## Shutting down.
@@ -276,6 +285,8 @@ Large refactoring of the code.
 - 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: 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)

+ 155 - 0
backends/abstract.go

@@ -0,0 +1,155 @@
+package backends
+
+import (
+	log "github.com/Sirupsen/logrus"
+
+	"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)
+	}
+	if b.config.LogReceivedMails {
+		log.Infof("Mail from: %s / to: %v", mail.MailFrom.String(), mail.RcptTo)
+	}
+	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 {
+			log.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
+}

+ 189 - 0
backends/backend.go

@@ -0,0 +1,189 @@
+package backends
+
+import (
+	"errors"
+	"fmt"
+	log "github.com/Sirupsen/logrus"
+	"github.com/flashmob/go-guerrilla/envelope"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+)
+
+// 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 configLoader interface {
+	loadConfig(backendConfig BackendConfig) (err 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
+}
+type helper struct {
+	saveMailChan chan *savePayload
+	wg           sync.WaitGroup
+}
+
+// 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      int
+}
+
+// possible values for state
+const (
+	BackendStateProcessing = iota
+	BackendStateShutdown
+)
+
+// New retrieve a backend specified by the backendName, and initialize it using
+// backendConfig
+func New(backendName string, backendConfig BackendConfig) (Backend, error) {
+	backend, found := backends[backendName]
+	if !found {
+		return nil, fmt.Errorf("backend %q not found", backendName)
+	}
+	p := &BackendGateway{b: backend}
+	err := p.Initialize(backendConfig)
+	if err != nil {
+		return nil, fmt.Errorf("error while initializing the backend: %s", err)
+	}
+	p.state = BackendStateProcessing
+	return p, nil
+}
+
+// Distributes an envelope to one of the backend workers
+func (gw *BackendGateway) Process(e *envelope.Envelope) BackendResult {
+	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("554 Error: " + status.err.Error())
+		}
+		return NewBackendResult(fmt.Sprintf("250 OK : queued as %s", status.hash))
+	case <-time.After(time.Second * 30):
+		log.Infof("Backend has timed out")
+		return NewBackendResult("554 Error: transaction timeout")
+	}
+}
+func (gw *BackendGateway) Shutdown() error {
+	gw.stateGuard.Lock()
+	defer gw.stateGuard.Unlock()
+	if gw.state != BackendStateShutdown {
+		err := gw.b.Shutdown()
+		if err == nil {
+			close(gw.saveMailChan) // workers will stop
+			gw.wg.Wait()
+			gw.state = BackendStateShutdown
+		}
+		return err
+	}
+	return nil
+}
+
+func (gw *BackendGateway) Initialize(cfg BackendConfig) error {
+	err := gw.b.Initialize(cfg)
+	if err == nil {
+		workersSize := gw.b.getNumberOfWorkers()
+		if workersSize < 1 {
+			return errors.New("Must have at least 1 worker")
+		}
+		if err := gw.b.testSettings(); err != nil {
+			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()
+			}()
+		}
+	}
+	return err
+}

+ 27 - 27
backends/dummy.go

@@ -1,39 +1,39 @@
 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 {
 	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
+	// helps with worker management
+	helper
 }
 
-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
 }

+ 24 - 81
backends/guerrilla_db_redis.go

@@ -1,24 +1,27 @@
 package backends
 
 import (
-	"encoding/json"
 	"errors"
 	"fmt"
-	"sync"
+
 	"time"
 
 	log "github.com/Sirupsen/logrus"
-	"github.com/flashmob/go-guerrilla"
 	"github.com/garyburd/redigo/redis"
 
+	"github.com/flashmob/go-guerrilla/envelope"
 	"github.com/ziutek/mymysql/autorc"
 	_ "github.com/ziutek/mymysql/godrv"
 )
 
+func init() {
+	backends["guerrilla-db-redis"] = &AbstractBackend{
+		extend: &GuerrillaDBAndRedisBackend{}}
+}
+
 type GuerrillaDBAndRedisBackend struct {
-	config       guerrillaDBAndRedisConfig
-	saveMailChan chan *savePayload
-	wg           sync.WaitGroup
+	AbstractBackend
+	config guerrillaDBAndRedisConfig
 }
 
 type guerrillaDBAndRedisConfig struct {
@@ -40,83 +43,28 @@ func convertError(name string) 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 guerrillaDBAndRedisConfig struct
-func (g *GuerrillaDBAndRedisBackend) loadConfig(backendConfig map[string]interface{}) error {
-	data, err := json.Marshal(backendConfig)
-	if err != nil {
-		return err
-	}
-
-	err = json.Unmarshal(data, &g.config)
-	if g.config.NumberOfWorkers < 1 {
-		return errors.New("Must have more than 1 worker")
-	}
-
-	return err
-}
-
-func (g *GuerrillaDBAndRedisBackend) Initialize(backendConfig map[string]interface{}) error {
-	err := g.loadConfig(backendConfig)
+func (g *GuerrillaDBAndRedisBackend) loadConfig(backendConfig BackendConfig) (err error) {
+	configType := baseConfig(&guerrillaDBAndRedisConfig{})
+	bcfg, err := g.extractConfig(backendConfig, configType)
 	if err != nil {
 		return err
 	}
-
-	if err := g.testDbConnections(); err != nil {
-		return err
-	}
-
-	g.saveMailChan = make(chan *savePayload, g.config.NumberOfWorkers)
-
-	// start some savemail workers
-	g.wg.Add(g.config.NumberOfWorkers)
-	for i := 0; i < g.config.NumberOfWorkers; i++ {
-		go g.saveMail()
-	}
-
+	m := bcfg.(*guerrillaDBAndRedisConfig)
+	g.config = *m
 	return nil
 }
 
-func (g *GuerrillaDBAndRedisBackend) Shutdown() error {
-	close(g.saveMailChan) // workers will stop
-	g.wg.Wait()
-	return nil
+func (g *GuerrillaDBAndRedisBackend) getNumberOfWorkers() int {
+	return g.config.NumberOfWorkers
 }
 
-func (g *GuerrillaDBAndRedisBackend) Process(mail *guerrilla.Envelope) guerrilla.BackendResult {
+func (g *GuerrillaDBAndRedisBackend) Process(mail *envelope.Envelope) BackendResult {
 	to := mail.RcptTo
-	from := mail.MailFrom
+	log.Info("(g *GuerrillaDBAndRedisBackend) Process called")
 	if len(to) == 0 {
-		return guerrilla.NewBackendResult("554 Error: no recipient")
-	}
-
-	// 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())
-		}
-		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")
+		return NewBackendResult("554 Error: no recipient")
 	}
-}
-
-type savePayload struct {
-	mail        *guerrilla.Envelope
-	from        *guerrilla.EmailAddress
-	recipient   *guerrilla.EmailAddress
-	savedNotify chan *saveStatus
-}
-
-type saveStatus struct {
-	err  error
-	hash string
+	return nil
 }
 
 type redisClient struct {
@@ -125,13 +73,12 @@ type redisClient struct {
 	time        int
 }
 
-func (g *GuerrillaDBAndRedisBackend) saveMail() {
+func (g *GuerrillaDBAndRedisBackend) saveMailWorker(saveMailChan chan *savePayload) {
 	var to, body string
 	var err error
 
 	var redisErr error
 	var length int
-
 	redisClient := &redisClient{}
 	db := autorc.New(
 		"tcp",
@@ -155,7 +102,7 @@ func (g *GuerrillaDBAndRedisBackend) saveMail() {
 	}
 	defer func() {
 		if r := recover(); r != nil {
-			// recover form closed channel
+			//recover form closed channel
 			fmt.Println("Recovered in f", r)
 		}
 		if db.Raw != nil {
@@ -165,13 +112,11 @@ func (g *GuerrillaDBAndRedisBackend) saveMail() {
 			log.Infof("closed redis")
 			redisClient.conn.Close()
 		}
-
-		g.wg.Done()
 	}()
 
 	//  receives values from the channel repeatedly until it is closed.
 	for {
-		payload := <-g.saveMailChan
+		payload := <-saveMailChan
 		if payload == nil {
 			log.Debug("No more saveMailChan payload")
 			return
@@ -235,7 +180,6 @@ func (g *GuerrillaDBAndRedisBackend) saveMail() {
 }
 
 func (c *redisClient) redisConnection(redisInterface string) (err error) {
-
 	if c.isConnected == false {
 		c.conn, err = redis.Dial("tcp", redisInterface)
 		if err != nil {
@@ -244,12 +188,11 @@ func (c *redisClient) redisConnection(redisInterface string) (err error) {
 		}
 		c.isConnected = true
 	}
-
 	return nil
 }
 
 // test database connection settings
-func (g *GuerrillaDBAndRedisBackend) testDbConnections() (err error) {
+func (g *GuerrillaDBAndRedisBackend) testSettings() (err error) {
 	db := autorc.New(
 		"tcp",
 		"",

+ 42 - 27
client.go

@@ -2,6 +2,9 @@ package guerrilla
 
 import (
 	"bufio"
+	"crypto/tls"
+	log "github.com/Sirupsen/logrus"
+	"github.com/flashmob/go-guerrilla/envelope"
 	"net"
 	"strings"
 	"sync"
@@ -25,7 +28,7 @@ const (
 )
 
 type client struct {
-	*Envelope
+	*envelope.Envelope
 	ID          uint64
 	ConnectedAt time.Time
 	KilledAt    time.Time
@@ -34,32 +37,18 @@ type client struct {
 	state        ClientState
 	messagesSent int
 	// Response to be written to the client
-	response  string
-	conn      net.Conn
-	bufin     *smtpBufferedReader
-	bufout    *bufio.Writer
-	timeoutMu sync.Mutex
-}
-
-// 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
+	response string
+	conn     net.Conn
+	bufin    *smtpBufferedReader
+	bufout   *bufio.Writer
+	// guards access to conn
+	connGuard sync.Mutex
 }
 
 func NewClient(conn net.Conn, clientID uint64) *client {
 	return &client{
 		conn: conn,
-		Envelope: &Envelope{
+		Envelope: &envelope.Envelope{
 			RemoteAddress: conn.RemoteAddr().String(),
 		},
 		ConnectedAt: time.Now(),
@@ -74,14 +63,14 @@ func (c *client) responseAdd(r string) {
 }
 
 func (c *client) resetTransaction() {
-	c.MailFrom = &EmailAddress{}
-	c.RcptTo = []EmailAddress{}
+	c.MailFrom = &envelope.EmailAddress{}
+	c.RcptTo = []envelope.EmailAddress{}
 	c.Data = ""
 	c.Subject = ""
 }
 
 func (c *client) isInTransaction() bool {
-	isMailFromEmpty := (c.MailFrom == nil || *c.MailFrom == (EmailAddress{}))
+	isMailFromEmpty := (c.MailFrom == nil || *c.MailFrom == (envelope.EmailAddress{}))
 	if isMailFromEmpty {
 		return false
 	}
@@ -113,13 +102,21 @@ func (c *client) scanSubject(reply string) {
 	}
 }
 
+// setTimeout adjust the timeout on the connection, goroutine safe
 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 {
 		c.conn.SetDeadline(time.Now().Add(t * time.Second))
 	}
+}
 
+// Closes a client connection, , goroutine safe
+func (c *client) closeConn() {
+	defer c.connGuard.Unlock()
+	c.connGuard.Lock()
+	c.conn.Close()
+	c.conn = nil
 }
 
 func (c *client) init(conn net.Conn, clientID uint64) {
@@ -141,3 +138,21 @@ func (c *client) init(conn net.Conn, clientID uint64) {
 func (c *client) getID() uint64 {
 	return c.ID
 }
+
+// Upgrades a client connection to TLS
+func (client *client) upgradeToTLS(tlsConfig *tls.Config) bool {
+	var tlsConn *tls.Conn
+	// load the config thread-safely
+	tlsConn = tls.Server(client.conn, tlsConfig)
+	err := tlsConn.Handshake()
+	if err != nil {
+		log.WithError(err).Warnf("[%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
+}

+ 109 - 52
cmd/guerrillad/serve.go

@@ -4,6 +4,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	log "github.com/Sirupsen/logrus"
+	"github.com/spf13/cobra"
 	"io/ioutil"
 	"os"
 	"os/exec"
@@ -13,15 +15,16 @@ import (
 	"syscall"
 	"time"
 
-	log "github.com/Sirupsen/logrus"
-	"github.com/spf13/cobra"
-
 	"github.com/flashmob/go-guerrilla"
 	"github.com/flashmob/go-guerrilla/backends"
+	"reflect"
+)
+
+const (
+	defaultPidFile = "/var/run/go-guerrilla.pid"
 )
 
 var (
-	iface      string
 	configPath string
 	pidFile    string
 
@@ -38,8 +41,9 @@ var (
 func init() {
 	serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
 		"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)
 }
@@ -49,30 +53,52 @@ func sigHandler(app guerrilla.Guerrilla) {
 	signal.Notify(signalChannel, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGKILL)
 
 	for sig := range signalChannel {
-
 		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 {
 				log.WithError(err).Error("Error while ReadConfig (reload)")
 			} else {
-				log.Infof("Configuration is reloaded at %s", guerrilla.ConfigLoadTime)
+				cmdConfig = newConfig
+				log.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 {
 			log.Infof("Shutdown signal caught")
 			app.Shutdown()
 			log.Infof("Shutdown completd, exiting.")
-			os.Exit(0)
+			return
 		} else {
-			os.Exit(0)
+			log.Infof("Shutdown, unknown signal caught")
+			return
 		}
 	}
 }
 
+func subscribeBackendEvent(event string, backend backends.Backend, app guerrilla.Guerrilla) {
+
+	app.Subscribe(event, func(cmdConfig *CmdConfig) {
+		var err error
+		if err = backend.Shutdown(); err != nil {
+			log.WithError(err).Warn("Backend failed to shutdown")
+			return
+		}
+		backend, err = backends.New(cmdConfig.BackendName, cmdConfig.BackendConfig)
+		if err != nil {
+			log.WithError(err).Fatalf("Error while loading the backend %q",
+				cmdConfig.BackendName)
+		} else {
+			log.Info("Backend started:", cmdConfig.BackendName)
+		}
+	})
+}
+
 func serve(cmd *cobra.Command, args []string) {
 	logVersion()
 
-	err := readConfig(configPath, verbose, &cmdConfig)
+	err := readConfig(configPath, pidFile, &cmdConfig)
 	if err != nil {
 		log.WithError(err).Fatal("Error while reading config")
 	}
@@ -91,40 +117,32 @@ func serve(cmd *cobra.Command, args []string) {
 		}
 	}
 
-	// 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)
+	if err != nil {
+		log.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)
+	if err != nil {
+		log.WithError(err).Error("Error(s) when creating new server(s)")
 	}
-
-	app := guerrilla.New(&cmdConfig.AppConfig, &backend)
-	go app.Start()
+	err = app.Start()
+	if err != nil {
+		log.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)
+	})
 	sigHandler(app)
+
 }
 
 // Superset of `guerrilla.AppConfig` containing options specific
@@ -132,26 +150,50 @@ func serve(cmd *cobra.Command, args []string) {
 type CmdConfig struct {
 	guerrilla.AppConfig
 	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
-func readConfig(path string, verbose bool, config *CmdConfig) error {
+func readConfig(path string, pidFile string, config *CmdConfig) error {
 	// load in the config.
 	data, err := ioutil.ReadFile(path)
 	if err != nil {
 		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 {
 		return errors.New("Empty `allowed_hosts` is not allowed")
 	}
-
 	guerrilla.ConfigLoadTime = time.Now()
 	return nil
 }
@@ -162,11 +204,26 @@ func getFileLimit() int {
 	if err != nil {
 		return -1
 	}
-
 	limit, err := strconv.Atoi(strings.TrimSpace(string(out)))
 	if err != nil {
 		return -1
 	}
-
 	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()
+				log.Infof("pid_file (%s) written with pid:%v", pidFile, pid)
+			} else {
+				log.WithError(err).Fatalf("Error while writing pidFile (%s)", pidFile)
+			}
+		} else {
+			log.WithError(err).Fatalf("Error while creating pidFile (%s)", pidFile)
+		}
+	}
+}

+ 1034 - 0
cmd/guerrillad/serve_test.go

@@ -0,0 +1,1034 @@
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	log "github.com/Sirupsen/logrus"
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/backends"
+	test "github.com/flashmob/go-guerrilla/tests"
+	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"github.com/spf13/cobra"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+)
+
+var configJsonA = `
+{
+    "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
+        },
+        {
+            "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
+        }
+    ]
+}
+`
+
+// backend config changed, log_received_mails is false
+var configJsonB = `
+{
+"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
+        }
+    ]
+}
+`
+
+// backend_name changed, is guerrilla-redis-db + added a server
+var configJsonC = `
+{
+"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
+        },
+        {
+            "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
+        }
+    ]
+}
+`
+
+// adds 127.0.0.1:4655, a secure server
+var configJsonD = `
+{
+"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
+        },
+        {
+            "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
+        }
+    ]
+}
+`
+
+// reload config
+func sigHup() {
+	if data, err := ioutil.ReadFile("pidfile.pid"); err == nil {
+		log.Infof("pid read is %s", data)
+		ecmd := exec.Command("kill", "-HUP", string(data))
+		_, err = ecmd.Output()
+		if err != nil {
+			log.Infof("could not SIGHUP", err)
+		}
+	} else {
+		log.WithError(err).Info("sighup - Could not read pidfle")
+	}
+
+}
+
+// shutdown after calling serve()
+func sigKill() {
+	if data, err := ioutil.ReadFile("pidfile.pid"); err == nil {
+		log.Infof("pid read is %s", data)
+		ecmd := exec.Command("kill", string(data))
+		_, err = ecmd.Output()
+		if err != nil {
+			log.Infof("could not sigkill", err)
+		}
+	} else {
+		log.WithError(err).Info("sigKill - Could not read pidfle")
+	}
+
+}
+
+// make sure that we get all the config change events
+func TestCmdConfigChangeEvents(t *testing.T) {
+
+	// hold the output of logs
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	//log.SetOutput(os.Stdout)
+	log.SetOutput(logOut)
+
+	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,
+	}
+	bcfg := backends.BackendConfig{"log_received_mails": true}
+	backend, err := backends.New("dummy", bcfg)
+	app, err := guerrilla.New(&oldconf.AppConfig, backend)
+	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
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+}
+
+// start server, chnage config, send SIG HUP, confirm that the pidfile changed & backend reloaded
+func TestServe(t *testing.T) {
+	// hold the output of logs
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	//log.SetOutput(os.Stdout)
+	log.SetOutput(logOut)
+
+	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(time.Second)
+
+	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
+	ecmd := exec.Command("kill", "-HUP", string(data))
+	_, err = ecmd.Output()
+	if err != nil {
+		t.Error("could not SIGHUP", err)
+		t.FailNow()
+	}
+	time.Sleep(time.Second) // 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()
+
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "Backend started:dummy"); i < 0 {
+			t.Error("Dummy backend not restared")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	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) {
+	// hold the output of logs
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	log.SetOutput(logOut)
+	// 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(time.Second)
+	// 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(time.Second * 1) // 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()
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); 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")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	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) {
+	// hold the output of logs
+
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	log.SetOutput(logOut)
+	// 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(time.Second)
+	// 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(time.Second * 1) // 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()
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); 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")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	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) {
+	// hold the output of logs
+	return
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	log.SetOutput(logOut)
+	// 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(time.Second)
+	// 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(time.Second * 1) // 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(time.Second * 1) // 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()
+
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); err == nil {
+		logOutput := string(read)
+		//fmt.Println(logOutput)
+		if i := strings.Index(logOutput, "Server [127.0.0.1:2228] has stopped"); i < 0 {
+			t.Error("did not stop [127.0.0.1:2228], most likely because Bus.Subscribe(\"server_change:stop_server\" didnt fire")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	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) {
+	// hold the output of logs
+
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	log.SetOutput(logOut)
+	// 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(time.Second)
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(time.Second)
+
+	// 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 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(time.Second) // 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 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()
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); 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")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	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) {
+	// hold the output of logs
+
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	log.SetOutput(logOut)
+	// 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(time.Second)
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(time.Second)
+
+	// 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 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
+								log.Info("TLS Handshake succeeded")
+							}
+
+						}
+					}
+				}
+			}
+			conn.Close()
+		}
+	}
+	testTlsHandshake()
+
+	// generate a new cert
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	sigHup()
+
+	time.Sleep(time.Second) // pause for config to reload
+	testTlsHandshake()
+
+	time.Sleep(time.Second)
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); 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")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	os.Remove("configJsonD.json")
+	os.Remove("./pidfile.pid")
+
+}
+
+// Test for missing TLS certificate, when starting or config reload
+
+func TestBadTLS(t *testing.T) {
+	// hold the output of logs
+
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+
+	//testcert.GenerateCert("mail2.guerrillamail.com", "", 365 * 24 * time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	//log.SetLevel(log.DebugLevel) // it will trash std out of debug
+	log.SetLevel(log.InfoLevel)
+	log.SetOutput(logOut)
+	//log.SetOutput(os.Stdout)
+	if err := os.Remove("./../../tests/mail2.guerrillamail.com.cert.pem"); err != nil {
+		log.WithError(err).Error("could not remove ./../../tests/mail2.guerrillamail.com.cert.pem")
+	} else {
+		log.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
+	cmd := &cobra.Command{}
+	configPath = "configJsonD.json"
+	var serveWG sync.WaitGroup
+	time.Sleep(time.Second)
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(time.Second)
+
+	// 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 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 {
+								log.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(time.Second) // pause for config to reload
+	testTlsHandshake()
+
+	time.Sleep(time.Second)
+	// send kill signal and wait for exit
+	sigKill()
+	serveWG.Wait()
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); 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")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	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) {
+	// hold the output of logs
+
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+
+	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	log.SetOutput(logOut)
+
+	// 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(time.Second)
+	serveWG.Add(1)
+	go func() {
+		serve(cmd, []string{})
+		serveWG.Done()
+	}()
+	time.Sleep(time.Second)
+
+	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()
+	logOut.Flush()
+	// did backend started as expected?
+	if read, err := ioutil.ReadAll(logIn); 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")
+		}
+	}
+	// don't forget to reset
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+
+	// cleanup
+	os.Remove("configJsonD.json")
+	os.Remove("./pidfile.pid")
+
+}

+ 222 - 11
config.go

@@ -1,22 +1,233 @@
 package guerrilla
 
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"reflect"
+	"strings"
+)
+
 // AppConfig is the holder of the configuration of the app
 type AppConfig struct {
 	Servers      []ServerConfig `json:"servers"`
 	AllowedHosts []string       `json:"allowed_hosts"`
+	PidFile      string         `json:"pid_file"`
 }
 
 // ServerConfig specifies config options for a single server
 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)
+	}
+	// 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:"+sc.ListenInterface+":new_log_file", sc)
+	} else {
+		// since config file has not changed, we reload it
+		app.Publish("server_change:"+sc.ListenInterface+":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
 }

+ 284 - 0
config_test.go

@@ -0,0 +1,284 @@
+package guerrilla
+
+import (
+	"bufio"
+	"bytes"
+	log "github.com/Sirupsen/logrus"
+	"github.com/flashmob/go-guerrilla/backends"
+	"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 = `
+{
+    "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,
+            "log_file":"/dev/stdout"
+        },
+
+        {
+            "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,
+            "log_file":"/dev/stdout"
+        },
+
+        {
+            "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,
+            "log_file":"/dev/stdout"
+        },
+
+        {
+            "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,
+            "log_file":"/dev/stdout"
+        }
+
+
+    ]
+}
+`
+
+// 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 = `
+{
+    "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,
+            "log_file":"/var/log/smtpd.log"
+        },
+        {
+            "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,
+            "log_file":"/dev/stdout"
+        },
+
+        {
+            "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,
+            "log_file":"/dev/stdout"
+        },
+
+        {
+            "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,
+            "log_file":"/dev/stdout"
+        }
+    ]
+}
+`
+
+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) {
+
+	// hold the output of logs
+	var logBuffer bytes.Buffer
+	// logs redirected to this writer
+	var logOut *bufio.Writer
+	// read the logs
+	var logIn *bufio.Reader
+	logOut = bufio.NewWriter(&logBuffer)
+	logIn = bufio.NewReader(&logBuffer)
+	log.SetLevel(log.DebugLevel)
+	//log.SetOutput(os.Stdout)
+	log.SetOutput(logOut)
+
+	oldconf := &AppConfig{}
+	oldconf.Load([]byte(configJsonA))
+	bcfg := backends.BackendConfig{"log_received_mails": true}
+	backend, _ := backends.New("dummy", bcfg)
+	app, _ := New(oldconf, backend)
+	// 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))
+	expectedEvents := map[string]bool{
+		"config_change:pid_file":                       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:127.0.0.1:2526:new_log_file":    false,
+		"server_change:127.0.0.1:2527:reopen_log_file": false,
+		"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
+	logBuffer.Reset()
+	logIn.Reset(&logBuffer)
+}

+ 32 - 0
envelope/envelope.go

@@ -0,0 +1,32 @@
+package envelope
+
+import "fmt"
+
+// 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    string
+	Subject string
+	TLS     bool
+}

+ 4 - 2
glide.lock

@@ -1,6 +1,8 @@
-hash: d96dd7a4b78faacec2eb40ef44e4e6598abf0be703021cae6dacd8080b6f4f54
-updated: 2016-12-16T15:58:00.591876692-08:00
+hash: 9d5e16555ee2b6363588a60d63b9ba42952cf8a77907b4629aca810f774e3f7b
+updated: 2017-01-06T01:17:48.317018392+11:00
 imports:
+- name: github.com/asaskevich/EventBus
+  version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2
 - name: github.com/garyburd/redigo
   version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
   subpackages:

+ 2 - 0
glide.yaml

@@ -15,3 +15,5 @@ import:
   - godrv
 - package: gopkg.in/iconv.v1
   version: ~1.1.1
+- package: github.com/asaskevich/EventBus
+  version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2

+ 1 - 1
goguerrilla.conf.sample

@@ -6,7 +6,7 @@
       "guerrillamail.net",
       "guerrillamail.org"
     ],
-    "primary_mail_host": "sharklasers.com",
+    "pid_file" : "/var/run/go-guerrilla.pid",
     "backend_name": "dummy",
     "backend_config": {
         "log_received_mails": true

+ 259 - 27
guerrilla.go

@@ -3,59 +3,265 @@ package guerrilla
 import (
 	"errors"
 	log "github.com/Sirupsen/logrus"
+	evbus "github.com/asaskevich/EventBus"
+	"github.com/flashmob/go-guerrilla/backends"
 	"sync"
 )
 
+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 {
-	Start() (startErrors []error)
+	Start() error
 	Shutdown()
+	Subscribe(topic string, fn interface{}) error
+	Publish(topic string, args ...interface{})
+	Unsubscribe(topic string, handler interface{}) error
 }
 
 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
 }
 
 // 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) (Guerrilla, error) {
+	g := &guerrilla{
+		Config:  *ac, // take a local copy
+		servers: make(map[string]*server, len(ac.Servers)),
+		backend: b,
+		bus:     evbus.New(),
+	}
+	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 {
+	log.Debug("making servers")
+	var errs Errors
+	for _, sc := range g.Config.Servers {
+		if _, ok := g.servers[sc.ListenInterface]; ok {
+			// server already instantiated
 			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)
 		if err != nil {
-			log.WithError(err).Error("Failed to create server")
-		} else {
-			g.servers = append(g.servers, *server)
+			log.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
+}
+
+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)
+		})
+		log.Infof("allowed_hosts config changed, a new list was set")
+	})
+
+	// server was removed from config
+	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)
+			log.Infof("New server added [%s]", sc.ListenInterface)
+			if g.state == GuerrillaStateStarted {
+				err := g.Start()
+				if err != nil {
+					log.WithError(err).Info("Event server_change:new_server returned errors when starting")
+				}
+			}
+		}
+	})
+	// start a server that already exists in 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 {
+				log.Infof("Starting server [%s]", server.listenInterface)
+				err := g.Start()
+				if err != nil {
+					log.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()
+				log.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)
+			log.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 {
+				log.Infof("Server [%s] new TLS configuration loaded", sc.ListenInterface)
+			} else {
+				log.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
+		})
+	})
+
 }
 
 // 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 {
 		return append(startErrors, errors.New("No servers to start, please check the config"))
 	}
 	// channel for reading errors
 	errs := make(chan error, len(g.servers))
 	var startWG sync.WaitGroup
-	startWG.Add(len(g.servers))
+
 	// 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) {
 			if err := s.Start(&startWG); err != nil {
 				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()
 
 	// close, then read any errors
@@ -65,13 +271,39 @@ func (g *guerrilla) Start() (startErrors []error) {
 			startErrors = append(startErrors, err)
 		}
 	}
-	return startErrors
+	if len(startErrors) > 0 {
+		return startErrors
+	}
+	return nil
 }
 
 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()
+			log.Infof("shutdown completed for [%s]", ListenInterface)
+		}
 	}
-	log.Infof("Backend shutdown completed")
+	if err := g.backend.Shutdown(); err != nil {
+		log.WithError(err).Warn("Backend failed to shutdown")
+	} else {
+		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)
 }

+ 0 - 60
models.go

@@ -3,10 +3,7 @@ package guerrilla
 import (
 	"bufio"
 	"errors"
-	"fmt"
 	"io"
-	"strconv"
-	"strings"
 )
 
 var (
@@ -14,63 +11,6 @@ var (
 	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
 type adjustableLimitedReader struct {
 	R *io.LimitedReader

+ 30 - 18
pool.go

@@ -41,6 +41,22 @@ type lentClients struct {
 	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.
 func NewPool(poolSize int) *Pool {
 	return &Pool{
@@ -65,10 +81,9 @@ func (p *Pool) ShutdownState() {
 	p.ShutdownChan <- 1             // release any waiting p.sem
 
 	// 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 +108,9 @@ func (p *Pool) IsShuttingDown() bool {
 
 // set a timeout for all lent clients
 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
@@ -146,15 +158,15 @@ func (p *Pool) Return(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) {
-	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)
 }

+ 135 - 83
server.go

@@ -13,7 +13,9 @@ import (
 
 	log "github.com/Sirupsen/logrus"
 
+	"github.com/flashmob/go-guerrilla/backends"
 	"sync"
+	"sync/atomic"
 )
 
 const (
@@ -31,68 +33,132 @@ const (
 	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
 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)
+	//mainConfigStore  atomic.Value // stores guerrilla.Config
+	configStore atomic.Value // stores guerrilla.ServerConfig
+	//config         *ServerConfig
+	backend         backends.Backend
+	tlsConfig       *tls.Config
+	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
+}
+
+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
-func newServer(sc ServerConfig, b *Backend) (*server, error) {
+func newServer(sc *ServerConfig, b backends.Backend) (*server, error) {
 	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,
+	}
+	server.setConfig(sc)
+	server.setTimeout(sc.Timeout)
+	if err := server.configureSSL(); err != nil {
+		return server, err
 	}
-	if server.config.TLSAlwaysOn || server.config.StartTLSOn {
-		cert, err := tls.LoadX509KeyPair(server.config.PublicKeyFile, server.config.PrivateKeyFile)
+	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 {
-			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},
 			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
+}
+
+// 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 {
 	var clientID uint64
 	clientID = 0
 
-	listener, err := net.Listen("tcp", server.config.ListenInterface)
+	listener, err := net.Listen("tcp", server.listenInterface)
 	server.listener = listener
 	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
+	log.Infof("Listening on TCP %s", server.listenInterface)
+	server.state = ServerStateRunning
+	startWG.Done() // start successful, don't wait for me
 
 	for {
-		log.Debugf("[%s] Waiting for a new client. Next Client ID: %d", server.config.ListenInterface, clientID+1)
+		log.Debugf("[%s] Waiting for a new client. Next Client ID: %d", server.listenInterface, clientID+1)
 		conn, err := listener.Accept()
 		clientID++
 		if err != nil {
 			if e, ok := err.(net.Error); ok && !e.Temporary() {
-				log.Infof("Server [%s] has stopped accepting new clients", server.config.ListenInterface)
+				log.Infof("Server [%s] has stopped accepting new clients", server.listenInterface)
 				// the listener has been closed, wait for clients to exit
-				log.Infof("shutting down pool [%s]", server.config.ListenInterface)
+				log.Infof("shutting down pool [%s]", server.listenInterface)
+				server.clientPool.ShutdownState()
 				server.clientPool.ShutdownWait()
+				server.state = ServerStateStopped
 				server.closedListener <- true
 				return nil
 			}
@@ -118,15 +184,17 @@ func (server *server) Start(startWG *sync.WaitGroup) error {
 }
 
 func (server *server) Shutdown() {
-	server.clientPool.ShutdownState()
 	if server.listener != nil {
+		// This will cause Start function to return, by causing an error on listener.Accept
 		server.listener.Close()
-		// wait for the listener to close.
+		// wait for the listener to listener.Accept
 		<-server.closedListener
 		// At this point Start will exit and close down the pool
 	} else {
+		server.clientPool.ShutdownState()
 		// listener already closed, wait for clients to exit
 		server.clientPool.ShutdownWait()
+		server.state = ServerStateStopped
 	}
 }
 
@@ -136,39 +204,17 @@ func (server *server) GetActiveClientsCount() int {
 
 // Verifies that the host is a valid recipient.
 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
 }
 
-// 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
-}
-
 // Reads from the client until a terminating sequence is encountered,
 // or until a timeout occurs.
-func (server *server) read(client *client) (string, error) {
+func (server *server) read(client *client, maxSize int64) (string, error) {
 	var input, reply string
 	var err error
 
@@ -180,7 +226,7 @@ func (server *server) read(client *client) (string, error) {
 	}
 
 	for {
-		client.setTimeout(server.timeout)
+		client.setTimeout(server.timeout.Load().(time.Duration))
 		reply, err = client.bufin.ReadString('\n')
 		input = input + reply
 		if err == nil && client.state == ClientData {
@@ -188,14 +234,16 @@ func (server *server) read(client *client) (string, error) {
 				// 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 int64(len(input)) > maxSize {
+				return input, fmt.Errorf("Maximum DATA size exceeded (%d)", maxSize)
 			}
 		}
 		if err != nil {
 			break
 		}
 		if strings.HasSuffix(input, suffix) {
+			// discard the suffix and stop reading
+			input = input[0 : len(input)-len(suffix)]
 			break
 		}
 	}
@@ -204,7 +252,7 @@ func (server *server) read(client *client) (string, error) {
 
 // Writes a response to the client.
 func (server *server) writeResponse(client *client) error {
-	client.setTimeout(server.timeout)
+	client.setTimeout(server.timeout.Load().(time.Duration))
 	size, err := client.bufout.WriteString(client.response)
 	if err != nil {
 		return err
@@ -223,32 +271,34 @@ func (server *server) isShuttingDown() bool {
 
 // Handles an entire client SMTP exchange
 func (server *server) handleClient(client *client) {
-	defer server.closeConn(client)
-
+	defer client.closeConn()
+	sc := server.configStore.Load().(ServerConfig)
 	log.Infof("Handle client [%s], id: %d", client.RemoteAddress, client.ID)
 
 	// Initial greeting
 	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())
 
-	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 := fmt.Sprintf("250-%s Hello", sc.Hostname)
 
 	// 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"
 	advertiseTLS := "250-STARTTLS\r\n"
 	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 && client.upgradeToTLS(tlsConfig) {
+			advertiseTLS = ""
+		} else {
+			// server requires TLS, but can't handshake
 			client.kill()
 		}
-		advertiseTLS = ""
 	}
-	if !server.config.StartTLSOn {
+	if !sc.StartTLSOn {
 		// STARTTLS turned off, don't advertise it
 		advertiseTLS = ""
 	}
@@ -260,7 +310,7 @@ func (server *server) handleClient(client *client) {
 			client.state = ClientCmd
 		case ClientCmd:
 			client.bufin.setLimit(CommandLineMaxLength)
-			input, err := server.read(client)
+			input, err := server.read(client, sc.MaxSize)
 			log.Debugf("Client sent: %s", input)
 			if err == io.EOF {
 				log.WithError(err).Warnf("Client closed the connection: %s", client.RemoteAddress)
@@ -348,7 +398,7 @@ func (server *server) handleClient(client *client) {
 				client.kill()
 
 			case strings.Index(cmd, "DATA") == 0:
-				if client.MailFrom.isEmpty() {
+				if client.MailFrom.IsEmpty() {
 					client.responseAdd("503 Error: No sender")
 					break
 				}
@@ -359,7 +409,7 @@ func (server *server) handleClient(client *client) {
 				client.responseAdd("354 Enter message, ending with '.' on a line by itself")
 				client.state = ClientData
 
-			case server.config.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
+			case sc.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
 				client.responseAdd("220 Ready to start TLS")
 				client.state = ClientStartTLS
 			default:
@@ -374,9 +424,10 @@ func (server *server) handleClient(client *client) {
 
 		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.
+			client.Data, err = server.read(client, sc.MaxSize)
 			if err != nil {
 				if err == LineLimitExceeded {
 					client.responseAdd("550 Error: " + LineLimitExceeded.Error())
@@ -404,8 +455,9 @@ func (server *server) handleClient(client *client) {
 			client.resetTransaction()
 
 		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 && client.upgradeToTLS(tlsConfig) {
 					advertiseTLS = ""
 					client.resetTransaction()
 				}

+ 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
+}

+ 54 - 105
tests/guerrilla_test.go

@@ -26,9 +26,9 @@ import (
 
 	"bytes"
 	"crypto/tls"
-	//	"crypto/x509"
 	"errors"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/tests/testcert"
 	"io/ioutil"
 	"net"
 	"strings"
@@ -65,8 +65,8 @@ func init() {
 		initErr = errors.New("Could not unmartial config," + err.Error())
 	} else {
 		setupCerts(config)
-		backend := getDummyBackend(config.BackendConfig)
-		app = guerrilla.New(&config.AppConfig, &backend)
+		backend, _ := getBackend("dummy", config.BackendConfig)
+		app, _ = guerrilla.New(&config.AppConfig, backend)
 	}
 
 }
@@ -113,48 +113,18 @@ var configJson = `
 }
 `
 
-func getDummyBackend(backendConfig map[string]interface{}) guerrilla.Backend {
-	var backend guerrilla.Backend
-	b := &backends.DummyBackend{}
-	b.Initialize(backendConfig)
-	backend = guerrilla.Backend(b)
-	return backend
+func getBackend(backendName string, backendConfig map[string]interface{}) (backends.Backend, error) {
+	return backends.New(backendName, backendConfig)
 }
 
 func setupCerts(c *TestConfig) {
 	for i := range c.Servers {
-		generateCert(c.Servers[i].Hostname, "", 365*24*time.Hour, false, 2048, "P256")
+		testcert.GenerateCert(c.Servers[i].Hostname, "", 365*24*time.Hour, false, 2048, "P256", "./")
 		c.Servers[i].PrivateKeyFile = c.Servers[i].Hostname + ".key.pem"
 		c.Servers[i].PublicKeyFile = c.Servers[i].Hostname + ".cert.pem"
 	}
 }
 
-func connect(serverIndex int, deadline time.Duration) (net.Conn, *bufio.Reader, error) {
-	var bufin *bufio.Reader
-
-	conn, err := net.Dial("tcp", config.Servers[serverIndex].ListenInterface)
-	if err != nil {
-		// handle error
-		//t.Error("Cannot dial server", config.Servers[0].ListenInterface)
-		return conn, bufin, errors.New("Cannot dial server: " + config.Servers[serverIndex].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
-}
-
 // Testing start and stop of server
 func TestStart(t *testing.T) {
 	if initErr != nil {
@@ -162,9 +132,7 @@ func TestStart(t *testing.T) {
 		t.FailNow()
 	}
 	if startErrors := app.Start(); startErrors != nil {
-		for _, err := range startErrors {
-			t.Error(err)
-		}
+		t.Error(startErrors)
 		t.FailNow()
 	}
 	time.Sleep(time.Second)
@@ -270,9 +238,7 @@ func TestGreeting(t *testing.T) {
 
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			t.FailNow()
 		}
 	}
@@ -301,22 +267,23 @@ func TestShutDown(t *testing.T) {
 		t.FailNow()
 	}
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 
 			// do a shutdown while the client is connected & in client state
 			go app.Shutdown()
+			time.Sleep(time.Millisecond * 150) // let server to Shutdown
 
 			// issue a command while shutting down
-			response, err := command(conn, bufin, "HELP")
+			response, err := Command(conn, bufin, "HELP")
 			if err != nil {
 				t.Error("Help command failed", err.Error())
 			}
@@ -331,9 +298,7 @@ func TestShutDown(t *testing.T) {
 
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -360,24 +325,24 @@ func TestRFC2821LimitRecipients(t *testing.T) {
 		t.FailNow()
 	}
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 
 			for i := 0; i < 101; i++ {
-				if _, err := command(conn, bufin, fmt.Sprintf("RCPT TO:test%[email protected]", i)); err != nil {
+				if _, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:test%[email protected]", i)); err != nil {
 					t.Error("RCPT TO", err.Error())
 					break
 				}
 			}
-			response, err := command(conn, bufin, "RCPT TO:[email protected]")
+			response, err := Command(conn, bufin, "RCPT TO:[email protected]")
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -392,9 +357,7 @@ func TestRFC2821LimitRecipients(t *testing.T) {
 
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -415,18 +378,18 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 	}
 
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 65)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 65)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -436,7 +399,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 			}
 			// what about if it's exactly 64?
 			// repeat > 64 characters in local part
-			response, err = command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 64)))
+			response, err = Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 64)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -451,9 +414,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -474,18 +435,18 @@ func TestRFC2821LimitPath(t *testing.T) {
 		t.FailNow()
 	}
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 256 characters in local part
-			response, err := command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 257-7)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:%[email protected]", strings.Repeat("a", 257-7)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -494,7 +455,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			// what about if it's exactly 256?
-			response, err = command(conn, bufin,
+			response, err = Command(conn, bufin,
 				fmt.Sprintf("RCPT TO:%s@%s.la", strings.Repeat("a", 64), strings.Repeat("b", 257-5-64)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
@@ -508,9 +469,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 		app.Shutdown()
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -528,18 +487,18 @@ func TestRFC2821LimitDomain(t *testing.T) {
 		t.FailNow()
 	}
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := command(conn, bufin, fmt.Sprintf("RCPT TO:a@%s.l", strings.Repeat("a", 255-2)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:a@%s.l", strings.Repeat("a", 255-2)))
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -548,7 +507,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			// what about if it's exactly 255?
-			response, err = command(conn, bufin,
+			response, err = Command(conn, bufin,
 				fmt.Sprintf("RCPT TO:a@%s.la", strings.Repeat("b", 255-4)))
 			if err != nil {
 				t.Error("command failed", err.Error())
@@ -562,9 +521,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 		app.Shutdown()
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -582,22 +539,22 @@ func TestNestedMailCmd(t *testing.T) {
 		t.FailNow()
 	}
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
-			response, err = command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -606,10 +563,10 @@ func TestNestedMailCmd(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			// Plot twist: if you EHLO , it should allow MAIL FROM again
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
-			response, err = command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -618,7 +575,7 @@ func TestNestedMailCmd(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			// Plot twist: if you RSET , it should allow MAIL FROM again
-			response, err = command(conn, bufin, "RSET")
+			response, err = Command(conn, bufin, "RSET")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -627,7 +584,7 @@ func TestNestedMailCmd(t *testing.T) {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 
-			response, err = command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err = Command(conn, bufin, "MAIL FROM:[email protected]")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -641,9 +598,7 @@ func TestNestedMailCmd(t *testing.T) {
 		app.Shutdown()
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -662,18 +617,18 @@ func TestCommandLineMaxLength(t *testing.T) {
 	}
 
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 1024 characters
-			response, err := command(conn, bufin, strings.Repeat("s", guerrilla.CommandLineMaxLength+1))
+			response, err := Command(conn, bufin, strings.Repeat("s", guerrilla.CommandLineMaxLength+1))
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
@@ -688,9 +643,7 @@ func TestCommandLineMaxLength(t *testing.T) {
 		app.Shutdown()
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -709,40 +662,40 @@ func TestDataMaxLength(t *testing.T) {
 	}
 
 	if startErrors := app.Start(); startErrors == nil {
-		conn, bufin, err := connect(0, 20)
+		conn, bufin, err := Connect(config.Servers[0], 20)
 		if err != nil {
 			// handle error
 			t.Error(err.Error(), config.Servers[0].ListenInterface)
 			t.FailNow()
 		} else {
 			// client goes into command state
-			if _, err := command(conn, bufin, "HELO localtester"); err != nil {
+			if _, err := Command(conn, bufin, "HELO localtester"); err != nil {
 				t.Error("Hello command failed", err.Error())
 			}
 
-			response, err := command(conn, bufin, "MAIL FROM:[email protected]")
+			response, err := Command(conn, bufin, "MAIL FROM:[email protected]")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
 			//fmt.Println(response)
-			response, err = command(conn, bufin, "RCPT TO:[email protected]")
+			response, err = Command(conn, bufin, "RCPT TO:[email protected]")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
 			//fmt.Println(response)
-			response, err = command(conn, bufin, "DATA")
+			response, err = Command(conn, bufin, "DATA")
 			if err != nil {
 				t.Error("command failed", err.Error())
 			}
 
-			response, err = command(
+			response, err = Command(
 				conn,
 				bufin,
 				fmt.Sprintf("Subject:test\r\n\r\nHello %s\r\n.\r\n",
 					strings.Repeat("n", int(config.Servers[0].MaxSize-26))))
 
 			//expected := "500 Line too long"
-			expected := "550 Error: Maximum line length exceeded"
+			expected := "451 Error: Maximum DATA size exceeded"
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response, err)
 			}
@@ -752,9 +705,7 @@ func TestDataMaxLength(t *testing.T) {
 		app.Shutdown()
 	} else {
 		if startErrors := app.Start(); startErrors != nil {
-			for _, err := range startErrors {
-				t.Error(err)
-			}
+			t.Error(startErrors)
 			app.Shutdown()
 			t.FailNow()
 		}
@@ -764,5 +715,3 @@ func TestDataMaxLength(t *testing.T) {
 	logBuffer.Reset()
 	logIn.Reset(&logBuffer)
 }
-
-//

+ 5 - 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
 
-// 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 (
 	"crypto/ecdsa"
@@ -63,7 +62,7 @@ func pemBlockForKey(priv interface{}) *pem.Block {
 
 // 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 {
 		log.Fatalf("Missing required --host parameter")
@@ -141,14 +140,14 @@ func generateCert(host string, validFrom string, validFor time.Duration, isCA bo
 		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 {
 		log.Fatalf("failed to open cert.pem for writing: %s", err)
 	}
 	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
 	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 {
 		log.Print("failed to open key.pem for writing:", err)
 		return

+ 3 - 2
util.go

@@ -2,14 +2,15 @@ package guerrilla
 
 import (
 	"errors"
+	"github.com/flashmob/go-guerrilla/envelope"
 	"regexp"
 	"strings"
 )
 
 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
 	if len(str) > RFC2821LimitPath {
 		return email, errors.New("501 Path too long")