Browse Source

Merge branch 'master' into dashboard

# Conflicts:
#	.gitignore
#	.travis.yml
#	README.md
#	api.go
#	api_test.go
#	backends/backend.go
#	backends/gateway.go
#	backends/gateway_test.go
#	backends/p_mysql.go
#	backends/p_redis.go
#	backends/processor.go
#	backends/validate.go
#	client.go
#	cmd/guerrillad/serve.go
#	cmd/guerrillad/serve_test.go
#	config.go
#	config_test.go
#	event.go
#	glide.lock
#	goguerrilla.conf.sample
#	guerrilla.go
#	log/log.go
#	mail/envelope.go
#	mail/envelope_test.go
#	server.go
#	tests/guerrilla_test.go
flashmob 8 years ago
parent
commit
bfe58e2070
32 changed files with 1090 additions and 1002 deletions
  1. 2 0
      .gitignore
  2. 1 1
      .travis.yml
  3. 167 403
      README.md
  4. 68 34
      api.go
  5. 8 8
      api_test.go
  6. 1 1
      backends/backend.go
  7. 128 66
      backends/gateway.go
  8. 1 1
      backends/gateway_test.go
  9. 106 41
      backends/p_mysql.go
  10. 10 11
      backends/p_redis.go
  11. 3 0
      backends/processor.go
  12. 1 1
      backends/util.go
  13. 3 2
      backends/validate.go
  14. 2 3
      client.go
  15. 2 2
      cmd/guerrillad/root.go
  16. 26 42
      cmd/guerrillad/serve.go
  17. 51 51
      cmd/guerrillad/serve_test.go
  18. 5 3
      config.go
  19. 3 3
      config_test.go
  20. 2 0
      event.go
  21. 5 5
      glide.lock
  22. 8 8
      goguerrilla.conf.sample
  23. 22 21
      guerrilla.go
  24. 189 0
      log/hook.go
  25. 102 217
      log/log.go
  26. 31 8
      mail/envelope.go
  27. 11 0
      mail/envelope_test.go
  28. 2 1
      pool.go
  29. 1 1
      response/quote.go
  30. 72 54
      server.go
  31. 53 9
      server_test.go
  32. 4 5
      tests/guerrilla_test.go

+ 2 - 0
.gitignore

@@ -1,5 +1,7 @@
 .idea
 .idea
 goguerrilla.conf
 goguerrilla.conf
+goguerrilla.conf.json
 /guerrillad
 /guerrillad
 vendor
 vendor
+go-guerrilla.wiki
 statik
 statik

+ 1 - 1
.travis.yml

@@ -23,4 +23,4 @@ install:
 script:
 script:
   - ./.travis.gofmt.sh
   - ./.travis.gofmt.sh
   - make guerrillad
   - make guerrillad
-  - make test
+  - make test

+ 167 - 403
README.md

@@ -1,337 +1,228 @@
 
 
 [![Build Status](https://travis-ci.org/flashmob/go-guerrilla.svg?branch=master)](https://travis-ci.org/flashmob/go-guerrilla)
 [![Build Status](https://travis-ci.org/flashmob/go-guerrilla.svg?branch=master)](https://travis-ci.org/flashmob/go-guerrilla)
 
 
-Go-Guerrilla SMTPd
+Go-Guerrilla SMTP Daemon
 ====================
 ====================
 
 
-An minimalist SMTP server written in Go, made for receiving large volumes of mail.
+A lightweight SMTP server written in Go, made for receiving large volumes of mail.
+To be used as a package in your Go project, or as a stand-alone daemon by running the "guerrillad" binary.
+
+Supports MySQL and Redis out-of-the-box, with many other vendor provided _processors_,
+such as [MailDir](https://github.com/flashmob/maildir-processor) and even [FastCGI](https://github.com/flashmob/fastcgi-processor)! 
+See below for a list of available processors.
 
 
 ![Go Guerrilla](/GoGuerrilla.png)
 ![Go Guerrilla](/GoGuerrilla.png)
 
 
-### What is Go Guerrilla SMTPd?
+### What is Go-Guerrilla?
 
 
-It's a small SMTP server written in Go, for the purpose of receiving large volume of email.
-Written for GuerrillaMail.com which processes hundreds of thousands of emails
-every hour.
+It's an SMTP server written in Go, for the purpose of receiving large volumes of email.
+It started as a project for GuerrillaMail.com which processes millions of emails every day,
+and needed a daemon with less bloat & written in a more memory-safe language that can 
+take advantage of modern multi-core architectures.
 
 
 The purpose of this daemon is to grab the email, save it,
 The purpose of this daemon is to grab the email, save it,
 and disconnect as quickly as possible, essentially performing the services of a
 and disconnect as quickly as possible, essentially performing the services of a
-Mail Transfer Agent (MTA).
-
-A typical user of this software would probably use it as a package in their own
-Go project in order to receive and deliver email.
+Mail Transfer Agent (MTA) without the sending functionality.
 
 
-Go-Guerrilla allows you to customize how the email is delivered.
+The software also includes a modular backend implementation, which can extend the email
+processing functionality to whatever needs you may require. We refer to these modules as 
+"_Processors_". Processors can be chained via the config to perform different tasks on 
+received email, or to validate recipients.
 
 
-Out of the box, Go-Guerrilla does not attempt to filter HTML, check for spam or do any
-sender verification. However, it comes with a modular middleware-like backend system which
-support a range of different features and ways of delivering email.
 See the list of available _Processors_ below.
 See the list of available _Processors_ below.
 
 
+For more details about the backend system, see the:
+[Backends, configuring and extending](https://github.com/flashmob/go-guerrilla/wiki/Backends,-configuring-and-extending) page.
+
+### License
+
 The software is using MIT License (MIT) - contributors welcome.
 The software is using MIT License (MIT) - contributors welcome.
 
 
 ### Features
 ### Features
 
 
-- Multi-server. The daemon can spawn multiple servers at once, all sharing the same backend
+#### Main Features
+
+- Multi-server. Can spawn multiple servers, all sharing the same backend
 for saving email.
 for saving email.
-- Config hot-reloading. Add/Remove/Enable/Disable servers without restarting. Reload TLS configuration, and most other settings on the fly.
+- Config hot-reloading. Add/Remove/Enable/Disable servers without restarting. 
+Reload TLS configuration, change most other settings on the fly.
 - Graceful shutdown: Minimise loss of email if you need to shutdown/restart.
 - Graceful shutdown: Minimise loss of email if you need to shutdown/restart.
-- Pooling: The daemon uses pooling where possible. It's friendly to the garbage collector.
-- Modular, component based, backend system for processing email that's easy to extend.  
-- Backend system arranged in a producer/consumer type structure, making use of Go's channels.
-- Fuzz tested.
-- Can be used as a package in your Go project.
+- Be a gentleman to the garbage collector: resources are pooled & recycled where possible.
+- Modular [Backend system](https://github.com/flashmob/go-guerrilla/wiki/Backends,-configuring-and-extending) 
+- Modern TLS support (STARTTLS or SMTPS).
+- Can be [used as a package](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package) in your Go project. 
+Get started in just a few lines of code!
+- [Fuzz tested](https://github.com/flashmob/go-guerrilla/wiki/Fuzz-testing). 
+[Auto-tested](https://travis-ci.org/flashmob/go-guerrilla). Battle Tested.
+
+#### Backend Features
+
+- Arranged as workers running in parallel, using a producer/consumer type structure, 
+ taking advantage of Go's channels and go-routines. 
+- Modular [backend system](https://github.com/flashmob/go-guerrilla/wiki/Backends,-configuring-and-extending)
+ structured using a [decorator-like pattern](https://en.wikipedia.org/wiki/Decorator_pattern) which allows the chaining of components (a.k.a. _Processors_) via the config.  
+- Different ways for processing / delivering email: Supports MySQL and Redis out-of-the box, many other 
+vendor provided processors available.
 
 
 ### Roadmap / Contributing & Bounties
 ### Roadmap / Contributing & Bounties
 
 
+Pull requests / issue reporting & discussion / code reviews always 
+welcome. To encourage more pull requests, we are now offering bounties. 
 
 
-Pull requests / issue reporting & discussion / code reviews always
-welcome. To encourage more pull requests, we are now offering bounties
-funded from our bitcoin donation address:
-
-`1grr11aWtbsyMUeB4EGfHvTuu7eFzkJ4A`
-
-So far we have the following bounties are still open:
-(Updated 22 Dec 2016)
-
-- Let's encrypt TLS certificate support!
-Take a look at https://github.com/flashmob/go-guerrilla/issues/29
-(0.5 for a successful merge)
-
-- Analytics: A web based admin panel that displays live statistics,
-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)
-
-- 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.)
+Take a look at our [Bounties and Roadmap](https://github.com/flashmob/go-guerrilla/wiki/Roadmap-and-Bounties) page!
 
 
-- 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
-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. (Flame graph maybe? https://github.com/uber/go-torch
-Please open an issue before to discuss scope)
-(0.25 BTC)
+Getting started
+===========================
 
 
-- Looking for someone to do a code review & possibly fix any tidbits,
-they find, or suggestions for doing things better.
-(Already one bounty of 0.25 paid, however, more is always welcome)
+(Assuming that you have GNU make and latest Go on your system)
 
 
-Ready to roll up your sleeves and have a go?
-Please open an issue for more clarification / details on Github.
-Also, welcome your suggestions for adding things to this Roadmap - please open an issue.
+#### Dependencies
 
 
-Another way to contribute is to donate to our bitcoin address to help
-us fund more bounties!
-`1grr11aWtbsyMUeB4EGfHvTuu7eFzkJ4A`
+Go-Guerrilla uses [Glide](https://github.com/Masterminds/glide) to manage 
+dependencies. If you have glide installed, just run `glide install` as usual.
+ 
+You can also run `$ go get ./..` if you don't want to use glide, and then run `$ make test`
+to ensure all is good.
 
 
-### Brief History and purpose
+To build the binary run:
 
 
-Go-Guerrilla is used as the primary server for receiving email at
-Guerrilla Mail. As of 2016, it's handling all connections without any
-proxy (Nginx).
+```
+$ make guerrillad
+```
 
 
-Originally, Guerrilla Mail ran Exim which piped email to a php script (2009).
-As the site got popular and more email came through, this approach
-eventually swamped the server.
+This will create a executable file named `guerrillad` that's ready to run.
 
 
-The next solution was to decrease the heavy setup into something more
-lightweight. A small script was written to implement a basic SMTP server (2010).
-Eventually that script also got swamped, so it was re-written to use
-event driven I/O (2012). A year later, the latest script also became inadequate
- so it was ported to Go and has served us well since.
+Next, copy the `goguerrilla.conf.sample` file to `goguerrilla.conf.json`. 
+You may need to customize the `pid_file` setting to somewhere local, 
+and also set `tls_always_on` to false if you don't have a valid certificate setup yet. 
 
 
+Next, run your server like this:
 
 
-Getting started
-===========================
+`$ ./guerrillad serve`
 
 
-(Assuming that you have GNU make and latest Go on your system)
+The configuration options are detailed on the [configuration page](https://github.com/flashmob/go-guerrilla/wiki/Configuration). 
+The main takeaway here is:
 
 
-To build for the first time (installs dependencies and builds the web dashboard):
-```
-$ make dependencies
-$ make dashboard
-$ make guerrillad
-```
+The default configuration uses 3 _processors_, they are set using the `save_process` 
+config option. Notice that it contains the following value: 
+`"HeadersParser|Header|Debugger"` - this means, once an email is received, it will
+first go through the `HeadersParser` processor where headers will be parsed.
+Next, it will go through the `Header` processor, where delivery headers will be added.
+Finally, it will finish at the `Debugger` which will log some debug messages.
 
 
-To build afterward, just run
-```
-$ make guerrillad
-```
+Where to go next?
 
 
-Rename goguerrilla.conf.sample to goguerrilla.conf
-```
-$ cp goguerrilla.conf.sample goguerrilla.conf
-```
+- Try setting up an [example configuration](https://github.com/flashmob/go-guerrilla/wiki/Configuration-example:-save-to-Redis-&-MySQL) 
+which saves email bodies to Redis and metadata to MySQL.
+- Try importing some of the 'vendored' processors into your project. See [MailDiranasaurus](https://github.com/flashmob/maildiranasaurus)
+as an example project which imports the [MailDir](https://github.com/flashmob/maildir-processor) and [FastCGI](https://github.com/flashmob/fastcgi-processor) processors.
+- Try hacking the source and [create your own processor](https://github.com/flashmob/go-guerrilla/wiki/Backends,-configuring-and-extending).
+- Once your daemon is running, you might want to stup [log rotation](https://github.com/flashmob/go-guerrilla/wiki/Automatic-log-file-management-with-logrotate).
 
 
-See `backends/guerrilla_db_redis.go` source to use an example for creating your own email saving backend,
-or the dummy one if you'd like to start from scratch.
-
-If you want to build on the sample `guerrilla-db-redis` module, setup the following table
-in MySQL:
-
-	CREATE TABLE IF NOT EXISTS `new_mail` (
-	  `mail_id` BIGINT(20) unsigned NOT NULL AUTO_INCREMENT,
-	  `date` datetime NOT NULL,
-	  `from` varchar(128) character set latin1 NOT NULL,
-	  `to` varchar(128) character set latin1 NOT NULL,
-	  `subject` varchar(255) NOT NULL,
-	  `body` text NOT NULL,
-	  `charset` varchar(32) character set latin1 NOT NULL,
-	  `mail` longblob NOT NULL,
-	  `spam_score` float NOT NULL,
-	  `hash` char(32) character set latin1 NOT NULL,
-	  `content_type` varchar(64) character set latin1 NOT NULL,
-	  `recipient` varchar(128) character set latin1 NOT NULL,
-	  `has_attach` int(11) NOT NULL,
-	  `ip_addr` varchar(15) NOT NULL,
-	  `return_path` VARCHAR(255) NOT NULL,
-	  `is_tls` BIT(1) DEFAULT b'0' NOT NULL,
-	  PRIMARY KEY  (`mail_id`),
-	  KEY `to` (`to`),
-	  KEY `hash` (`hash`),
-	  KEY `date` (`date`)
-	) ENGINE=InnoDB  DEFAULT CHARSET=utf8
-
-The above table does not store the body of the email which makes it quick
-to query and join, while the body of the email is fetched from Redis
-for future processing. The `mail` field can contain data in case Redis is down.
-Otherwise, if data is in Redis, the `mail` will be blank, and
-the `body` field will contain the word 'redis'.
-
-You can implement your own saveMail function to use whatever storage /
-backend fits for you. Please share them ^_^, in particular, we would
-like to see other formats such as maildir and mbox.
 
 
 
 
 Use as a package
 Use as a package
 ============================
 ============================
-Guerrilla SMTPd can also be imported and used as a package in your project.
+Go-Guerrilla can be imported and used as a package in your Go project.
+
+### Quickstart
+
 
 
-## Import Guerrilla.
+#### 1. Import the guerrilla package
 ```go
 ```go
-import "github.com/flashmob/go-guerrilla"
+import (
+    "github.com/flashmob/go-guerrilla/guerrilla"
+)
 
 
 
 
 ```
 ```
 
 
-## Implement the `Backend` interface
-Or use one of the implementations in the `backends` sub-package). This is how
-your application processes emails received by the Guerrilla app.
-```go
-import "github.com/flashmob/go-guerrilla/mail"
-import "github.com/flashmob/go-guerrilla/backends"
+You may use ``$ go get ./...`` to get all dependencies, also Go-Guerrilla uses 
+[glide](https://github.com/Masterminds/glide) for dependency management.
 
 
-type CustomBackend struct {...}
+#### 2. Start a server
 
 
-func (cb *CustomBackend) Process(e *mail.Envelope) backends.Result {
-  err := saveSomewhere(e.NewReader())
-  if err != nil {
-    return guerrilla.NewResult(fmt.Sprintf("554 Error: %s", err.Error()))
-  }
-  return guerrilla.NewResult("250 OK")
-}
-```
+This will start a server with the default settings, listening on `127.0.0.1:2525`
 
 
-## Create a logger
 
 
 ```go
 ```go
-import "github.com/flashmob/go-guerrilla/log"
 
 
-mainlog, err := log.GetLogger(log.OutputStderr.String());
-if  err != nil {
-    fmt.Println("Cannot open log:", err)
-    os.Exit(1)
-}
-```
+d := guerrilla.Daemon{}
+err := d.Start()
 
 
-## Create an app instance.
-See Configuration section below for setting configuration options.
-```go
-config := &guerrilla.AppConfig{
-  Servers: []guerrilla.ServerConfig{...},
-  AllowedHosts: []string{...}
+if err == nil {
+    fmt.Println("Server Started!")
 }
 }
-backend := &CustomBackend{...}
-app, err := guerrilla.New(config, backend, mainlog)
 ```
 ```
 
 
-## Start the app.
-`Start` is non-blocking, so make sure the main goroutine is kept busy
-```go
-startErrors := app.Start()
-```
+`d.Start()` *does not block* after the server has been started, so make sure that you keep your program busy.
+
+The defaults are: 
+* Server listening to 127.0.0.1:2525
+* use your hostname to determine your which hosts to accept email for
+* 100 maximum clients
+* 10MB max message size 
+* log to Stderror, 
+* log level set to "`debug`"
+* timeout to 30 sec 
+* Backend configured with the following processors: `HeadersParser|Header|Debugger` where it will log the received emails.
+
+Next, you may want to [change the interface](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#starting-a-server---custom-listening-interface) (`127.0.0.1:2525`) to the one of your own choice.
+
+#### API Documentation topics
+
+Please continue to the [API documentation](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package) for the following topics:
+
+
+- [Suppressing log output](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#starting-a-server---suppressing-log-output)
+- [Custom listening interface](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#starting-a-server---custom-listening-interface)
+- [What else can be configured](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#what-else-can-be-configured)
+- [Backends](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#backends)
+    - [About the backend system](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#about-the-backend-system)
+    - [Backend Configuration](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#backend-configuration)
+    - [Registering a Processor](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#registering-a-processor)
+- [Loading config from JSON](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#loading-config-from-json)
+- [Config hot-reloading](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#config-hot-reloading)
+- [Logging](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#logging-stuff)
+- [Log re-opening](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#log-re-opening)
+- [Graceful shutdown](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#graceful-shutdown)
+- [Pub/Sub](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#pubsub)
+- [More Examples](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#more-examples)
+
+Use as a Daemon
+==========================================================
 
 
-## Shutting down.
-`Shutdown` will do a graceful shutdown, close all the connections, close
- the ports, and gracefully shutdown the backend. It will block until all
-  operations are complete.
+### Manual for using from the command line
 
 
-```go
-app.Shutdown()
-```
+- [guerrillad command](https://github.com/flashmob/go-guerrilla/wiki/Running-from-command-line#guerrillad-command)
+    - [Starting](https://github.com/flashmob/go-guerrilla/wiki/Running-from-command-line#starting)
+    - [Re-loading configuration](https://github.com/flashmob/go-guerrilla/wiki/Running-from-command-line#re-loading-the-config)
+    - [Re-open logs](https://github.com/flashmob/go-guerrilla/wiki/Running-from-command-line#re-open-log-file)
+    - [Examples](https://github.com/flashmob/go-guerrilla/wiki/Running-from-command-line#examples)
+
+### Other topics
+
+- [Using Nginx as a proxy](https://github.com/flashmob/go-guerrilla/wiki/Using-Nginx-as-a-proxy)
+- [Testing STARTTLS](https://github.com/flashmob/go-guerrilla/wiki/Running-from-command-line#testing-starttls)
+- [Benchmarking](https://github.com/flashmob/go-guerrilla/wiki/Profiling#benchmarking)
 
 
-Configuration
-============================================
-The configuration is in strict JSON format. Here is an annotated configuration.
-Copy goguerrilla.conf.sample to goguerrilla.conf
-
-
-    {
-        "allowed_hosts": ["guerrillamail.com","guerrillamailblock.com","sharklasers.com","guerrillamail.net","guerrillamail.org"], // What hosts to accept
-        "pid_file" : "/var/run/go-guerrilla.pid", // pid = process id, so that other programs can send signals to our server
-        "log_file" : "stderr", // can be "off", "stderr", "stdout" or any path to a file
-        "log_level" : "info", // can be  "debug", "info", "error", "warn", "fatal", "panic"
-        "backend_name": "guerrilla-db-redis", // what backend to use for saving email. See /backends dir
-        "backend_config" :
-            {
-                "mysql_db":"gmail_mail",
-                "mysql_host":"127.0.0.1:3306",
-                "mysql_pass":"ok",
-                "mysql_user":"root",
-                "mail_table":"new_mail",
-                "redis_interface" : "127.0.0.1:6379",
-                "redis_expire_seconds" : 7200,
-                "save_workers_size" : 3,
-                "primary_mail_host":"sharklasers.com"
-            },
-        "dashboard": {
-            "is_enable": true,
-            "listen_interface": ":8080", // Where the dashboard will be accessible
-            "tick_interval": "5s", // Interval at which data is measured, parseable by time.ParseDuration
-            "max_window": "24h", // Maximum interval to keep data
-            "ranking_aggregation_interval": "6h" // Aggregation granularity of rankings
-        },
-        "servers" : [ // the following is an array of objects, each object represents a new server that will be spawned
-            {
-                "is_enabled" : true, // boolean
-                "host_name":"mail.test.com", // the hostname of the server as set by MX record
-                "max_size": 1000000, // maximum size of an email in bytes
-                "private_key_file":"/path/to/pem/file/test.com.key",  // full path to pem file private key
-                "public_key_file":"/path/to/pem/file/test.com.crt", // full path to pem file certificate
-                "timeout":180, // timeout in number of seconds before an idle connection is closed
-                "listen_interface":"127.0.0.1:25", // listen on ip and port
-                "start_tls_on":true, // supports the STARTTLS command?
-                "tls_always_on":false, // always connect using TLS? If true, start_tls_on will be false
-                "max_clients": 1000, // max clients at one time
-                "log_file":"/dev/stdout" // optional. Can be "off", "stderr", "stdout" or any path to a file. Will use global setting of empty.
-            },
-            // the following is a second server, but listening on port 465 and always using TLS
-            {
-                "is_enabled" : true,
-                "host_name":"mail.test.com",
-                "max_size":1000000,
-                "private_key_file":"/path/to/pem/file/test.com.key",
-                "public_key_file":"/path/to/pem/file/test.com.crt",
-                "timeout":180,
-                "listen_interface":"127.0.0.1:465",
-                "start_tls_on":false,
-                "tls_always_on":true,
-                "max_clients":500
-            }
-            // repeat as many servers as you need
-        ]
-    }
-    }
-
-The Json parser is very strict on syntax. If there's a parse error and it
-doesn't give much clue, then test your syntax here:
-http://jsonlint.com/#
 
 
 Email Processing Backend
 Email Processing Backend
 =====================
 =====================
 
 
-The main job of a go-guerrilla backend is to validate recipients and deliver emails. The term
+The main job of a Go-Guerrilla backend is to validate recipients and deliver emails. The term
 "delivery" is often synonymous with saving email to secondary storage.
 "delivery" is often synonymous with saving email to secondary storage.
 
 
-The default backend implementation manages multiple workers. These workers are composed of
+The default backend implementation manages multiple workers. These workers are composed of 
 smaller components called "Processors" which are chained using the config to perform a series of steps.
 smaller components called "Processors" which are chained using the config to perform a series of steps.
 Each processor specifies a distinct feature of behaviour. For example, a processor may save
 Each processor specifies a distinct feature of behaviour. For example, a processor may save
-the emails to a particular storage system such as MySQL, or it may add additional headers before
+the emails to a particular storage system such as MySQL, or it may add additional headers before 
 passing the email to the next _processor_.
 passing the email to the next _processor_.
 
 
 To extend or add a new feature, one would write a new Processor, then add it to the config.
 To extend or add a new feature, one would write a new Processor, then add it to the config.
 There are a few default _processors_ to get you started.
 There are a few default _processors_ to get you started.
 
 
-### Documentation
-
-See the full documentation here:
-[About Backends: introduction, configuration, extending](https://github.com/flashmob/go-guerrilla/wiki/About-Backends:-introduction,-configuring-and-extending)
 
 
 ### Included Processors
 ### Included Processors
 
 
@@ -346,183 +237,56 @@ See the full documentation here:
 |Redis|Saves the email data to Redis.|
 |Redis|Saves the email data to Redis.|
 |GuerrillaDbRedis|A 'monolithic' processor used at Guerrilla Mail; included for example
 |GuerrillaDbRedis|A 'monolithic' processor used at Guerrilla Mail; included for example
 
 
-### External Processors
+### Available Processors
+
+The following processors can be imported to your project, then use the
+[Daemon.AddProcessor](https://github.com/flashmob/go-guerrilla/wiki/Using-as-a-package#registering-a-processor) function to register, then add to your config.
 
 
 | Processor | Description |
 | Processor | Description |
 |-----------|-------------|
 |-----------|-------------|
 |[MailDir](https://github.com/flashmob/maildir-processor)|Save emails to a maildir. [MailDiranasaurus](https://github.com/flashmob/maildiranasaurus) is an example project|
 |[MailDir](https://github.com/flashmob/maildir-processor)|Save emails to a maildir. [MailDiranasaurus](https://github.com/flashmob/maildiranasaurus) is an example project|
-|[FastCgi](https://github.com/flashmob/fastcgi-processor)|Deliver email directly to PHP-FPM or a similar FastCGI backend.
+|[FastCGI](https://github.com/flashmob/fastcgi-processor)|Deliver email directly to PHP-FPM or a similar FastCGI backend.
 
 
 Have a processor that you would like to share? Submit a PR to add it to the list!
 Have a processor that you would like to share? Submit a PR to add it to the list!
 
 
-Web Dashboard
-=============
-
-An optional web-based dashboard is built into Go-Guerrilla. To use it, set the dashboard options in the config file, as shown in the Configuration section.
-
 Releases
 Releases
 ========
 ========
 
 
-(Master branch - Release Candidate 1 for v2.0)
-Large refactoring of the code. 
-- Introduced "backends": modular architecture for saving email
-- Issue: Use as a package in your own projects! https://github.com/flashmob/go-guerrilla/issues/20
-- Issue: Do not include dot-suffix in emails https://github.com/flashmob/go-guerrilla/issues/24
-- 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)
-- Small optimizations to the way email is saved
-
-1.5 - 2nd Nov 2016
-- Fixed a DoS vulnerability, stop reading after an input limit is reached
-- Fixed syntax error in Json goguerrilla.conf.sample
-- Do not load certificates if SSL is not enabled
-- check database back-end connections before starting
-
-1.4 - 25th Oct 2016
-- New Feature: multiple servers!
-- Changed the configuration file format to support multiple servers,
-this means that a new configuration file would need to be created form the
-sample (goguerrilla.conf.sample)
-- Organised code into separate files. Config is now strongly typed, etc
-- Deprecated nginx proxy support
-
-
-1.3 14th July 2016
-- Number of saveMail workers added to config (GM_SAVE_WORKERS)
-- convenience function for reading int values form config
-- advertise PIPELINING
-- added HELP command
-- rcpt to host validation: now case insensitive and done earlier (after DATA)
-- iconv switched to: go get gopkg.in/iconv.v1
-
-1.2 1st July 2016
-- Reload config on SIGHUP
-- Write current process id (pid) to a file, /var/run/go-guerrilla.pid by default
-
-
-Using Nginx as a proxy
-======================
-Note: This release temporarily does not have proxy support.
-An issue has been opened to put back in https://github.com/flashmob/go-guerrilla/issues/30
-Nginx can be used to proxy SMTP traffic for GoGuerrilla SMTPd
-
-Why proxy SMTP with Nginx?
-
- *	Terminate TLS connections: (eg. Early Golang versions were not there yet when it came to TLS.)
- OpenSSL on the other hand, used in Nginx, has a complete implementation of TLS with familiar configuration.
- *	Nginx could be used for load balancing and authentication
-
- 1.	Compile nginx with --with-mail --with-mail_ssl_module (most current nginx packages have this compiled already)
-
- 2.	Configuration:
-
-
-		mail {
-	        server {
-	                listen  15.29.8.163:25;
-	                protocol smtp;
-	                server_name  ak47.example.com;
-	                auth_http smtpauth.local:80/auth.txt;
-	                smtp_auth none;
-	                timeout 30000;
-	                smtp_capabilities "SIZE 15728640";
-
-	                # ssl default off. Leave off if starttls is on
-	                #ssl                  on;
-	                ssl_certificate      /etc/ssl/certs/ssl-cert-snakeoil.pem;
-	                ssl_certificate_key  /etc/ssl/private/ssl-cert-snakeoil.key;
-	                ssl_session_timeout  5m;
-	                # See https://mozilla.github.io/server-side-tls/ssl-config-generator/ Intermediate settings
-	                ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
-	                ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
-	                ssl_prefer_server_ciphers on;
-	                # TLS off unless client issues STARTTLS command
-	                starttls on;
-	                proxy on;
-	        }
-		}
+Current release: 1.5.1 - 4th Nov 2016
 
 
-		http {
+Next Planned release: 2.0.0 - TBA
 
 
-		    # Add somewhere inside your http block..
-		    # make sure that you have added smtpauth.local to /etc/hosts
-		    # What this block does is tell the above stmp server to connect
-		    # to our golang server configured to run on 127.0.0.1:2525
+See our [change log](https://github.com/flashmob/go-guerrilla/wiki/Change-Log) for change and release history
 
 
-		    server {
-                    listen 15.29.8.163:80;
-                    server_name 15.29.8.163 smtpauth.local;
-                    root /home/user/http/auth/;
-                    access_log off;
-                    location /auth.txt {
-                        add_header Auth-Status OK;
-                        # where to find your smtp server?
-                        add_header Auth-Server 127.0.0.1;
-                        add_header Auth-Port 2525;
-                    }
-
-                }
-
-		}
-
-
-
-
-Starting / Command Line usage
-==========================================================
-
-All command line arguments are optional
-
-	-config="goguerrilla.conf": Path to the configuration file
-	 -if="": Interface and port to listen on, eg. 127.0.0.1:2525
-	 -v="n": Verbose, [y | n]
-
-Starting from the command line (example)
-
-	/usr/bin/nohup /home/mike/goguerrilla -config=/home/mike/goguerrilla.conf 2>&1 &
-
-This will place goguerrilla in the background and continue running
-
-You may also put another process to watch your goguerrilla process and re-start it
-if something goes wrong.
-
-Testing STARTTLS
-
-Use openssl:
-
-    $ openssl s_client -starttls smtp -crlf -connect 127.0.0.1:2526
 
 
+Using Nginx as a proxy
+======================
 
 
-Benchmarking:
-==========================================================
+For such purposes as load balancing, terminating TLS early,
+ or supporting SSL versions not supported by Go (highly not recommenced if you
+ want to use older SSL versions), 
+ it is possible to [use NGINX as a proxy](https://github.com/flashmob/go-guerrilla/wiki/Using-Nginx-as-a-proxy).
 
 
-https://web.archive.org/web/20110725141905/http://www.jrh.org/smtp/index.html
 
 
-Test 500 clients:
-$ time smtp-source -c -l 5000 -t [email protected] -s 500 -m 5000 5.9.7.183
 
 
-Authors
+Credits
 =======
 =======
 
 
-Project Lead:
+Project Lead: 
 -------------
 -------------
 Flashmob, GuerrillaMail.com, Contact: [email protected]
 Flashmob, GuerrillaMail.com, Contact: [email protected]
 
 
-Major Contributors:
+Major Contributors: 
 -------------------
 -------------------
 
 
 * Reza Mohammadi https://github.com/remohammadi
 * Reza Mohammadi https://github.com/remohammadi
-* Jordan Schalm https://github.com/jordanschalm
+* Jordan Schalm https://github.com/jordanschalm 
+* Philipp Resch https://github.com/dapaxx
 
 
 Thanks to:
 Thanks to:
 ----------
 ----------
 * https://github.com/dvcrn
 * https://github.com/dvcrn
 * https://github.com/athoune
 * https://github.com/athoune
+* https://github.com/Xeoncross
 
 
 ... and anyone else who opened an issue / sent a PR / gave suggestions!
 ... and anyone else who opened an issue / sent a PR / gave suggestions!

+ 68 - 34
api.go

@@ -10,14 +10,23 @@ import (
 	"time"
 	"time"
 )
 )
 
 
+// Daemon provides a convenient API when using go-guerrilla as a package in your Go project.
+// Is's facade for Guerrilla, AppConfig, backends.Backend and log.Logger
 type Daemon struct {
 type Daemon struct {
 	Config  *AppConfig
 	Config  *AppConfig
 	Logger  log.Logger
 	Logger  log.Logger
 	Backend backends.Backend
 	Backend backends.Backend
 
 
+	// Guerrilla will be managed through the API
 	g Guerrilla
 	g Guerrilla
 
 
 	configLoadTime time.Time
 	configLoadTime time.Time
+	subs           []deferredSub
+}
+
+type deferredSub struct {
+	topic Event
+	fn    interface{}
 }
 }
 
 
 const defaultInterface = "127.0.0.1:2525"
 const defaultInterface = "127.0.0.1:2525"
@@ -39,11 +48,10 @@ func (d *Daemon) Start() (err error) {
 			return err
 			return err
 		}
 		}
 		if d.Logger == nil {
 		if d.Logger == nil {
-			d.Logger, err = log.GetLogger(d.Config.LogFile)
+			d.Logger, err = log.GetLogger(d.Config.LogFile, d.Config.LogLevel)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
-			d.Logger.SetLevel(d.Config.LogLevel)
 		}
 		}
 		if d.Backend == nil {
 		if d.Backend == nil {
 			d.Backend, err = backends.New(d.Config.BackendConfig, d.Logger)
 			d.Backend, err = backends.New(d.Config.BackendConfig, d.Logger)
@@ -55,6 +63,11 @@ func (d *Daemon) Start() (err error) {
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		for i := range d.subs {
+			d.Subscribe(d.subs[i].topic, d.subs[i].fn)
+
+		}
+		d.subs = make([]deferredSub, 0)
 	}
 	}
 	err = d.g.Start()
 	err = d.g.Start()
 	if err == nil {
 	if err == nil {
@@ -75,41 +88,42 @@ func (d *Daemon) Shutdown() {
 }
 }
 
 
 // LoadConfig reads in the config from a JSON file.
 // LoadConfig reads in the config from a JSON file.
+// Note: if d.Config is nil, the sets d.Config with the unmarshalled AppConfig which will be returned
 func (d *Daemon) LoadConfig(path string) (AppConfig, error) {
 func (d *Daemon) LoadConfig(path string) (AppConfig, error) {
+	var ac AppConfig
 	data, err := ioutil.ReadFile(path)
 	data, err := ioutil.ReadFile(path)
 	if err != nil {
 	if err != nil {
-		return *d.Config, fmt.Errorf("Could not read config file: %s", err.Error())
+		return ac, fmt.Errorf("Could not read config file: %s", err.Error())
 	}
 	}
-	d.Config = &AppConfig{}
-	if err := d.Config.Load(data); err != nil {
-		return *d.Config, err
+	err = ac.Load(data)
+	if err != nil {
+		return ac, err
+	}
+	if d.Config == nil {
+		d.Config = &ac
 	}
 	}
-	d.configLoadTime = time.Now()
-	return *d.Config, nil
+	return ac, nil
 }
 }
 
 
 // SetConfig is same as LoadConfig, except you can pass AppConfig directly
 // SetConfig is same as LoadConfig, except you can pass AppConfig directly
 // does not emit any change events, instead use ReloadConfig after daemon has started
 // does not emit any change events, instead use ReloadConfig after daemon has started
-func (d *Daemon) SetConfig(c *AppConfig) error {
-	// Config.Load takes []byte so we need to serialize
-	data, err := json.Marshal(c)
+func (d *Daemon) SetConfig(c AppConfig) error {
+	// need to call c.Load, thus need to convert the config
+	// d.load takes json bytes, marshal it
+	data, err := json.Marshal(&c)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	// put the data into a fresh d.Config
-	d.Config = &AppConfig{}
-	if err := d.Config.Load(data); err != nil {
+	err = c.Load(data)
+	if err != nil {
 		return err
 		return err
 	}
 	}
-	d.configLoadTime = time.Now()
+	d.Config = &c
 	return nil
 	return nil
 }
 }
 
 
 // Reload a config using the passed in AppConfig and emit config change events
 // Reload a config using the passed in AppConfig and emit config change events
-func (d *Daemon) ReloadConfig(c *AppConfig) error {
-	if d.Config == nil {
-		return errors.New("d.Config nil")
-	}
+func (d *Daemon) ReloadConfig(c AppConfig) error {
 	oldConfig := *d.Config
 	oldConfig := *d.Config
 	err := d.SetConfig(c)
 	err := d.SetConfig(c)
 	if err != nil {
 	if err != nil {
@@ -124,16 +138,13 @@ func (d *Daemon) ReloadConfig(c *AppConfig) error {
 
 
 // Reload a config from a file and emit config change events
 // Reload a config from a file and emit config change events
 func (d *Daemon) ReloadConfigFile(path string) error {
 func (d *Daemon) ReloadConfigFile(path string) error {
-	if d.Config == nil {
-		return errors.New("d.Config nil")
-	}
-	var oldConfig AppConfig
-	oldConfig = *d.Config
-	_, err := d.LoadConfig(path)
+	ac, err := d.LoadConfig(path)
 	if err != nil {
 	if err != nil {
 		d.Log().WithError(err).Error("Error while reloading config from file")
 		d.Log().WithError(err).Error("Error while reloading config from file")
 		return err
 		return err
-	} else {
+	} else if d.Config != nil {
+		oldConfig := *d.Config
+		d.Config = &ac
 		d.Log().Infof("Configuration was reloaded at %s", d.configLoadTime)
 		d.Log().Infof("Configuration was reloaded at %s", d.configLoadTime)
 		d.Config.EmitChangeEvents(&oldConfig, d.g)
 		d.Config.EmitChangeEvents(&oldConfig, d.g)
 	}
 	}
@@ -142,22 +153,42 @@ func (d *Daemon) ReloadConfigFile(path string) error {
 
 
 // ReopenLogs send events to re-opens all log files.
 // ReopenLogs send events to re-opens all log files.
 // Typically, one would call this after rotating logs
 // Typically, one would call this after rotating logs
-func (d *Daemon) ReopenLogs() {
+func (d *Daemon) ReopenLogs() error {
+	if d.Config == nil {
+		return errors.New("d.Config nil")
+	}
 	d.Config.EmitLogReopenEvents(d.g)
 	d.Config.EmitLogReopenEvents(d.g)
+	return nil
 }
 }
 
 
 // Subscribe for subscribing to config change events
 // Subscribe for subscribing to config change events
 func (d *Daemon) Subscribe(topic Event, fn interface{}) error {
 func (d *Daemon) Subscribe(topic Event, fn interface{}) error {
+	if d.g == nil {
+		d.subs = append(d.subs, deferredSub{topic, fn})
+		return nil
+	}
+
 	return d.g.Subscribe(topic, fn)
 	return d.g.Subscribe(topic, fn)
 }
 }
 
 
 // for publishing config change events
 // for publishing config change events
 func (d *Daemon) Publish(topic Event, args ...interface{}) {
 func (d *Daemon) Publish(topic Event, args ...interface{}) {
+	if d.g == nil {
+		return
+	}
 	d.g.Publish(topic, args...)
 	d.g.Publish(topic, args...)
 }
 }
 
 
 // for unsubscribing from config change events
 // for unsubscribing from config change events
 func (d *Daemon) Unsubscribe(topic Event, handler interface{}) error {
 func (d *Daemon) Unsubscribe(topic Event, handler interface{}) error {
+	if d.g == nil {
+		for i := range d.subs {
+			if d.subs[i].topic == topic && d.subs[i].fn == handler {
+				d.subs = append(d.subs[:i], d.subs[i+1:]...)
+			}
+		}
+		return nil
+	}
 	return d.g.Unsubscribe(topic, handler)
 	return d.g.Unsubscribe(topic, handler)
 }
 }
 
 
@@ -168,13 +199,16 @@ func (d *Daemon) Log() log.Logger {
 		return d.Logger
 		return d.Logger
 	}
 	}
 	out := log.OutputStderr.String()
 	out := log.OutputStderr.String()
-	if d.Config != nil && len(d.Config.LogFile) > 0 {
-		out = d.Config.LogFile
-	}
-	l, err := log.GetLogger(out)
-	if err == nil {
-		l.SetLevel("info")
+	level := log.InfoLevel.String()
+	if d.Config != nil {
+		if len(d.Config.LogFile) > 0 {
+			out = d.Config.LogFile
+		}
+		if len(d.Config.LogLevel) > 0 {
+			level = d.Config.LogLevel
+		}
 	}
 	}
+	l, _ := log.GetLogger(out, level)
 	return l
 	return l
 
 
 }
 }
@@ -199,7 +233,7 @@ func (d *Daemon) configureDefaults() error {
 // then attaches to the logs once the config is loaded.
 // then attaches to the logs once the config is loaded.
 // This will propagate down to the servers / backend too.
 // This will propagate down to the servers / backend too.
 func (d *Daemon) resetLogger() error {
 func (d *Daemon) resetLogger() error {
-	l, err := log.GetLogger(d.Config.LogFile)
+	l, err := log.GetLogger(d.Config.LogFile, d.Config.LogLevel)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 8 - 8
api_test.go

@@ -257,13 +257,13 @@ func TestReopenLog(t *testing.T) {
 func TestSetConfig(t *testing.T) {
 func TestSetConfig(t *testing.T) {
 
 
 	os.Truncate("test/testlog", 0)
 	os.Truncate("test/testlog", 0)
-	cfg := &AppConfig{LogFile: "tests/testlog"}
+	cfg := AppConfig{LogFile: "tests/testlog"}
 	sc := ServerConfig{
 	sc := ServerConfig{
 		ListenInterface: "127.0.0.1:2526",
 		ListenInterface: "127.0.0.1:2526",
 		IsEnabled:       true,
 		IsEnabled:       true,
 	}
 	}
 	cfg.Servers = append(cfg.Servers, sc)
 	cfg.Servers = append(cfg.Servers, sc)
-	d := Daemon{Config: cfg}
+	d := Daemon{Config: &cfg}
 
 
 	// lets add a new server
 	// lets add a new server
 	sc.ListenInterface = "127.0.0.1:2527"
 	sc.ListenInterface = "127.0.0.1:2527"
@@ -301,13 +301,13 @@ func TestSetConfig(t *testing.T) {
 func TestSetConfigError(t *testing.T) {
 func TestSetConfigError(t *testing.T) {
 
 
 	os.Truncate("tests/testlog", 0)
 	os.Truncate("tests/testlog", 0)
-	cfg := &AppConfig{LogFile: "tests/testlog"}
+	cfg := AppConfig{LogFile: "tests/testlog"}
 	sc := ServerConfig{
 	sc := ServerConfig{
 		ListenInterface: "127.0.0.1:2526",
 		ListenInterface: "127.0.0.1:2526",
 		IsEnabled:       true,
 		IsEnabled:       true,
 	}
 	}
 	cfg.Servers = append(cfg.Servers, sc)
 	cfg.Servers = append(cfg.Servers, sc)
-	d := Daemon{Config: cfg}
+	d := Daemon{Config: &cfg}
 
 
 	// lets add a new server with bad TLS
 	// lets add a new server with bad TLS
 	sc.ListenInterface = "127.0.0.1:2527"
 	sc.ListenInterface = "127.0.0.1:2527"
@@ -444,7 +444,7 @@ func TestReloadConfig(t *testing.T) {
 	d := Daemon{}
 	d := Daemon{}
 	d.Start()
 	d.Start()
 
 
-	cfg := &AppConfig{
+	cfg := AppConfig{
 		LogFile:      "tests/testlog",
 		LogFile:      "tests/testlog",
 		AllowedHosts: []string{"grr.la"},
 		AllowedHosts: []string{"grr.la"},
 		BackendConfig: backends.BackendConfig{
 		BackendConfig: backends.BackendConfig{
@@ -466,7 +466,7 @@ func TestPubSubAPI(t *testing.T) {
 	d.Start()
 	d.Start()
 
 
 	// new config
 	// new config
-	cfg := &AppConfig{
+	cfg := AppConfig{
 		PidFile:      "tests/pidfilex.pid",
 		PidFile:      "tests/pidfilex.pid",
 		LogFile:      "tests/testlog",
 		LogFile:      "tests/testlog",
 		AllowedHosts: []string{"grr.la"},
 		AllowedHosts: []string{"grr.la"},
@@ -490,7 +490,7 @@ func TestPubSubAPI(t *testing.T) {
 
 
 	d.Unsubscribe(EventConfigPidFile, pidEvHandler)
 	d.Unsubscribe(EventConfigPidFile, pidEvHandler)
 	cfg.PidFile = "tests/pidfile2.pid"
 	cfg.PidFile = "tests/pidfile2.pid"
-	d.Publish(EventConfigPidFile, cfg)
+	d.Publish(EventConfigPidFile, &cfg)
 	d.ReloadConfig(cfg)
 	d.ReloadConfig(cfg)
 
 
 	b, err := ioutil.ReadFile("tests/testlog")
 	b, err := ioutil.ReadFile("tests/testlog")
@@ -510,7 +510,7 @@ func TestAPILog(t *testing.T) {
 	d := Daemon{}
 	d := Daemon{}
 	l := d.Log()
 	l := d.Log()
 	l.Info("logtest1") // to stderr
 	l.Info("logtest1") // to stderr
-	if l.GetLevel() != "info" {
+	if l.GetLevel() != log.InfoLevel.String() {
 		t.Error("Log level does not eq info, it is ", l.GetLevel())
 		t.Error("Log level does not eq info, it is ", l.GetLevel())
 	}
 	}
 	d.Logger = nil
 	d.Logger = nil

+ 1 - 1
backends/backend.go

@@ -145,7 +145,7 @@ func Log() log.Logger {
 	if v, ok := Svc.mainlog.Load().(log.Logger); ok {
 	if v, ok := Svc.mainlog.Load().(log.Logger); ok {
 		return v
 		return v
 	}
 	}
-	l, _ := log.GetLogger(log.OutputStderr.String())
+	l, _ := log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
 	return l
 	return l
 }
 }
 
 

+ 128 - 66
backends/gateway.go

@@ -44,10 +44,10 @@ type GatewayConfig struct {
 	SaveProcess string `json:"save_process,omitempty"`
 	SaveProcess string `json:"save_process,omitempty"`
 	// ValidateProcess is like ProcessorStack, but for recipient validation tasks
 	// ValidateProcess is like ProcessorStack, but for recipient validation tasks
 	ValidateProcess string `json:"validate_process,omitempty"`
 	ValidateProcess string `json:"validate_process,omitempty"`
-	// TimeoutSave is the number of seconds before timeout when saving an email
-	TimeoutSave int `json:"gw_save_timeout,omitempty"`
-	// TimeoutValidateRcpt is how many seconds before timeout when validating a recipient
-	TimeoutValidateRcpt int `json:"gw_val_rcpt_timeout,omitempty"`
+	// TimeoutSave is duration before timeout when saving an email, eg "29s"
+	TimeoutSave string `json:"gw_save_timeout,omitempty"`
+	// TimeoutValidateRcpt duration before timeout when validating a recipient, eg "1s"
+	TimeoutValidateRcpt string `json:"gw_val_rcpt_timeout,omitempty"`
 }
 }
 
 
 // workerMsg is what get placed on the BackendGateway.saveMailChan channel
 // workerMsg is what get placed on the BackendGateway.saveMailChan channel
@@ -109,18 +109,37 @@ func New(backendConfig BackendConfig, l log.Logger) (Backend, error) {
 	return b, nil
 	return b, nil
 }
 }
 
 
-// Process distributes an envelope to one of the backend workers
+var workerMsgPool = sync.Pool{
+	// if not available, then create a new one
+	New: func() interface{} {
+		return &workerMsg{}
+	},
+}
+
+// reset resets a workerMsg that has been borrowed from the pool
+func (w *workerMsg) reset(e *mail.Envelope, task SelectTask) {
+	if w.notifyMe == nil {
+		w.notifyMe = make(chan *notifyMsg)
+	}
+	w.e = e
+	w.task = task
+}
+
+// Process distributes an envelope to one of the backend workers with a TaskSaveMail task
 func (gw *BackendGateway) Process(e *mail.Envelope) Result {
 func (gw *BackendGateway) Process(e *mail.Envelope) Result {
 	if gw.State != BackendStateRunning {
 	if gw.State != BackendStateRunning {
 		return NewResult(response.Canned.FailBackendNotRunning + gw.State.String())
 		return NewResult(response.Canned.FailBackendNotRunning + gw.State.String())
 	}
 	}
+	// borrow a workerMsg from the pool
+	workerMsg := workerMsgPool.Get().(*workerMsg)
+	workerMsg.reset(e, TaskSaveMail)
 	// place on the channel so that one of the save mail workers can pick it up
 	// place on the channel so that one of the save mail workers can pick it up
-	savedNotify := make(chan *notifyMsg)
-	gw.conveyor <- &workerMsg{e, savedNotify, TaskSaveMail}
+	gw.conveyor <- workerMsg
 	// wait for the save to complete
 	// wait for the save to complete
 	// or timeout
 	// or timeout
 	select {
 	select {
-	case status := <-savedNotify:
+	case status := <-workerMsg.notifyMe:
+		defer workerMsgPool.Put(workerMsg) // can be recycled since we used the notifyMe channel
 		if status.err != nil {
 		if status.err != nil {
 			return NewResult(response.Canned.FailBackendTransaction + status.err.Error())
 			return NewResult(response.Canned.FailBackendTransaction + status.err.Error())
 		}
 		}
@@ -138,13 +157,18 @@ func (gw *BackendGateway) ValidateRcpt(e *mail.Envelope) RcptError {
 	if gw.State != BackendStateRunning {
 	if gw.State != BackendStateRunning {
 		return StorageNotAvailable
 		return StorageNotAvailable
 	}
 	}
+	if _, ok := gw.validators[0].(NoopProcessor); ok {
+		// no validator processors configured
+		return nil
+	}
 	// place on the channel so that one of the save mail workers can pick it up
 	// place on the channel so that one of the save mail workers can pick it up
-	notify := make(chan *notifyMsg)
-	gw.conveyor <- &workerMsg{e, notify, TaskValidateRcpt}
+	workerMsg := workerMsgPool.Get().(*workerMsg)
+	workerMsg.reset(e, TaskValidateRcpt)
+	gw.conveyor <- workerMsg
 	// wait for the validation to complete
 	// wait for the validation to complete
 	// or timeout
 	// or timeout
 	select {
 	select {
-	case status := <-notify:
+	case status := <-workerMsg.notifyMe:
 		if status.err != nil {
 		if status.err != nil {
 			return status.err
 			return status.err
 		}
 		}
@@ -179,7 +203,7 @@ func (gw *BackendGateway) Reinitialize() error {
 	if gw.State != BackendStateShuttered {
 	if gw.State != BackendStateShuttered {
 		return errors.New("backend must be in BackendStateshuttered state to Reinitialize")
 		return errors.New("backend must be in BackendStateshuttered state to Reinitialize")
 	}
 	}
-	//
+	// clear the Initializers and Shutdowners
 	Svc.reset()
 	Svc.reset()
 
 
 	err := gw.Initialize(gw.config)
 	err := gw.Initialize(gw.config)
@@ -191,7 +215,7 @@ func (gw *BackendGateway) Reinitialize() error {
 	return err
 	return err
 }
 }
 
 
-// newChain creates a new Processor by chaining multiple Processors in a call stack
+// newStack creates a new Processor by chaining multiple Processors in a call stack
 // Decorators are functions of Decorator type, source files prefixed with p_*
 // Decorators are functions of Decorator type, source files prefixed with p_*
 // Each decorator does a specific task during the processing stage.
 // Each decorator does a specific task during the processing stage.
 // This function uses the config value save_process or validate_process to figure out which Decorator to use
 // This function uses the config value save_process or validate_process to figure out which Decorator to use
@@ -199,7 +223,8 @@ func (gw *BackendGateway) newStack(stackConfig string) (Processor, error) {
 	var decorators []Decorator
 	var decorators []Decorator
 	cfg := strings.ToLower(strings.TrimSpace(stackConfig))
 	cfg := strings.ToLower(strings.TrimSpace(stackConfig))
 	if len(cfg) == 0 {
 	if len(cfg) == 0 {
-		cfg = strings.ToLower(defaultProcessor)
+		//cfg = strings.ToLower(defaultProcessor)
+		return NoopProcessor{}, nil
 	}
 	}
 	items := strings.Split(cfg, "|")
 	items := strings.Split(cfg, "|")
 	for i := range items {
 	for i := range items {
@@ -238,43 +263,43 @@ func (gw *BackendGateway) Initialize(cfg BackendConfig) error {
 		return errors.New("Can only Initialize in BackendStateNew or BackendStateShuttered state")
 		return errors.New("Can only Initialize in BackendStateNew or BackendStateShuttered state")
 	}
 	}
 	err := gw.loadConfig(cfg)
 	err := gw.loadConfig(cfg)
-	if err == nil {
-		workersSize := gw.workersSize()
-		if workersSize < 1 {
+	if err != nil {
+		gw.State = BackendStateError
+		return err
+	}
+	workersSize := gw.workersSize()
+	if workersSize < 1 {
+		gw.State = BackendStateError
+		return errors.New("Must have at least 1 worker")
+	}
+	gw.processors = make([]Processor, 0)
+	gw.validators = make([]Processor, 0)
+	for i := 0; i < workersSize; i++ {
+		p, err := gw.newStack(gw.gwConfig.SaveProcess)
+		if err != nil {
 			gw.State = BackendStateError
 			gw.State = BackendStateError
-			return errors.New("Must have at least 1 worker")
+			return err
 		}
 		}
-		gw.processors = make([]Processor, 0)
-		gw.validators = make([]Processor, 0)
-		for i := 0; i < workersSize; i++ {
-			p, err := gw.newStack(gw.gwConfig.SaveProcess)
-			if err != nil {
-				gw.State = BackendStateError
-				return err
-			}
-			gw.processors = append(gw.processors, p)
+		gw.processors = append(gw.processors, p)
 
 
-			v, err := gw.newStack(gw.gwConfig.ValidateProcess)
-			if err != nil {
-				gw.State = BackendStateError
-				return err
-			}
-			gw.validators = append(gw.validators, v)
-		}
-		// initialize processors
-		if err := Svc.initialize(cfg); err != nil {
+		v, err := gw.newStack(gw.gwConfig.ValidateProcess)
+		if err != nil {
 			gw.State = BackendStateError
 			gw.State = BackendStateError
 			return err
 			return err
 		}
 		}
-		if gw.conveyor == nil {
-			gw.conveyor = make(chan *workerMsg, workersSize)
-		}
-		// ready to start
-		gw.State = BackendStateInitialized
-		return nil
+		gw.validators = append(gw.validators, v)
 	}
 	}
-	gw.State = BackendStateError
-	return err
+	// initialize processors
+	if err := Svc.initialize(cfg); err != nil {
+		gw.State = BackendStateError
+		return err
+	}
+	if gw.conveyor == nil {
+		gw.conveyor = make(chan *workerMsg, workersSize)
+	}
+	// ready to start
+	gw.State = BackendStateInitialized
+	return nil
 }
 }
 
 
 // Start starts the worker goroutines, assuming it has been initialized or shuttered before
 // Start starts the worker goroutines, assuming it has been initialized or shuttered before
@@ -293,12 +318,18 @@ func (gw *BackendGateway) Start() error {
 			stop := make(chan bool)
 			stop := make(chan bool)
 			go func(workerId int, stop chan bool) {
 			go func(workerId int, stop chan bool) {
 				// blocks here until the worker exits
 				// blocks here until the worker exits
-				gw.workDispatcher(
-					gw.conveyor,
-					gw.processors[workerId],
-					gw.validators[workerId],
-					workerId+1,
-					stop)
+				for {
+					state := gw.workDispatcher(
+						gw.conveyor,
+						gw.processors[workerId],
+						gw.validators[workerId],
+						workerId+1,
+						stop)
+					// keep running after panic
+					if state != dispatcherStatePanic {
+						break
+					}
+				}
 				gw.wg.Done()
 				gw.wg.Done()
 			}(i, stop)
 			}(i, stop)
 			gw.workStoppers = append(gw.workStoppers, stop)
 			gw.workStoppers = append(gw.workStoppers, stop)
@@ -313,7 +344,7 @@ func (gw *BackendGateway) Start() error {
 // workersSize gets the number of workers to use for saving email by reading the save_workers_size config value
 // workersSize gets the number of workers to use for saving email by reading the save_workers_size config value
 // Returns 1 if no config value was set
 // Returns 1 if no config value was set
 func (gw *BackendGateway) workersSize() int {
 func (gw *BackendGateway) workersSize() int {
-	if gw.gwConfig.WorkersSize == 0 {
+	if gw.gwConfig.WorkersSize <= 0 {
 		return 1
 		return 1
 	}
 	}
 	return gw.gwConfig.WorkersSize
 	return gw.gwConfig.WorkersSize
@@ -321,52 +352,81 @@ func (gw *BackendGateway) workersSize() int {
 
 
 // saveTimeout returns the maximum amount of seconds to wait before timing out a save processing task
 // saveTimeout returns the maximum amount of seconds to wait before timing out a save processing task
 func (gw *BackendGateway) saveTimeout() time.Duration {
 func (gw *BackendGateway) saveTimeout() time.Duration {
-	if gw.gwConfig.TimeoutSave == 0 {
+	if gw.gwConfig.TimeoutSave == "" {
+		return saveTimeout
+	}
+	t, err := time.ParseDuration(gw.gwConfig.TimeoutSave)
+	if err != nil {
 		return saveTimeout
 		return saveTimeout
 	}
 	}
-	return time.Duration(gw.gwConfig.TimeoutSave)
+	return t
 }
 }
 
 
 // validateRcptTimeout returns the maximum amount of seconds to wait before timing out a recipient validation  task
 // validateRcptTimeout returns the maximum amount of seconds to wait before timing out a recipient validation  task
 func (gw *BackendGateway) validateRcptTimeout() time.Duration {
 func (gw *BackendGateway) validateRcptTimeout() time.Duration {
-	if gw.gwConfig.TimeoutValidateRcpt == 0 {
+	if gw.gwConfig.TimeoutValidateRcpt == "" {
+		return validateRcptTimeout
+	}
+	t, err := time.ParseDuration(gw.gwConfig.TimeoutValidateRcpt)
+	if err != nil {
 		return validateRcptTimeout
 		return validateRcptTimeout
 	}
 	}
-	return time.Duration(gw.gwConfig.TimeoutValidateRcpt)
+	return t
 }
 }
 
 
+type dispatcherState int
+
+const (
+	dispatcherStateStopped dispatcherState = iota
+	dispatcherStateIdle
+	dispatcherStateWorking
+	dispatcherStateNotify
+	dispatcherStatePanic
+)
+
 func (gw *BackendGateway) workDispatcher(
 func (gw *BackendGateway) workDispatcher(
 	workIn chan *workerMsg,
 	workIn chan *workerMsg,
 	save Processor,
 	save Processor,
 	validate Processor,
 	validate Processor,
 	workerId int,
 	workerId int,
-	stop chan bool) {
+	stop chan bool) (state dispatcherState) {
+
+	var msg *workerMsg
 
 
 	defer func() {
 	defer func() {
+
+		// panic recovery mechanism: it may panic when processing
+		// since processors may call arbitrary code, some may be 3rd party / unstable
+		// we need to detect the panic, and notify the backend that it failed & unlock the envelope
 		if r := recover(); r != nil {
 		if r := recover(); r != nil {
-			// recover form closed channel
-			Log().Error("worker recovered form panic:", r, string(debug.Stack()))
+			Log().Error("worker recovered from panic:", r, string(debug.Stack()))
+
+			if state == dispatcherStateWorking {
+				msg.notifyMe <- &notifyMsg{err: errors.New("storage failed")}
+				msg.e.Unlock()
+			}
+			state = dispatcherStatePanic
+			return
 		}
 		}
-		// close any connections / files
-		Svc.shutdown()
+		// state is dispatcherStateStopped if it reached here
+		return
 
 
 	}()
 	}()
+	state = dispatcherStateIdle
 	Log().Infof("processing worker started (#%d)", workerId)
 	Log().Infof("processing worker started (#%d)", workerId)
 	for {
 	for {
 		select {
 		select {
 		case <-stop:
 		case <-stop:
+			state = dispatcherStateStopped
 			Log().Infof("stop signal for worker (#%d)", workerId)
 			Log().Infof("stop signal for worker (#%d)", workerId)
 			return
 			return
-		case msg := <-workIn:
-			if msg == nil {
-				Log().Debugf("worker stopped (#%d)", workerId)
-				return
-			}
+		case msg = <-workIn:
 			msg.e.Lock()
 			msg.e.Lock()
+			state = dispatcherStateWorking
 			if msg.task == TaskSaveMail {
 			if msg.task == TaskSaveMail {
 				// process the email here
 				// process the email here
-				// TODO we should check the err
 				result, _ := save.Process(msg.e, TaskSaveMail)
 				result, _ := save.Process(msg.e, TaskSaveMail)
+				state = dispatcherStateNotify
 				if result.Code() < 300 {
 				if result.Code() < 300 {
 					// if all good, let the gateway know that it was queued
 					// if all good, let the gateway know that it was queued
 					msg.notifyMe <- &notifyMsg{nil, msg.e.QueuedId}
 					msg.notifyMe <- &notifyMsg{nil, msg.e.QueuedId}
@@ -376,6 +436,7 @@ func (gw *BackendGateway) workDispatcher(
 				}
 				}
 			} else if msg.task == TaskValidateRcpt {
 			} else if msg.task == TaskValidateRcpt {
 				_, err := validate.Process(msg.e, TaskValidateRcpt)
 				_, err := validate.Process(msg.e, TaskValidateRcpt)
+				state = dispatcherStateNotify
 				if err != nil {
 				if err != nil {
 					// validation failed
 					// validation failed
 					msg.notifyMe <- &notifyMsg{err: err}
 					msg.notifyMe <- &notifyMsg{err: err}
@@ -386,6 +447,7 @@ func (gw *BackendGateway) workDispatcher(
 			}
 			}
 			msg.e.Unlock()
 			msg.e.Unlock()
 		}
 		}
+		state = dispatcherStateIdle
 	}
 	}
 }
 }
 
 

+ 1 - 1
backends/gateway_test.go

@@ -58,7 +58,7 @@ func TestStartProcessStop(t *testing.T) {
 	gateway := &BackendGateway{}
 	gateway := &BackendGateway{}
 	err := gateway.Initialize(c)
 	err := gateway.Initialize(c)
 
 
-	mainlog, _ := log.GetLogger(log.OutputOff.String())
+	mainlog, _ := log.GetLogger(log.OutputOff.String(), "debug")
 	Svc.SetMainlog(mainlog)
 	Svc.SetMainlog(mainlog)
 
 
 	if err != nil {
 	if err != nil {

+ 106 - 41
backends/p_mysql.go

@@ -2,6 +2,7 @@ package backends
 
 
 import (
 import (
 	"database/sql"
 	"database/sql"
+	"fmt"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -9,6 +10,8 @@ import (
 	"github.com/go-sql-driver/mysql"
 	"github.com/go-sql-driver/mysql"
 
 
 	"github.com/flashmob/go-guerrilla/response"
 	"github.com/flashmob/go-guerrilla/response"
+	"math/big"
+	"net"
 	"runtime/debug"
 	"runtime/debug"
 )
 )
 
 
@@ -43,7 +46,7 @@ const procMySQLReadTimeout = time.Second * 10
 const procMySQLWriteTimeout = time.Second * 10
 const procMySQLWriteTimeout = time.Second * 10
 
 
 type MysqlProcessorConfig struct {
 type MysqlProcessorConfig struct {
-	MysqlTable  string `json:"mail_table"`
+	MysqlTable  string `json:"mysql_mail_table"`
 	MysqlDB     string `json:"mysql_db"`
 	MysqlDB     string `json:"mysql_db"`
 	MysqlHost   string `json:"mysql_host"`
 	MysqlHost   string `json:"mysql_host"`
 	MysqlPass   string `json:"mysql_pass"`
 	MysqlPass   string `json:"mysql_pass"`
@@ -74,9 +77,9 @@ func (m *MysqlProcessor) connect(config *MysqlProcessorConfig) (*sql.DB, error)
 		return nil, err
 		return nil, err
 	}
 	}
 	// do we have permission to access the table?
 	// do we have permission to access the table?
-	_, err = db.Query("SELECT mail_id FROM " + m.config.MysqlTable + "LIMIT 1")
+	_, err = db.Query("SELECT mail_id FROM " + m.config.MysqlTable + " LIMIT 1")
 	if err != nil {
 	if err != nil {
-		Log().Error("cannot select table", err)
+		//Log().Error("cannot select table", err)
 		return nil, err
 		return nil, err
 	}
 	}
 	Log().Info("connected to mysql on tcp ", config.MysqlHost)
 	Log().Info("connected to mysql on tcp ", config.MysqlHost)
@@ -92,9 +95,11 @@ func (g *MysqlProcessor) prepareInsertQuery(rows int, db *sql.DB) *sql.Stmt {
 		return g.cache[rows-1]
 		return g.cache[rows-1]
 	}
 	}
 	sqlstr := "INSERT INTO " + g.config.MysqlTable + " "
 	sqlstr := "INSERT INTO " + g.config.MysqlTable + " "
-	sqlstr += "(`date`, `to`, `from`, `subject`, `body`, `charset`, `mail`, `spam_score`, `hash`, `content_type`, `recipient`, `has_attach`, `ip_addr`, `return_path`, `is_tls`)"
-	sqlstr += " values "
-	values := "(NOW(), ?, ?, ?, ? , 'UTF-8' , ?, 0, ?, '', ?, 0, ?, ?, ?)"
+	sqlstr += "(`date`, `to`, `from`, `subject`, `body`,  `mail`, `spam_score`, "
+	sqlstr += "`hash`, `content_type`, `recipient`, `has_attach`, `ip_addr`, "
+	sqlstr += "`return_path`, `is_tls`, `message_id`, `reply_to`, `sender`)"
+	sqlstr += " VALUES "
+	values := "(NOW(), ?, ?, ?, ? , ?, 0, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)"
 	// add more rows
 	// add more rows
 	comma := ""
 	comma := ""
 	for i := 0; i < rows; i++ {
 	for i := 0; i < rows; i++ {
@@ -112,8 +117,7 @@ func (g *MysqlProcessor) prepareInsertQuery(rows int, db *sql.DB) *sql.Stmt {
 	return stmt
 	return stmt
 }
 }
 
 
-func (g *MysqlProcessor) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *[]interface{}) {
-	var execErr error
+func (g *MysqlProcessor) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *[]interface{}) (execErr error) {
 	defer func() {
 	defer func() {
 		if r := recover(); r != nil {
 		if r := recover(); r != nil {
 			Log().Error("Recovered form panic:", r, string(debug.Stack()))
 			Log().Error("Recovered form panic:", r, string(debug.Stack()))
@@ -133,6 +137,30 @@ func (g *MysqlProcessor) doQuery(c int, db *sql.DB, insertStmt *sql.Stmt, vals *
 	if execErr != nil {
 	if execErr != nil {
 		Log().WithError(execErr).Error("There was a problem the insert")
 		Log().WithError(execErr).Error("There was a problem the insert")
 	}
 	}
+	return
+}
+
+// for storing ip addresses in the ip_addr column
+func (g *MysqlProcessor) ip2bint(ip string) *big.Int {
+	bint := big.NewInt(0)
+	addr := net.ParseIP(ip)
+	if strings.Index(ip, "::") > 0 {
+		bint.SetBytes(addr.To16())
+	} else {
+		bint.SetBytes(addr.To4())
+	}
+	return bint
+}
+
+func (g *MysqlProcessor) fillAddressFromHeader(e *mail.Envelope, headerKey string) string {
+	if v, ok := e.Header[headerKey]; ok {
+		addr, err := mail.NewAddress(v[0])
+		if err != nil {
+			return ""
+		}
+		return addr.String()
+	}
+	return ""
 }
 }
 
 
 func MySql() Decorator {
 func MySql() Decorator {
@@ -140,8 +168,9 @@ func MySql() Decorator {
 	var config *MysqlProcessorConfig
 	var config *MysqlProcessorConfig
 	var vals []interface{}
 	var vals []interface{}
 	var db *sql.DB
 	var db *sql.DB
-	mp := &MysqlProcessor{}
+	m := &MysqlProcessor{}
 
 
+	// open the database connection (it will also check if we can select the table)
 	Svc.AddInitializer(InitializeWith(func(backendConfig BackendConfig) error {
 	Svc.AddInitializer(InitializeWith(func(backendConfig BackendConfig) error {
 		configType := BaseConfig(&MysqlProcessorConfig{})
 		configType := BaseConfig(&MysqlProcessorConfig{})
 		bcfg, err := Svc.ExtractConfig(backendConfig, configType)
 		bcfg, err := Svc.ExtractConfig(backendConfig, configType)
@@ -149,16 +178,15 @@ func MySql() Decorator {
 			return err
 			return err
 		}
 		}
 		config = bcfg.(*MysqlProcessorConfig)
 		config = bcfg.(*MysqlProcessorConfig)
-		mp.config = config
-		db, err = mp.connect(config)
+		m.config = config
+		db, err = m.connect(config)
 		if err != nil {
 		if err != nil {
-			Log().Errorf("cannot open mysql: %s", err)
 			return err
 			return err
 		}
 		}
 		return nil
 		return nil
 	}))
 	}))
 
 
-	// shutdown
+	// shutdown will close the database connection
 	Svc.AddShutdowner(ShutdownWith(func() error {
 	Svc.AddShutdowner(ShutdownWith(func() error {
 		if db != nil {
 		if db != nil {
 			return db.Close()
 			return db.Close()
@@ -171,9 +199,10 @@ func MySql() Decorator {
 
 
 			if task == TaskSaveMail {
 			if task == TaskSaveMail {
 				var to, body string
 				var to, body string
-				to = trimToLimit(strings.TrimSpace(e.RcptTo[0].User)+"@"+config.PrimaryHost, 255)
+
 				hash := ""
 				hash := ""
 				if len(e.Hashes) > 0 {
 				if len(e.Hashes) > 0 {
+					// if saved in redis, hash will be the redis key
 					hash = e.Hashes[0]
 					hash = e.Hashes[0]
 					e.QueuedId = e.Hashes[0]
 					e.QueuedId = e.Hashes[0]
 				}
 				}
@@ -189,33 +218,68 @@ func MySql() Decorator {
 					body = "redis"
 					body = "redis"
 				}
 				}
 
 
-				// build the values for the query
-				vals = []interface{}{} // clear the vals
-				vals = append(vals,
-					to,
-					trimToLimit(e.MailFrom.String(), 255),
-					trimToLimit(e.Subject, 255),
-					body)
-				if body == "redis" {
-					// data already saved in redis
-					vals = append(vals, "")
-				} else if co != nil {
-					// use a compressor (automatically adds e.DeliveryHeader)
-					vals = append(vals, co.String())
-
-				} else {
-					vals = append(vals, e.String())
-				}
+				for i := range e.RcptTo {
 
 
-				vals = append(vals,
-					hash,
-					to,
-					e.RemoteIP,
-					trimToLimit(e.MailFrom.String(), 255),
-					e.TLS)
+					// use the To header, otherwise rcpt to
+					to = trimToLimit(m.fillAddressFromHeader(e, "To"), 255)
+					if to == "" {
+						// trimToLimit(strings.TrimSpace(e.RcptTo[i].User)+"@"+config.PrimaryHost, 255)
+						to = trimToLimit(strings.TrimSpace(e.RcptTo[i].String()), 255)
+					}
+					mid := trimToLimit(m.fillAddressFromHeader(e, "Message-Id"), 255)
+					if mid == "" {
+						mid = fmt.Sprintf("%s.%s@%s", hash, e.RcptTo[i].User, config.PrimaryHost)
+					}
+					// replyTo is the 'Reply-to' header, it may be blank
+					replyTo := trimToLimit(m.fillAddressFromHeader(e, "Reply-To"), 255)
+					// sender is the 'Sender' header, it may be blank
+					sender := trimToLimit(m.fillAddressFromHeader(e, "Sender"), 255)
+
+					recipient := trimToLimit(strings.TrimSpace(e.RcptTo[i].String()), 255)
+					contentType := ""
+					if v, ok := e.Header["Content-Type"]; ok {
+						contentType = trimToLimit(v[0], 255)
+					}
+
+					// build the values for the query
+					vals = []interface{}{} // clear the vals
+					vals = append(vals,
+						to,
+						trimToLimit(e.MailFrom.String(), 255), // from
+						trimToLimit(e.Subject, 255),
+						body, // body describes how to interpret the data, eg 'redis' means stored in redis, and 'gzip' stored in mysql, using gzip compression
+					)
+					// `mail` column
+					if body == "redis" {
+						// data already saved in redis
+						vals = append(vals, "")
+					} else if co != nil {
+						// use a compressor (automatically adds e.DeliveryHeader)
+						vals = append(vals, co.String())
+
+					} else {
+						vals = append(vals, e.String())
+					}
+
+					vals = append(vals,
+						hash, // hash (redis hash if saved in redis)
+						contentType,
+						recipient,
+						m.ip2bint(e.RemoteIP).Bytes(),         // ip_addr store as varbinary(16)
+						trimToLimit(e.MailFrom.String(), 255), // return_path
+						e.TLS,   // is_tls
+						mid,     // message_id
+						replyTo, // reply_to
+						sender,
+					)
+
+					stmt := m.prepareInsertQuery(1, db)
+					err := m.doQuery(1, db, stmt, &vals)
+					if err != nil {
+						return NewResult(fmt.Sprint("554 Error: could not save email")), StorageError
+					}
+				}
 
 
-				stmt := mp.prepareInsertQuery(1, db)
-				mp.doQuery(1, db, stmt, &vals)
 				// continue to the next Processor in the decorator chain
 				// continue to the next Processor in the decorator chain
 				return p.Process(e, task)
 				return p.Process(e, task)
 			} else if task == TaskValidateRcpt {
 			} else if task == TaskValidateRcpt {
@@ -225,10 +289,11 @@ func MySql() Decorator {
 					// validate only the _last_ recipient that was appended
 					// validate only the _last_ recipient that was appended
 					last := e.RcptTo[len(e.RcptTo)-1]
 					last := e.RcptTo[len(e.RcptTo)-1]
 					if len(last.User) > 255 {
 					if len(last.User) > 255 {
-						// TODO what kind of response to send?
-						return NewResult(response.Canned.FailNoSenderDataCmd), NoSuchUser
+						// return with an error
+						return NewResult(response.Canned.FailRcptCmd), NoSuchUser
 					}
 					}
 				}
 				}
+				// continue to the next processor
 				return p.Process(e, task)
 				return p.Process(e, task)
 			} else {
 			} else {
 				return p.Process(e, task)
 				return p.Process(e, task)

+ 10 - 11
backends/p_redis.go

@@ -92,7 +92,6 @@ func Redis() Decorator {
 				if len(e.Hashes) > 0 {
 				if len(e.Hashes) > 0 {
 					e.QueuedId = e.Hashes[0]
 					e.QueuedId = e.Hashes[0]
 					hash = e.Hashes[0]
 					hash = e.Hashes[0]
-
 					var stringer fmt.Stringer
 					var stringer fmt.Stringer
 					// a compressor was set
 					// a compressor was set
 					if c, ok := e.Values["zlib-compressor"]; ok {
 					if c, ok := e.Values["zlib-compressor"]; ok {
@@ -101,22 +100,22 @@ func Redis() Decorator {
 						stringer = e
 						stringer = e
 					}
 					}
 					redisErr = redisClient.redisConnection(config.RedisInterface)
 					redisErr = redisClient.redisConnection(config.RedisInterface)
-
-					if redisErr == nil {
-						_, doErr := redisClient.conn.Do("SETEX", hash, config.RedisExpireSeconds, stringer)
-						if doErr != nil {
-							redisErr = doErr
-						}
-					}
 					if redisErr != nil {
 					if redisErr != nil {
-						Log().WithError(redisErr).Warn("Error while talking to redis")
+						Log().WithError(redisErr).Warn("Error while connecting to redis")
+						result := NewResult(response.Canned.FailBackendTransaction)
+						return result, redisErr
+					}
+					_, doErr := redisClient.conn.Do("SETEX", hash, config.RedisExpireSeconds, stringer)
+					if doErr != nil {
+						Log().WithError(doErr).Warn("Error while SETEX to redis")
 						result := NewResult(response.Canned.FailBackendTransaction)
 						result := NewResult(response.Canned.FailBackendTransaction)
 						return result, redisErr
 						return result, redisErr
-					} else {
-						e.Values["redis"] = "redis" // the backend system will know to look in redis for the message data
 					}
 					}
+					e.Values["redis"] = "redis" // the next processor will know to look in redis for the message data
 				} else {
 				} else {
 					Log().Error("Redis needs a Hash() process before it")
 					Log().Error("Redis needs a Hash() process before it")
+					result := NewResult(response.Canned.FailBackendTransaction)
+					return result, StorageError
 				}
 				}
 
 
 				return p.Process(e, task)
 				return p.Process(e, task)

+ 3 - 0
backends/processor.go

@@ -46,3 +46,6 @@ type DefaultProcessor struct{}
 func (w DefaultProcessor) Process(e *mail.Envelope, task SelectTask) (Result, error) {
 func (w DefaultProcessor) Process(e *mail.Envelope, task SelectTask) (Result, error) {
 	return BackendResultOK, nil
 	return BackendResultOK, nil
 }
 }
+
+// if no processors specified, skip operation
+type NoopProcessor struct{ DefaultProcessor }

+ 1 - 1
backends/util.go

@@ -15,6 +15,7 @@ import (
 // Accounts for folding headers.
 // Accounts for folding headers.
 var headerRegex, _ = regexp.Compile(`^([\S ]+):([\S ]+(?:\r\n\s[\S ]+)?)`)
 var headerRegex, _ = regexp.Compile(`^([\S ]+):([\S ]+(?:\r\n\s[\S ]+)?)`)
 
 
+// ParseHeaders is deprecated, see mail.Envelope.ParseHeaders instead
 func ParseHeaders(mailData string) map[string]string {
 func ParseHeaders(mailData string) map[string]string {
 	var headerSectionEnds int
 	var headerSectionEnds int
 	for i, char := range mailData[:len(mailData)-4] {
 	for i, char := range mailData[:len(mailData)-4] {
@@ -25,7 +26,6 @@ func ParseHeaders(mailData string) map[string]string {
 		}
 		}
 	}
 	}
 	headers := make(map[string]string)
 	headers := make(map[string]string)
-	// TODO header comments and textproto Reader instead of regex
 	matches := headerRegex.FindAllStringSubmatch(mailData[:headerSectionEnds], -1)
 	matches := headerRegex.FindAllStringSubmatch(mailData[:headerSectionEnds], -1)
 	for _, h := range matches {
 	for _, h := range matches {
 		name := textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(strings.Replace(h[1], "\r\n", "", -1)))
 		name := textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(strings.Replace(h[1], "\r\n", "", -1)))

+ 3 - 2
backends/validate.go

@@ -9,8 +9,9 @@ type RcptError error
 var (
 var (
 	NoSuchUser          = RcptError(errors.New("no such user"))
 	NoSuchUser          = RcptError(errors.New("no such user"))
 	StorageNotAvailable = RcptError(errors.New("storage not available"))
 	StorageNotAvailable = RcptError(errors.New("storage not available"))
-	StorageTooBusy      = RcptError(errors.New("stoarge too busy"))
-	StorageTimeout      = RcptError(errors.New("stoarge timeout"))
+	StorageTooBusy      = RcptError(errors.New("storage too busy"))
+	StorageTimeout      = RcptError(errors.New("storage timeout"))
 	QuotaExceeded       = RcptError(errors.New("quota exceeded"))
 	QuotaExceeded       = RcptError(errors.New("quota exceeded"))
 	UserSuspended       = RcptError(errors.New("user suspended"))
 	UserSuspended       = RcptError(errors.New("user suspended"))
+	StorageError        = RcptError(errors.New("storage error"))
 )
 )

+ 2 - 3
client.go

@@ -5,13 +5,12 @@ import (
 	"bytes"
 	"bytes"
 	"crypto/tls"
 	"crypto/tls"
 	"fmt"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/mail"
 	"net"
 	"net"
 	"net/textproto"
 	"net/textproto"
 	"sync"
 	"sync"
 	"time"
 	"time"
-
-	"github.com/flashmob/go-guerrilla/log"
-	"github.com/flashmob/go-guerrilla/mail"
 )
 )
 
 
 // ClientState indicates which part of the SMTP transaction a given client is in.
 // ClientState indicates which part of the SMTP transaction a given client is in.

+ 2 - 2
cmd/guerrillad/root.go

@@ -7,8 +7,8 @@ import (
 
 
 var rootCmd = &cobra.Command{
 var rootCmd = &cobra.Command{
 	Use:   "guerrillad",
 	Use:   "guerrillad",
-	Short: "small SMTP server",
-	Long: `It's a small SMTP server written in Go, for the purpose of receiving large volumes of email.
+	Short: "small SMTP daemon",
+	Long: `It's a small SMTP daemon written in Go, for the purpose of receiving large volumes of email.
 Written for GuerrillaMail.com which processes tens of thousands of emails every hour.`,
 Written for GuerrillaMail.com which processes tens of thousands of emails every hour.`,
 	Run: nil,
 	Run: nil,
 }
 }

+ 26 - 42
cmd/guerrillad/serve.go

@@ -1,8 +1,10 @@
 package main
 package main
 
 
 import (
 import (
-	"encoding/json"
 	"fmt"
 	"fmt"
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/spf13/cobra"
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"os/signal"
 	"os/signal"
@@ -10,10 +12,6 @@ import (
 	"strings"
 	"strings"
 	"syscall"
 	"syscall"
 	"time"
 	"time"
-
-	"github.com/flashmob/go-guerrilla"
-	"github.com/flashmob/go-guerrilla/log"
-	"github.com/spf13/cobra"
 )
 )
 
 
 const (
 const (
@@ -26,11 +24,10 @@ var (
 
 
 	serveCmd = &cobra.Command{
 	serveCmd = &cobra.Command{
 		Use:   "serve",
 		Use:   "serve",
-		Short: "start the small SMTP server",
+		Short: "start the daemon and start all available servers",
 		Run:   serve,
 		Run:   serve,
 	}
 	}
 
 
-	cmdConfig     = CmdConfig{}
 	signalChannel = make(chan os.Signal, 1) // for trapping SIGHUP and friends
 	signalChannel = make(chan os.Signal, 1) // for trapping SIGHUP and friends
 	mainlog       log.Logger
 	mainlog       log.Logger
 
 
@@ -40,12 +37,16 @@ var (
 func init() {
 func init() {
 	// log to stderr on startup
 	// log to stderr on startup
 	var err error
 	var err error
-	mainlog, err = log.GetLogger(log.OutputStderr.String())
+	mainlog, err = log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
 	if err != nil {
 	if err != nil {
 		mainlog.WithError(err).Errorf("Failed creating a logger to %s", log.OutputStderr)
 		mainlog.WithError(err).Errorf("Failed creating a logger to %s", log.OutputStderr)
 	}
 	}
+	cfgFile := "goguerrilla.conf" // deprecated default name
+	if _, err := os.Stat(cfgFile); err != nil {
+		cfgFile = "goguerrilla.conf.json" // use the new name
+	}
 	serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
 	serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
-		"goguerrilla.conf", "Path to the configuration file")
+		cfgFile, "Path to the configuration file")
 	// intentionally didn't specify default pidFile; value from config is used if flag is empty
 	// intentionally didn't specify default pidFile; value from config is used if flag is empty
 	serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
 	serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
 		"", "Path to the pid file")
 		"", "Path to the pid file")
@@ -63,7 +64,11 @@ func sigHandler() {
 	)
 	)
 	for sig := range signalChannel {
 	for sig := range signalChannel {
 		if sig == syscall.SIGHUP {
 		if sig == syscall.SIGHUP {
-			d.ReloadConfigFile(configPath)
+			if ac, err := readConfig(configPath, pidFile); err == nil {
+				d.ReloadConfig(*ac)
+			} else {
+				mainlog.WithError(err).Error("Could not reload config")
+			}
 		} else if sig == syscall.SIGUSR1 {
 		} else if sig == syscall.SIGUSR1 {
 			d.ReopenLogs()
 			d.ReopenLogs()
 		} else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT {
 		} else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT {
@@ -89,22 +94,22 @@ func sigHandler() {
 func serve(cmd *cobra.Command, args []string) {
 func serve(cmd *cobra.Command, args []string) {
 	logVersion()
 	logVersion()
 	d = guerrilla.Daemon{Logger: mainlog}
 	d = guerrilla.Daemon{Logger: mainlog}
-	err := readConfig(configPath, pidFile)
+	ac, err := readConfig(configPath, pidFile)
 	if err != nil {
 	if err != nil {
 		mainlog.WithError(err).Fatal("Error while reading config")
 		mainlog.WithError(err).Fatal("Error while reading config")
 	}
 	}
-	mainlog.SetLevel(cmdConfig.LogLevel)
+	d.SetConfig(*ac)
 
 
 	// Check that max clients is not greater than system open file limit.
 	// Check that max clients is not greater than system open file limit.
 	fileLimit := getFileLimit()
 	fileLimit := getFileLimit()
 
 
 	if fileLimit > 0 {
 	if fileLimit > 0 {
 		maxClients := 0
 		maxClients := 0
-		for _, s := range cmdConfig.Servers {
+		for _, s := range ac.Servers {
 			maxClients += s.MaxClients
 			maxClients += s.MaxClients
 		}
 		}
 		if maxClients > fileLimit {
 		if maxClients > fileLimit {
-			mainlog.Warnf("Combined max clients for all servers (%d) is greater than open file limit (%d). "+
+			mainlog.Fatalf("Combined max clients for all servers (%d) is greater than open file limit (%d). "+
 				"Please increase your open file limit or decrease max clients.", maxClients, fileLimit)
 				"Please increase your open file limit or decrease max clients.", maxClients, fileLimit)
 		}
 		}
 	}
 	}
@@ -118,38 +123,15 @@ func serve(cmd *cobra.Command, args []string) {
 
 
 }
 }
 
 
-// Superset of `guerrilla.AppConfig` containing options specific
-// the the command line interface.
-type CmdConfig struct {
-	guerrilla.AppConfig
-}
-
-func (c *CmdConfig) load(jsonBytes []byte) error {
-	err := json.Unmarshal(jsonBytes, &c)
-	if err != nil {
-		return fmt.Errorf("Could not parse config file: %s", err.Error())
-	} else {
-		// load in guerrilla.AppConfig
-		return c.AppConfig.Load(jsonBytes)
-	}
-}
-
-func (c *CmdConfig) emitChangeEvents(oldConfig *CmdConfig, app guerrilla.Guerrilla) {
-	// if your CmdConfig has any extra fields, you can emit events here
-	// ...
-	// 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, pidFile string) error {
+// ReadConfig is called at startup, or when a SIG_HUP is caught
+func readConfig(path string, pidFile string) (*guerrilla.AppConfig, error) {
 	// Load in the config.
 	// Load in the config.
 	// Note here is the only place we can make an exception to the
 	// Note here is the only place we can make an exception to the
 	// "treat config values as immutable". For example, here the
 	// "treat config values as immutable". For example, here the
 	// command line flags can override config values
 	// command line flags can override config values
 	appConfig, err := d.LoadConfig(path)
 	appConfig, err := d.LoadConfig(path)
 	if err != nil {
 	if err != nil {
-		return fmt.Errorf("Could not read config file: %s", err.Error())
+		return &appConfig, fmt.Errorf("Could not read config file: %s", err.Error())
 	}
 	}
 	// override config pidFile with with flag from the command line
 	// override config pidFile with with flag from the command line
 	if len(pidFile) > 0 {
 	if len(pidFile) > 0 {
@@ -157,8 +139,10 @@ func readConfig(path string, pidFile string) error {
 	} else if len(appConfig.PidFile) == 0 {
 	} else if len(appConfig.PidFile) == 0 {
 		appConfig.PidFile = defaultPidFile
 		appConfig.PidFile = defaultPidFile
 	}
 	}
-	d.SetConfig(&appConfig)
-	return nil
+	if verbose {
+		appConfig.LogLevel = "debug"
+	}
+	return &appConfig, nil
 }
 }
 
 
 func getFileLimit() int {
 func getFileLimit() int {

+ 51 - 51
cmd/guerrillad/serve_test.go

@@ -326,13 +326,13 @@ func TestCmdConfigChangeEvents(t *testing.T) {
 		guerrilla.EventConfigBackendConfig: false,
 		guerrilla.EventConfigBackendConfig: false,
 		guerrilla.EventConfigServerNew:     false,
 		guerrilla.EventConfigServerNew:     false,
 	}
 	}
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 
 
 	bcfg := backends.BackendConfig{"log_received_mails": true}
 	bcfg := backends.BackendConfig{"log_received_mails": true}
 	backend, err := backends.New(bcfg, mainlog)
 	backend, err := backends.New(bcfg, mainlog)
 	app, err := guerrilla.New(oldconf, backend, mainlog)
 	app, err := guerrilla.New(oldconf, backend, mainlog)
 	if err != nil {
 	if err != nil {
-		//log.Info("Failed to create new app", err)
+		t.Error("Failed to create new app", err)
 	}
 	}
 	toUnsubscribe := map[guerrilla.Event]func(c *guerrilla.AppConfig){}
 	toUnsubscribe := map[guerrilla.Event]func(c *guerrilla.AppConfig){}
 	toUnsubscribeS := map[guerrilla.Event]func(c *guerrilla.ServerConfig){}
 	toUnsubscribeS := map[guerrilla.Event]func(c *guerrilla.ServerConfig){}
@@ -382,10 +382,10 @@ func TestCmdConfigChangeEvents(t *testing.T) {
 
 
 // start server, change config, send SIG HUP, confirm that the pidfile changed & backend reloaded
 // start server, change config, send SIG HUP, confirm that the pidfile changed & backend reloaded
 func TestServe(t *testing.T) {
 func TestServe(t *testing.T) {
-	os.Truncate("../../tests/testlog", 0)
+
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 
 
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 
 
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
@@ -441,7 +441,7 @@ func TestServe(t *testing.T) {
 	}
 	}
 
 
 	// cleanup
 	// cleanup
-
+	os.Truncate("../../tests/testlog", 0)
 	os.Remove("configJsonA.json")
 	os.Remove("configJsonA.json")
 	os.Remove("./pidfile.pid")
 	os.Remove("./pidfile.pid")
 	os.Remove("./pidfile2.pid")
 	os.Remove("./pidfile2.pid")
@@ -454,7 +454,7 @@ func TestServe(t *testing.T) {
 // then connect to it & HELO.
 // then connect to it & HELO.
 func TestServerAddEvent(t *testing.T) {
 func TestServerAddEvent(t *testing.T) {
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
@@ -467,8 +467,8 @@ func TestServerAddEvent(t *testing.T) {
 	}()
 	}()
 	time.Sleep(testPauseDuration) // allow the server to start
 	time.Sleep(testPauseDuration) // allow the server to start
 	// now change the config by adding a server
 	// now change the config by adding a server
-	conf := &CmdConfig{}                                 // blank one
-	conf.load([]byte(configJsonA))                       // load configJsonA
+	conf := &guerrilla.AppConfig{}                       // blank one
+	conf.Load([]byte(configJsonA))                       // load configJsonA
 	newServer := conf.Servers[0]                         // copy the first server config
 	newServer := conf.Servers[0]                         // copy the first server config
 	newServer.ListenInterface = "127.0.0.1:2526"         // change it
 	newServer.ListenInterface = "127.0.0.1:2526"         // change it
 	newConf := conf                                      // copy the cmdConfg
 	newConf := conf                                      // copy the cmdConfg
@@ -521,7 +521,7 @@ func TestServerAddEvent(t *testing.T) {
 // then connect to 127.0.0.1:2228 & HELO.
 // then connect to 127.0.0.1:2228 & HELO.
 func TestServerStartEvent(t *testing.T) {
 func TestServerStartEvent(t *testing.T) {
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
@@ -534,8 +534,8 @@ func TestServerStartEvent(t *testing.T) {
 	}()
 	}()
 	time.Sleep(testPauseDuration)
 	time.Sleep(testPauseDuration)
 	// now change the config by adding a server
 	// now change the config by adding a server
-	conf := &CmdConfig{}           // blank one
-	conf.load([]byte(configJsonA)) // load configJsonA
+	conf := &guerrilla.AppConfig{} // blank one
+	conf.Load([]byte(configJsonA)) // load configJsonA
 
 
 	newConf := conf // copy the cmdConfg
 	newConf := conf // copy the cmdConfg
 	newConf.Servers[1].IsEnabled = true
 	newConf.Servers[1].IsEnabled = true
@@ -591,7 +591,7 @@ func TestServerStartEvent(t *testing.T) {
 
 
 func TestServerStopEvent(t *testing.T) {
 func TestServerStopEvent(t *testing.T) {
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
@@ -604,8 +604,8 @@ func TestServerStopEvent(t *testing.T) {
 	}()
 	}()
 	time.Sleep(testPauseDuration)
 	time.Sleep(testPauseDuration)
 	// now change the config by enabling a server
 	// now change the config by enabling a server
-	conf := &CmdConfig{}           // blank one
-	conf.load([]byte(configJsonA)) // load configJsonA
+	conf := &guerrilla.AppConfig{} // blank one
+	conf.Load([]byte(configJsonA)) // load configJsonA
 
 
 	newConf := conf // copy the cmdConfg
 	newConf := conf // copy the cmdConfg
 	newConf.Servers[1].IsEnabled = true
 	newConf.Servers[1].IsEnabled = true
@@ -679,11 +679,11 @@ func TestServerStopEvent(t *testing.T) {
 
 
 func TestAllowedHostsEvent(t *testing.T) {
 func TestAllowedHostsEvent(t *testing.T) {
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
-	conf := &CmdConfig{}           // blank one
-	conf.load([]byte(configJsonD)) // load configJsonD
+	conf := &guerrilla.AppConfig{} // blank one
+	conf.Load([]byte(configJsonD)) // load configJsonD
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
 	configPath = "configJsonD.json"
 	configPath = "configJsonD.json"
 	var serveWG sync.WaitGroup
 	var serveWG sync.WaitGroup
@@ -696,8 +696,8 @@ func TestAllowedHostsEvent(t *testing.T) {
 	time.Sleep(testPauseDuration)
 	time.Sleep(testPauseDuration)
 
 
 	// now connect and try RCPT TO with an invalid host
 	// 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)
+	if conn, buffin, err := test.Connect(conf.Servers[1], 20); err != nil {
+		t.Error("Could not connect to new server", conf.Servers[1].ListenInterface, err)
 	} else {
 	} else {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 			expect := "250 secure.test.com Hello"
 			expect := "250 secure.test.com Hello"
@@ -729,8 +729,8 @@ func TestAllowedHostsEvent(t *testing.T) {
 	time.Sleep(testPauseDuration) // pause for config to reload
 	time.Sleep(testPauseDuration) // pause for config to reload
 
 
 	// now repeat the same conversion, RCPT TO should be accepted
 	// 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)
+	if conn, buffin, err := test.Connect(conf.Servers[1], 20); err != nil {
+		t.Error("Could not connect to new server", conf.Servers[1].ListenInterface, err)
 	} else {
 	} else {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 			expect := "250 secure.test.com Hello"
 			expect := "250 secure.test.com Hello"
@@ -782,11 +782,11 @@ func TestTLSConfigEvent(t *testing.T) {
 	if _, err := os.Stat("../../tests/mail2.guerrillamail.com.cert.pem"); err != nil {
 	if _, err := os.Stat("../../tests/mail2.guerrillamail.com.cert.pem"); err != nil {
 		t.Error("Did not create cert ", err)
 		t.Error("Did not create cert ", err)
 	}
 	}
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
-	conf := &CmdConfig{}           // blank one
-	conf.load([]byte(configJsonD)) // load configJsonD
+	conf := &guerrilla.AppConfig{} // blank one
+	conf.Load([]byte(configJsonD)) // load configJsonD
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
 	configPath = "configJsonD.json"
 	configPath = "configJsonD.json"
 	var serveWG sync.WaitGroup
 	var serveWG sync.WaitGroup
@@ -799,8 +799,8 @@ func TestTLSConfigEvent(t *testing.T) {
 
 
 	// Test STARTTLS handshake
 	// Test STARTTLS handshake
 	testTlsHandshake := func() {
 	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)
+		if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
+			t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 		} else {
 		} else {
 			if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 			if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 				expect := "250 mail.test.com Hello"
 				expect := "250 mail.test.com Hello"
@@ -817,7 +817,7 @@ func TestTLSConfigEvent(t *testing.T) {
 								ServerName:         "127.0.0.1",
 								ServerName:         "127.0.0.1",
 							})
 							})
 							if err := tlsConn.Handshake(); err != nil {
 							if err := tlsConn.Handshake(); err != nil {
-								t.Error("Failed to handshake", conf.AppConfig.Servers[0].ListenInterface)
+								t.Error("Failed to handshake", conf.Servers[0].ListenInterface)
 							} else {
 							} else {
 								conn = tlsConn
 								conn = tlsConn
 								mainlog.Info("TLS Handshake succeeded")
 								mainlog.Info("TLS Handshake succeeded")
@@ -889,8 +889,8 @@ func TestBadTLSStart(t *testing.T) {
 		}
 		}
 		// next run the server
 		// next run the server
 		ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
 		ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
-		conf := &CmdConfig{}           // blank one
-		conf.load([]byte(configJsonD)) // load configJsonD
+		conf := &guerrilla.AppConfig{} // blank one
+		conf.Load([]byte(configJsonD)) // load configJsonD
 
 
 		cmd := &cobra.Command{}
 		cmd := &cobra.Command{}
 		configPath = "configJsonD.json"
 		configPath = "configJsonD.json"
@@ -924,13 +924,13 @@ func TestBadTLSStart(t *testing.T) {
 // Test config reload with a bad TLS config
 // Test config reload with a bad TLS config
 // It should ignore the config reload, keep running with old settings
 // It should ignore the config reload, keep running with old settings
 func TestBadTLSReload(t *testing.T) {
 func TestBadTLSReload(t *testing.T) {
-	mainlog, _ = log.GetLogger("../../tests/testlog")
-	// start with a good vert
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
+	// start with a good cert
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
-	conf := &CmdConfig{}           // blank one
-	conf.load([]byte(configJsonD)) // load configJsonD
+	conf := &guerrilla.AppConfig{} // blank one
+	conf.Load([]byte(configJsonD)) // load configJsonD
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
 	configPath = "configJsonD.json"
 	configPath = "configJsonD.json"
 	var serveWG sync.WaitGroup
 	var serveWG sync.WaitGroup
@@ -942,8 +942,8 @@ func TestBadTLSReload(t *testing.T) {
 	}()
 	}()
 	time.Sleep(testPauseDuration)
 	time.Sleep(testPauseDuration)
 
 
-	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
-		t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} else {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 			expect := "250 mail.test.com Hello"
 			expect := "250 mail.test.com Hello"
@@ -969,8 +969,8 @@ func TestBadTLSReload(t *testing.T) {
 
 
 	// we should still be able to to talk to it
 	// we should still be able to to talk to it
 
 
-	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)
+	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} else {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 			expect := "250 mail.test.com Hello"
 			expect := "250 mail.test.com Hello"
@@ -1002,12 +1002,12 @@ func TestBadTLSReload(t *testing.T) {
 // Start with configJsonD.json
 // Start with configJsonD.json
 
 
 func TestSetTimeoutEvent(t *testing.T) {
 func TestSetTimeoutEvent(t *testing.T) {
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
-	conf := &CmdConfig{}           // blank one
-	conf.load([]byte(configJsonD)) // load configJsonD
+	conf := &guerrilla.AppConfig{} // blank one
+	conf.Load([]byte(configJsonD)) // load configJsonD
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
 	configPath = "configJsonD.json"
 	configPath = "configJsonD.json"
 	var serveWG sync.WaitGroup
 	var serveWG sync.WaitGroup
@@ -1034,8 +1034,8 @@ func TestSetTimeoutEvent(t *testing.T) {
 	time.Sleep(testPauseDuration) // config reload
 	time.Sleep(testPauseDuration) // config reload
 
 
 	var waitTimeout sync.WaitGroup
 	var waitTimeout sync.WaitGroup
-	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)
+	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} else {
 		waitTimeout.Add(1)
 		waitTimeout.Add(1)
 		go func() {
 		go func() {
@@ -1080,12 +1080,12 @@ func TestSetTimeoutEvent(t *testing.T) {
 // Start in log_level = debug
 // Start in log_level = debug
 // Load config & start server
 // Load config & start server
 func TestDebugLevelChange(t *testing.T) {
 func TestDebugLevelChange(t *testing.T) {
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	// start the server by emulating the serve command
 	// start the server by emulating the serve command
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
 	ioutil.WriteFile("configJsonD.json", []byte(configJsonD), 0644)
-	conf := &CmdConfig{}           // blank one
-	conf.load([]byte(configJsonD)) // load configJsonD
+	conf := &guerrilla.AppConfig{} // blank one
+	conf.Load([]byte(configJsonD)) // load configJsonD
 	conf.LogLevel = "debug"
 	conf.LogLevel = "debug"
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}
 	configPath = "configJsonD.json"
 	configPath = "configJsonD.json"
@@ -1098,8 +1098,8 @@ func TestDebugLevelChange(t *testing.T) {
 	}()
 	}()
 	time.Sleep(testPauseDuration)
 	time.Sleep(testPauseDuration)
 
 
-	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
-		t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} else {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 		if result, err := test.Command(conn, buffin, "HELO"); err == nil {
 			expect := "250 mail.test.com Hello"
 			expect := "250 mail.test.com Hello"
@@ -1112,7 +1112,7 @@ func TestDebugLevelChange(t *testing.T) {
 	// set the log_level to info
 	// set the log_level to info
 
 
 	newConf := conf // copy the cmdConfg
 	newConf := conf // copy the cmdConfg
-	newConf.LogLevel = "info"
+	newConf.LogLevel = log.InfoLevel.String()
 	if jsonbytes, err := json.Marshal(newConf); err == nil {
 	if jsonbytes, err := json.Marshal(newConf); err == nil {
 		ioutil.WriteFile("configJsonD.json", []byte(jsonbytes), 0644)
 		ioutil.WriteFile("configJsonD.json", []byte(jsonbytes), 0644)
 	} else {
 	} else {
@@ -1123,8 +1123,8 @@ func TestDebugLevelChange(t *testing.T) {
 	time.Sleep(testPauseDuration) // log to change
 	time.Sleep(testPauseDuration) // log to change
 
 
 	// connect again, this time we should see info
 	// connect again, this time we should see info
-	if conn, buffin, err := test.Connect(conf.AppConfig.Servers[0], 20); err != nil {
-		t.Error("Could not connect to server", conf.AppConfig.Servers[0].ListenInterface, err)
+	if conn, buffin, err := test.Connect(conf.Servers[0], 20); err != nil {
+		t.Error("Could not connect to server", conf.Servers[0].ListenInterface, err)
 	} else {
 	} else {
 		if result, err := test.Command(conn, buffin, "NOOP"); err == nil {
 		if result, err := test.Command(conn, buffin, "NOOP"); err == nil {
 			expect := "200 2.0.0 OK"
 			expect := "200 2.0.0 OK"
@@ -1162,7 +1162,7 @@ func TestDebugLevelChange(t *testing.T) {
 func TestBadBackendReload(t *testing.T) {
 func TestBadBackendReload(t *testing.T) {
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 	testcert.GenerateCert("mail2.guerrillamail.com", "", 365*24*time.Hour, false, 2048, "P256", "../../tests/")
 
 
-	mainlog, _ = log.GetLogger("../../tests/testlog")
+	mainlog, _ = log.GetLogger("../../tests/testlog", "debug")
 
 
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonA), 0644)
 	cmd := &cobra.Command{}
 	cmd := &cobra.Command{}

+ 5 - 3
config.go

@@ -5,13 +5,12 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
 	"os"
 	"os"
 	"reflect"
 	"reflect"
 	"strings"
 	"strings"
 
 
-	"github.com/flashmob/go-guerrilla/backends"
-	"github.com/flashmob/go-guerrilla/log"
-
 	"github.com/flashmob/go-guerrilla/dashboard"
 	"github.com/flashmob/go-guerrilla/dashboard"
 )
 )
 
 
@@ -66,6 +65,9 @@ type ServerConfig struct {
 	// LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off".
 	// LogFile is where the logs go. Use path to file, or "stderr", "stdout" or "off".
 	// defaults to AppConfig.Log file setting
 	// defaults to AppConfig.Log file setting
 	LogFile string `json:"log_file,omitempty"`
 	LogFile string `json:"log_file,omitempty"`
+	// XClientOn when using a proxy such as Nginx, XCLIENT command is used to pass the
+	// original client's IP address & client's HELO
+	XClientOn bool `json:"xclient_on,omitempty"`
 
 
 	// The following used to watch certificate changes so that the TLS can be reloaded
 	// The following used to watch certificate changes so that the TLS can be reloaded
 	_privateKeyFile_mtime int
 	_privateKeyFile_mtime int

+ 3 - 3
config_test.go

@@ -196,7 +196,7 @@ func TestConfigChangeEvents(t *testing.T) {
 
 
 	oldconf := &AppConfig{}
 	oldconf := &AppConfig{}
 	oldconf.Load([]byte(configJsonA))
 	oldconf.Load([]byte(configJsonA))
-	logger, _ := log.GetLogger(oldconf.LogFile)
+	logger, _ := log.GetLogger(oldconf.LogFile, oldconf.LogLevel)
 	bcfg := backends.BackendConfig{"log_received_mails": true}
 	bcfg := backends.BackendConfig{"log_received_mails": true}
 	backend, err := backends.New(bcfg, logger)
 	backend, err := backends.New(bcfg, logger)
 	if err != nil {
 	if err != nil {
@@ -213,8 +213,8 @@ func TestConfigChangeEvents(t *testing.T) {
 	os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now())
 	os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now())
 	newconf := &AppConfig{}
 	newconf := &AppConfig{}
 	newconf.Load([]byte(configJsonB))
 	newconf.Load([]byte(configJsonB))
-	newconf.Servers[0].LogFile = "off" // test for log file change
-	newconf.LogLevel = "info"
+	newconf.Servers[0].LogFile = log.OutputOff.String() // test for log file change
+	newconf.LogLevel = log.InfoLevel.String()
 	newconf.LogFile = "off"
 	newconf.LogFile = "off"
 	expectedEvents := map[Event]bool{
 	expectedEvents := map[Event]bool{
 		EventConfigPidFile:         false,
 		EventConfigPidFile:         false,

+ 2 - 0
event.go

@@ -41,6 +41,8 @@ const (
 	EventConfigServerMaxClients
 	EventConfigServerMaxClients
 	// when a server's TLS config changed
 	// when a server's TLS config changed
 	EventConfigServerTLSConfig
 	EventConfigServerTLSConfig
+	//
+	EventConfigPostLoad
 )
 )
 
 
 var eventList = [...]string{
 var eventList = [...]string{

+ 5 - 5
glide.lock

@@ -1,5 +1,5 @@
-hash: 9b3a0edd571c70602c69b140883f5f14b8cfb151dc4ee54fb21acba4772f69ab
-updated: 2017-03-12T17:09:35.504459835-07:00
+hash: edbacc9b8ae3fcad4c01969c3efc5d815d79ffdc544d0bd56c501018696c2285
+updated: 2017-03-17T11:29:21.745184616+11:00
 imports:
 imports:
 - name: github.com/asaskevich/EventBus
 - name: github.com/asaskevich/EventBus
   version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2
   version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2
@@ -23,11 +23,11 @@ imports:
   subpackages:
   subpackages:
   - fs
   - fs
 - name: github.com/Sirupsen/logrus
 - name: github.com/Sirupsen/logrus
-  version: 0208149b40d863d2c1a2f8fe5753096a9cf2cc8b
+  version: ba1b36c82c5e05c4f912a88eab0dcd91a171688f
 - name: github.com/spf13/cobra
 - name: github.com/spf13/cobra
-  version: 16c014f1a19d865b765b420e74508f80eb831ada
+  version: b62566898a99f2db9c68ed0026aa0a052e59678d
 - name: github.com/spf13/pflag
 - name: github.com/spf13/pflag
-  version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7
+  version: 25f8b5b07aece3207895bf19f7ab517eb3b22a40
 - name: golang.org/x/sys
 - name: golang.org/x/sys
   version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea
   version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea
   subpackages:
   subpackages:

+ 8 - 8
goguerrilla.conf.sample

@@ -9,19 +9,19 @@
       "guerrillamail.org"
       "guerrillamail.org"
     ],
     ],
     "pid_file" : "/var/run/go-guerrilla.pid",
     "pid_file" : "/var/run/go-guerrilla.pid",
+    "dashboard": {
+          "is_enabled": true,
+          "listen_interface": ":8080",
+          "tick_interval": "5s",
+          "max_window": "24h",
+          "ranking_aggregation_interval": "6h"
+        },
     "backend_config": {
     "backend_config": {
         "log_received_mails": true,
         "log_received_mails": true,
         "save_workers_size": 1,
         "save_workers_size": 1,
         "save_process" : "HeadersParser|Header|Debugger",
         "save_process" : "HeadersParser|Header|Debugger",
         "primary_mail_host" : "mail.example.com"
         "primary_mail_host" : "mail.example.com"
     },
     },
-    "dashboard": {
-      "is_enabled": true,
-      "listen_interface": ":8080",
-      "tick_interval": "5s",
-      "max_window": "24h",
-      "ranking_aggregation_interval": "6h"
-    },
     "servers" : [
     "servers" : [
         {
         {
             "is_enabled" : true,
             "is_enabled" : true,
@@ -37,7 +37,7 @@
             "log_file" : "stderr"
             "log_file" : "stderr"
         },
         },
         {
         {
-            "is_enabled" : true,
+            "is_enabled" : false,
             "host_name":"mail.test.com",
             "host_name":"mail.test.com",
             "max_size":1000000,
             "max_size":1000000,
             "private_key_file":"/path/to/pem/file/test.com.key",
             "private_key_file":"/path/to/pem/file/test.com.key",

+ 22 - 21
guerrilla.go

@@ -3,13 +3,11 @@ package guerrilla
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
 	"os"
 	"os"
 	"sync"
 	"sync"
 	"sync/atomic"
 	"sync/atomic"
-
-	"github.com/flashmob/go-guerrilla/backends"
-	"github.com/flashmob/go-guerrilla/dashboard"
-	"github.com/flashmob/go-guerrilla/log"
 )
 )
 
 
 const (
 const (
@@ -69,7 +67,7 @@ func (ls *logStore) mainlog() log.Logger {
 	if v, ok := ls.Load().(log.Logger); ok {
 	if v, ok := ls.Load().(log.Logger); ok {
 		return v
 		return v
 	}
 	}
-	l, _ := log.GetLogger(log.OutputStderr.String())
+	l, _ := log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
 	return l
 	return l
 }
 }
 
 
@@ -88,7 +86,11 @@ func New(ac *AppConfig, b backends.Backend, l log.Logger) (Guerrilla, error) {
 	g.setMainlog(l)
 	g.setMainlog(l)
 
 
 	if ac.LogLevel != "" {
 	if ac.LogLevel != "" {
-		g.mainlog().SetLevel(ac.LogLevel)
+		if h, ok := l.(*log.HookedLogger); ok {
+			if h, err := log.GetLogger(h.GetLogDest(), ac.LogLevel); err == nil {
+				g.setMainlog(h)
+			}
+		}
 	}
 	}
 
 
 	g.state = GuerrillaStateNew
 	g.state = GuerrillaStateNew
@@ -206,7 +208,7 @@ func (g *guerrilla) subscribeEvents() {
 	g.Subscribe(EventConfigLogFile, func(c *AppConfig) {
 	g.Subscribe(EventConfigLogFile, func(c *AppConfig) {
 		var err error
 		var err error
 		var l log.Logger
 		var l log.Logger
-		if l, err = log.GetLogger(c.LogFile); err == nil {
+		if l, err = log.GetLogger(c.LogFile, c.LogLevel); err == nil {
 			g.setMainlog(l)
 			g.setMainlog(l)
 			g.mapServers(func(server *server) {
 			g.mapServers(func(server *server) {
 				// it will change server's logger when the next client gets accepted
 				// it will change server's logger when the next client gets accepted
@@ -227,11 +229,14 @@ func (g *guerrilla) subscribeEvents() {
 
 
 	// when log level changes, apply to mainlog and server logs
 	// when log level changes, apply to mainlog and server logs
 	g.Subscribe(EventConfigLogLevel, func(c *AppConfig) {
 	g.Subscribe(EventConfigLogLevel, func(c *AppConfig) {
-		g.mainlog().SetLevel(c.LogLevel)
-		g.mapServers(func(server *server) {
-			server.log.SetLevel(c.LogLevel)
-		})
-		g.mainlog().Infof("log level changed to [%s]", c.LogLevel)
+		l, err := log.GetLogger(g.mainlog().GetLogDest(), c.LogLevel)
+		if err == nil {
+			g.logStore.Store(l)
+			g.mapServers(func(server *server) {
+				server.logStore.Store(l)
+			})
+			g.mainlog().Infof("log level changed to [%s]", c.LogLevel)
+		}
 	})
 	})
 
 
 	// write out our pid whenever the file name changes in the config
 	// write out our pid whenever the file name changes in the config
@@ -322,7 +327,8 @@ func (g *guerrilla) subscribeEvents() {
 		if server, err := g.findServer(sc.ListenInterface); err == nil {
 		if server, err := g.findServer(sc.ListenInterface); err == nil {
 			var err error
 			var err error
 			var l log.Logger
 			var l log.Logger
-			if l, err = log.GetLogger(sc.LogFile); err == nil {
+			level := g.mainlog().GetLevel()
+			if l, err = log.GetLogger(sc.LogFile, level); err == nil {
 				g.setMainlog(l)
 				g.setMainlog(l)
 				backends.Svc.SetMainlog(l)
 				backends.Svc.SetMainlog(l)
 				// it will change to the new logger on the next accepted client
 				// it will change to the new logger on the next accepted client
@@ -343,13 +349,13 @@ func (g *guerrilla) subscribeEvents() {
 	// when the daemon caught a sighup, event for individual server
 	// when the daemon caught a sighup, event for individual server
 	g.Subscribe(EventConfigServerLogReopen, func(sc *ServerConfig) {
 	g.Subscribe(EventConfigServerLogReopen, func(sc *ServerConfig) {
 		if server, err := g.findServer(sc.ListenInterface); err == nil {
 		if server, err := g.findServer(sc.ListenInterface); err == nil {
-			server.log.Reopen()
+			server.log().Reopen()
 			g.mainlog().Infof("Server [%s] re-opened log file [%s]", sc.ListenInterface, sc.LogFile)
 			g.mainlog().Infof("Server [%s] re-opened log file [%s]", sc.ListenInterface, sc.LogFile)
 		}
 		}
 	})
 	})
 	// when the backend changes
 	// when the backend changes
 	g.Subscribe(EventConfigBackendConfig, func(appConfig *AppConfig) {
 	g.Subscribe(EventConfigBackendConfig, func(appConfig *AppConfig) {
-		logger, _ := log.GetLogger(appConfig.LogFile)
+		logger, _ := log.GetLogger(appConfig.LogFile, appConfig.LogLevel)
 		// shutdown the backend first.
 		// shutdown the backend first.
 		var err error
 		var err error
 		if err = g.backend().Shutdown(); err != nil {
 		if err = g.backend().Shutdown(); err != nil {
@@ -437,10 +443,6 @@ func (g *guerrilla) Start() error {
 	// wait for all servers to start (or fail)
 	// wait for all servers to start (or fail)
 	startWG.Wait()
 	startWG.Wait()
 
 
-	if g.Config.Dashboard.Enabled {
-		go dashboard.Run(&g.Config.Dashboard)
-	}
-
 	// close, then read any errors
 	// close, then read any errors
 	close(errs)
 	close(errs)
 	for err := range errs {
 	for err := range errs {
@@ -456,7 +458,7 @@ func (g *guerrilla) Start() error {
 
 
 func (g *guerrilla) Shutdown() {
 func (g *guerrilla) Shutdown() {
 
 
-	// shot down the servers first
+	// shut down the servers first
 	g.mapServers(func(s *server) {
 	g.mapServers(func(s *server) {
 		if s.state == ServerStateRunning {
 		if s.state == ServerStateRunning {
 			s.Shutdown()
 			s.Shutdown()
@@ -478,7 +480,6 @@ func (g *guerrilla) Shutdown() {
 
 
 // SetLogger sets the logger for the app and propagates it to sub-packages (eg.
 // SetLogger sets the logger for the app and propagates it to sub-packages (eg.
 func (g *guerrilla) SetLogger(l log.Logger) {
 func (g *guerrilla) SetLogger(l log.Logger) {
-	l.SetLevel(g.Config.LogLevel)
 	g.setMainlog(l)
 	g.setMainlog(l)
 	backends.Svc.SetMainlog(l)
 	backends.Svc.SetMainlog(l)
 }
 }

+ 189 - 0
log/hook.go

@@ -0,0 +1,189 @@
+package log
+
+import (
+	"bufio"
+	log "github.com/Sirupsen/logrus"
+	"io"
+	"io/ioutil"
+	"os"
+	"strings"
+	"sync"
+)
+
+// custom logrus hook
+
+// hookMu ensures all io operations are synced. Always on exported functions
+var hookMu sync.Mutex
+
+// LoggerHook extends the log.Hook interface by adding Reopen() and Rename()
+type LoggerHook interface {
+	log.Hook
+	Reopen() error
+}
+type LogrusHook struct {
+	w io.Writer
+	// file descriptor, can be re-opened
+	fd *os.File
+	// filename to the file descriptor
+	fname string
+	// txtFormatter that doesn't use colors
+	plainTxtFormatter *log.TextFormatter
+
+	mu sync.Mutex
+}
+
+// newLogrusHook creates a new hook. dest can be a file name or one of the following strings:
+// "stderr" - log to stderr, lines will be written to os.Stdout
+// "stdout" - log to stdout, lines will be written to os.Stdout
+// "off" - no log, lines will be written to ioutil.Discard
+func NewLogrusHook(dest string) (LoggerHook, error) {
+	hookMu.Lock()
+	defer hookMu.Unlock()
+	hook := LogrusHook{fname: dest}
+	err := hook.setup(dest)
+	return &hook, err
+}
+
+type OutputOption int
+
+const (
+	OutputStderr OutputOption = 1 + iota
+	OutputStdout
+	OutputOff
+	OutputNull
+	OutputFile
+)
+
+var outputOptions = [...]string{
+	"stderr",
+	"stdout",
+	"off",
+	"",
+	"file",
+}
+
+func (o OutputOption) String() string {
+	return outputOptions[o-1]
+}
+
+func parseOutputOption(str string) OutputOption {
+	switch str {
+	case "stderr":
+		return OutputStderr
+	case "stdout":
+		return OutputStdout
+	case "off":
+		return OutputOff
+	case "":
+		return OutputNull
+	}
+	return OutputFile
+}
+
+// Setup sets the hook's writer w and file descriptor fd
+// assumes the hook.fd is closed and nil
+func (hook *LogrusHook) setup(dest string) error {
+
+	out := parseOutputOption(dest)
+	if out == OutputNull || out == OutputStderr {
+		hook.w = os.Stderr
+	} else if out == OutputStdout {
+		hook.w = os.Stdout
+	} else if out == OutputOff {
+		hook.w = ioutil.Discard
+	} else {
+		if _, err := os.Stat(dest); err == nil {
+			// file exists open the file for appending
+			if err := hook.openAppend(dest); err != nil {
+				return err
+			}
+		} else {
+			// create the file
+			if err := hook.openCreate(dest); err != nil {
+				return err
+			}
+		}
+	}
+	// disable colors when writing to file
+	if hook.fd != nil {
+		hook.plainTxtFormatter = &log.TextFormatter{DisableColors: true}
+	}
+	return nil
+}
+
+// openAppend opens the dest file for appending. Default to os.Stderr if it can't open dest
+func (hook *LogrusHook) openAppend(dest string) (err error) {
+	fd, err := os.OpenFile(dest, os.O_APPEND|os.O_WRONLY, 0644)
+	if err != nil {
+		log.WithError(err).Error("Could not open log file for appending")
+		hook.w = os.Stderr
+		hook.fd = nil
+		return
+	}
+	hook.w = bufio.NewWriter(fd)
+	hook.fd = fd
+	return
+}
+
+// openCreate creates a new dest file for appending. Default to os.Stderr if it can't open dest
+func (hook *LogrusHook) openCreate(dest string) (err error) {
+	fd, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
+	if err != nil {
+		log.WithError(err).Error("Could not create log file")
+		hook.w = os.Stderr
+		hook.fd = nil
+		return
+	}
+	hook.w = bufio.NewWriter(fd)
+	hook.fd = fd
+	return
+}
+
+// Fire implements the logrus Hook interface. It disables color text formatting if writing to a file
+func (hook *LogrusHook) Fire(entry *log.Entry) error {
+	hookMu.Lock()
+	defer hookMu.Unlock()
+	if line, err := entry.String(); err == nil {
+		r := strings.NewReader(line)
+		if _, err = io.Copy(hook.w, r); err != nil {
+			return err
+		}
+		if wb, ok := hook.w.(*bufio.Writer); ok {
+			if err := wb.Flush(); err != nil {
+				return err
+			}
+			if hook.fd != nil {
+				hook.fd.Sync()
+			}
+		}
+		return err
+	} else {
+		return err
+	}
+}
+
+// Levels implements the logrus Hook interface
+func (hook *LogrusHook) Levels() []log.Level {
+	return log.AllLevels
+}
+
+// Reopen closes and re-open log file descriptor, which is a special feature of this hook
+func (hook *LogrusHook) Reopen() error {
+	hookMu.Lock()
+	defer hookMu.Unlock()
+	var err error
+	if hook.fd != nil {
+		if err = hook.fd.Close(); err != nil {
+			return err
+		}
+		// The file could have been re-named by an external program such as logrotate(8)
+		if _, err := os.Stat(hook.fname); err != nil {
+			// The file doesn't exist, create a new one.
+			return hook.openCreate(hook.fname)
+		} else {
+			return hook.openAppend(hook.fname)
+		}
+	}
+	return err
+
+}

+ 102 - 217
log/log.go

@@ -1,19 +1,56 @@
 package log
 package log
 
 
 import (
 import (
-	"bufio"
+	log "github.com/Sirupsen/logrus"
 	"io"
 	"io"
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
 	"os"
 	"os"
-	"strings"
 	"sync"
 	"sync"
+)
 
 
-	log "github.com/Sirupsen/logrus"
-
-	"github.com/flashmob/go-guerrilla/dashboard"
+// The following are taken from logrus
+const (
+	// PanicLevel level, highest level of severity. Logs and then calls panic with the
+	// message passed to Debug, Info, ...
+	PanicLevel Level = iota
+	// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
+	// logging level is set to Panic.
+	FatalLevel
+	// ErrorLevel level. Logs. Used for errors that should definitely be noted.
+	// Commonly used for hooks to send errors to an error tracking service.
+	ErrorLevel
+	// WarnLevel level. Non-critical entries that deserve eyes.
+	WarnLevel
+	// InfoLevel level. General operational entries about what's going on inside the
+	// application.
+	InfoLevel
+	// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
+	DebugLevel
 )
 )
 
 
+type Level uint8
+
+// Convert the Level to a string. E.g. PanicLevel becomes "panic".
+func (level Level) String() string {
+	switch level {
+	case DebugLevel:
+		return "debug"
+	case InfoLevel:
+		return "info"
+	case WarnLevel:
+		return "warning"
+	case ErrorLevel:
+		return "error"
+	case FatalLevel:
+		return "fatal"
+	case PanicLevel:
+		return "panic"
+	}
+
+	return "unknown"
+}
+
 type Logger interface {
 type Logger interface {
 	log.FieldLogger
 	log.FieldLogger
 	WithConn(conn net.Conn) *log.Entry
 	WithConn(conn net.Conn) *log.Entry
@@ -33,9 +70,18 @@ type HookedLogger struct {
 	*log.Logger
 	*log.Logger
 
 
 	h LoggerHook
 	h LoggerHook
+
+	// destination, file name or "stderr", "stdout" or "off"
+	dest string
+
+	oo OutputOption
 }
 }
 
 
-type loggerCache map[string]Logger
+type loggerKey struct {
+	dest, level string
+}
+
+type loggerCache map[loggerKey]Logger
 
 
 // loggers store the cached loggers created by NewLogger
 // loggers store the cached loggers created by NewLogger
 var loggers struct {
 var loggers struct {
@@ -55,27 +101,34 @@ var loggers struct {
 // Each Logger returned is cached on dest, subsequent call will get the cached logger if dest matches
 // Each Logger returned is cached on dest, subsequent call will get the cached logger if dest matches
 // If there was an error, the log will revert to stderr instead of using a custom hook
 // If there was an error, the log will revert to stderr instead of using a custom hook
 
 
-func GetLogger(dest string) (Logger, error) {
+func GetLogger(dest string, level string) (Logger, error) {
 	loggers.Lock()
 	loggers.Lock()
 	defer loggers.Unlock()
 	defer loggers.Unlock()
+	key := loggerKey{dest, level}
 	if loggers.cache == nil {
 	if loggers.cache == nil {
 		loggers.cache = make(loggerCache, 1)
 		loggers.cache = make(loggerCache, 1)
 	} else {
 	} else {
-		if l, ok := loggers.cache[dest]; ok {
+		if l, ok := loggers.cache[key]; ok {
 			// return the one we found in the cache
 			// return the one we found in the cache
 			return l, nil
 			return l, nil
 		}
 		}
 	}
 	}
-	logrus := log.New()
-	// we'll use the hook to output instead
-	logrus.Out = ioutil.Discard
-
-	l := &HookedLogger{}
+	o := parseOutputOption(dest)
+	logrus, err := newLogrus(o, level)
+	if err != nil {
+		return nil, err
+	}
+	l := &HookedLogger{dest: dest}
 	l.Logger = logrus
 	l.Logger = logrus
 
 
 	// cache it
 	// cache it
-	loggers.cache[dest] = l
+	loggers.cache[key] = l
 
 
+	if o != OutputFile {
+		return l, nil
+	}
+	// we'll use the hook to output instead
+	logrus.Out = ioutil.Discard
 	// setup the hook
 	// setup the hook
 	if h, err := NewLogrusHook(dest); err != nil {
 	if h, err := NewLogrusHook(dest); err != nil {
 		// revert back to stderr
 		// revert back to stderr
@@ -86,13 +139,40 @@ func GetLogger(dest string) (Logger, error) {
 		l.h = h
 		l.h = h
 	}
 	}
 
 
-	// add the dashboard hook
-	logrus.Hooks.Add(dashboard.LogHook)
-
 	return l, nil
 	return l, nil
 
 
 }
 }
 
 
+func newLogrus(o OutputOption, level string) (*log.Logger, error) {
+	logLevel, err := log.ParseLevel(level)
+	if err != nil {
+		return nil, err
+	}
+	var out io.Writer
+
+	if o != OutputFile {
+		if o == OutputNull || o == OutputStderr {
+			out = os.Stderr
+		} else if o == OutputStdout {
+			out = os.Stdout
+		} else if o == OutputOff {
+			out = ioutil.Discard
+		}
+	} else {
+		// we'll use a hook to output instead
+		out = ioutil.Discard
+	}
+
+	logger := &log.Logger{
+		Out:       out,
+		Formatter: new(log.TextFormatter),
+		Hooks:     make(log.LevelHooks),
+		Level:     logLevel,
+	}
+
+	return logger, nil
+}
+
 // AddHook adds a new logrus hook
 // AddHook adds a new logrus hook
 func (l *HookedLogger) AddHook(h log.Hook) {
 func (l *HookedLogger) AddHook(h log.Hook) {
 	log.AddHook(h)
 	log.AddHook(h)
@@ -109,7 +189,6 @@ func (l *HookedLogger) SetLevel(level string) {
 	if logLevel, err = log.ParseLevel(level); err != nil {
 	if logLevel, err = log.ParseLevel(level); err != nil {
 		return
 		return
 	}
 	}
-	l.Level = logLevel
 	log.SetLevel(logLevel)
 	log.SetLevel(logLevel)
 }
 }
 
 
@@ -120,12 +199,15 @@ func (l *HookedLogger) GetLevel() string {
 
 
 // Reopen closes the log file and re-opens it
 // Reopen closes the log file and re-opens it
 func (l *HookedLogger) Reopen() error {
 func (l *HookedLogger) Reopen() error {
+	if l.h == nil {
+		return nil
+	}
 	return l.h.Reopen()
 	return l.h.Reopen()
 }
 }
 
 
-// Fgetname Gets the file name
+// GetLogDest Gets the file name
 func (l *HookedLogger) GetLogDest() string {
 func (l *HookedLogger) GetLogDest() string {
-	return l.h.GetLogDest()
+	return l.dest
 }
 }
 
 
 // WithConn extends logrus to be able to log with a net.Conn
 // WithConn extends logrus to be able to log with a net.Conn
@@ -137,200 +219,3 @@ func (l *HookedLogger) WithConn(conn net.Conn) *log.Entry {
 	}
 	}
 	return l.WithField("addr", addr)
 	return l.WithField("addr", addr)
 }
 }
-
-// custom logrus hook
-
-// hookMu ensures all io operations are synced. Always on exported functions
-var hookMu sync.Mutex
-
-// LoggerHook extends the log.Hook interface by adding Reopen() and Rename()
-type LoggerHook interface {
-	log.Hook
-	Reopen() error
-	GetLogDest() string
-}
-type LogrusHook struct {
-	w io.Writer
-	// file descriptor, can be re-opened
-	fd *os.File
-	// filename to the file descriptor
-	fname string
-	// txtFormatter that doesn't use colors
-	plainTxtFormatter *log.TextFormatter
-
-	mu sync.Mutex
-}
-
-// newLogrusHook creates a new hook. dest can be a file name or one of the following strings:
-// "stderr" - log to stderr, lines will be written to os.Stdout
-// "stdout" - log to stdout, lines will be written to os.Stdout
-// "off" - no log, lines will be written to ioutil.Discard
-func NewLogrusHook(dest string) (LoggerHook, error) {
-	hookMu.Lock()
-	defer hookMu.Unlock()
-	hook := LogrusHook{fname: dest}
-	err := hook.setup(dest)
-	return &hook, err
-}
-
-type OutputOption int
-
-const (
-	OutputStderr OutputOption = 1 + iota
-	OutputStdout
-	OutputOff
-	OutputNull
-	OutputFile
-)
-
-var outputOptions = [...]string{
-	"stderr",
-	"stdout",
-	"off",
-	"",
-	"file",
-}
-
-func (o OutputOption) String() string {
-	return outputOptions[o-1]
-}
-
-func parseOutputOption(str string) OutputOption {
-	switch str {
-	case "stderr":
-		return OutputStderr
-	case "stdout":
-		return OutputStdout
-	case "off":
-		return OutputOff
-	case "":
-		return OutputNull
-	}
-	return OutputFile
-}
-
-// Setup sets the hook's writer w and file descriptor fd
-// assumes the hook.fd is closed and nil
-func (hook *LogrusHook) setup(dest string) error {
-
-	out := parseOutputOption(dest)
-	if out == OutputNull || out == OutputStderr {
-		hook.w = os.Stderr
-	} else if out == OutputStdout {
-		hook.w = os.Stdout
-	} else if out == OutputOff {
-		hook.w = ioutil.Discard
-	} else {
-		if _, err := os.Stat(dest); err == nil {
-			// file exists open the file for appending
-			if err := hook.openAppend(dest); err != nil {
-				return err
-			}
-		} else {
-			// create the file
-			if err := hook.openCreate(dest); err != nil {
-				return err
-			}
-		}
-	}
-	// disable colors when writing to file
-	if hook.fd != nil {
-		hook.plainTxtFormatter = &log.TextFormatter{DisableColors: true}
-	}
-	return nil
-}
-
-// openAppend opens the dest file for appending. Default to os.Stderr if it can't open dest
-func (hook *LogrusHook) openAppend(dest string) (err error) {
-	fd, err := os.OpenFile(dest, os.O_APPEND|os.O_WRONLY, 0644)
-	if err != nil {
-		log.WithError(err).Error("Could not open log file for appending")
-		hook.w = os.Stderr
-		hook.fd = nil
-		return
-	}
-	hook.w = bufio.NewWriter(fd)
-	hook.fd = fd
-	return
-}
-
-// openCreate creates a new dest file for appending. Default to os.Stderr if it can't open dest
-func (hook *LogrusHook) openCreate(dest string) (err error) {
-	fd, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
-	if err != nil {
-		log.WithError(err).Error("Could not create log file")
-		hook.w = os.Stderr
-		hook.fd = nil
-		return
-	}
-	hook.w = bufio.NewWriter(fd)
-	hook.fd = fd
-	return
-}
-
-// Fire implements the logrus Hook interface. It disables color text formatting if writing to a file
-func (hook *LogrusHook) Fire(entry *log.Entry) error {
-	hookMu.Lock()
-	defer hookMu.Unlock()
-	if hook.fd != nil {
-		// save the old hook
-		oldhook := entry.Logger.Formatter
-		defer func() {
-			// set the back to the old hook after we're done
-			entry.Logger.Formatter = oldhook
-		}()
-		// use the plain text hook
-		entry.Logger.Formatter = hook.plainTxtFormatter
-		// todo : `go go test -v -race` detected a race condition, try log.SetFormatter()
-	}
-	if line, err := entry.String(); err == nil {
-		r := strings.NewReader(line)
-		if _, err = io.Copy(hook.w, r); err != nil {
-			return err
-		}
-		if wb, ok := hook.w.(*bufio.Writer); ok {
-			if err := wb.Flush(); err != nil {
-				return err
-			}
-			if hook.fd != nil {
-				hook.fd.Sync()
-			}
-		}
-		return err
-	} else {
-		return err
-	}
-}
-
-// GetLogDest returns the destination of the log as a string
-func (hook *LogrusHook) GetLogDest() string {
-	hookMu.Lock()
-	defer hookMu.Unlock()
-	return hook.fname
-}
-
-// Levels implements the logrus Hook interface
-func (hook *LogrusHook) Levels() []log.Level {
-	return log.AllLevels
-}
-
-// Reopen closes and re-open log file descriptor, which is a special feature of this hook
-func (hook *LogrusHook) Reopen() error {
-	hookMu.Lock()
-	defer hookMu.Unlock()
-	var err error
-	if hook.fd != nil {
-		if err = hook.fd.Close(); err != nil {
-			return err
-		}
-		// The file could have been re-named by an external program such as logrotate(8)
-		if _, err := os.Stat(hook.fname); err != nil {
-			// The file doesn't exist,create a new one.
-			return hook.openCreate(hook.fname)
-		} else {
-			return hook.openAppend(hook.fname)
-		}
-	}
-	return err
-
-}

+ 31 - 8
mail/envelope.go

@@ -11,6 +11,7 @@ import (
 	"io"
 	"io"
 	"io/ioutil"
 	"io/ioutil"
 	"mime/quotedprintable"
 	"mime/quotedprintable"
+	"net/mail"
 	"net/textproto"
 	"net/textproto"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
@@ -18,7 +19,7 @@ import (
 	"time"
 	"time"
 )
 )
 
 
-const maxHeaderChunk = iota + (3 << 10) // 3KB
+const maxHeaderChunk = 1 + (3 << 10) // 3KB
 
 
 // Address encodes an email address of the form `<user@host>`
 // Address encodes an email address of the form `<user@host>`
 type Address struct {
 type Address struct {
@@ -34,6 +35,26 @@ func (ep *Address) IsEmpty() bool {
 	return ep.User == "" && ep.Host == ""
 	return ep.User == "" && ep.Host == ""
 }
 }
 
 
+var ap = mail.AddressParser{}
+
+// NewAddress takes a string of an RFC 5322 address of the
+// form "Gogh Fir <[email protected]>" or "[email protected]".
+func NewAddress(str string) (Address, error) {
+	a, err := ap.Parse(str)
+	if err != nil {
+		return Address{}, err
+	}
+	pos := strings.Index(a.Address, "@")
+	if pos > 0 {
+		return Address{
+				User: a.Address[0:pos],
+				Host: a.Address[pos+1:],
+			},
+			nil
+	}
+	return Address{}, errors.New("invalid address")
+}
+
 // Email represents a single SMTP message.
 // Email represents a single SMTP message.
 type Envelope struct {
 type Envelope struct {
 	// Remote IP address
 	// Remote IP address
@@ -130,25 +151,27 @@ func (e *Envelope) String() string {
 	return e.DeliveryHeader + e.Data.String()
 	return e.DeliveryHeader + e.Data.String()
 }
 }
 
 
-// ResetTransaction is called when the transaction is reset (but save connection)
+// ResetTransaction is called when the transaction is reset (keeping the connection open)
 func (e *Envelope) ResetTransaction() {
 func (e *Envelope) ResetTransaction() {
 	e.MailFrom = Address{}
 	e.MailFrom = Address{}
 	e.RcptTo = []Address{}
 	e.RcptTo = []Address{}
 	// reset the data buffer, keep it allocated
 	// reset the data buffer, keep it allocated
 	e.Data.Reset()
 	e.Data.Reset()
-}
 
 
-// Seed is called when used with a new connection, once it's accepted
-func (e *Envelope) Reseed(RemoteIP string, clientID uint64) {
+	// todo: these are probably good candidates for buffers / use sync.Pool (after profiling)
 	e.Subject = ""
 	e.Subject = ""
-	e.RemoteIP = RemoteIP
-	e.Helo = ""
 	e.Header = nil
 	e.Header = nil
-	e.TLS = false
 	e.Hashes = make([]string, 0)
 	e.Hashes = make([]string, 0)
 	e.DeliveryHeader = ""
 	e.DeliveryHeader = ""
 	e.Values = make(map[string]interface{})
 	e.Values = make(map[string]interface{})
+}
+
+// Seed is called when used with a new connection, once it's accepted
+func (e *Envelope) Reseed(RemoteIP string, clientID uint64) {
+	e.RemoteIP = RemoteIP
 	e.QueuedId = queuedID(clientID)
 	e.QueuedId = queuedID(clientID)
+	e.Helo = ""
+	e.TLS = false
 }
 }
 
 
 // PushRcpt adds a recipient email address to the envelope
 // PushRcpt adds a recipient email address to the envelope

+ 11 - 0
mail/envelope_test.go

@@ -16,7 +16,18 @@ func TestMimeHeaderDecode(t *testing.T) {
 		t.Error("expecting André Pirard, got:", str)
 		t.Error("expecting André Pirard, got:", str)
 	}
 	}
 }
 }
+func TestNewAddress(t *testing.T) {
 
 
+	addr, err := NewAddress("<hoop>")
+	if err == nil {
+		t.Error("there should be an error:", addr)
+	}
+
+	addr, err = NewAddress(`Gogh Fir <[email protected]>`)
+	if err != nil {
+		t.Error("there should be no error:", addr.Host, err)
+	}
+}
 func TestEnvelope(t *testing.T) {
 func TestEnvelope(t *testing.T) {
 	e := NewEnvelope("127.0.0.1", 22)
 	e := NewEnvelope("127.0.0.1", 22)
 
 

+ 2 - 1
pool.go

@@ -150,12 +150,13 @@ func (p *Pool) Borrow(conn net.Conn, clientID uint64, logger log.Logger, ep *mai
 
 
 // Return returns a Client back to the pool.
 // Return returns a Client back to the pool.
 func (p *Pool) Return(c Poolable) {
 func (p *Pool) Return(c Poolable) {
+	p.activeClientsRemove(c)
 	select {
 	select {
 	case p.pool <- c:
 	case p.pool <- c:
 	default:
 	default:
 		// hasta la vista, baby...
 		// hasta la vista, baby...
 	}
 	}
-	p.activeClientsRemove(c)
+
 	<-p.sem // make room for the next serving client
 	<-p.sem // make room for the next serving client
 }
 }
 
 

+ 1 - 1
response/quote.go

@@ -33,7 +33,7 @@ var quotes = struct {
 		"214-The Dude: No, you're not wrong Walter, you're just an ass-hole." +
 		"214-The Dude: No, you're not wrong Walter, you're just an ass-hole." +
 		"214 Walter Sobchak: Okay then.",
 		"214 Walter Sobchak: Okay then.",
 	14: "214-Private Snoop: you see what happens lebowski?" + CRLF +
 	14: "214-Private Snoop: you see what happens lebowski?" + CRLF +
-		"214-The Dude: nobody calls me lebowski, you got the wrong guy, I'm the the dude, man." + CRLF +
+		"214-The Dude: nobody calls me lebowski, you got the wrong guy, I'm the dude, man." + CRLF +
 		"214-Private Snoop: Your name's Lebowski, Lebowski. Your wife is Bunny." + CRLF +
 		"214-Private Snoop: Your name's Lebowski, Lebowski. Your wife is Bunny." + CRLF +
 		"214-The Dude: My wife? Bunny? Do you see a wedding ring on my finger? " + CRLF +
 		"214-The Dude: My wife? Bunny? Do you see a wedding ring on my finger? " + CRLF +
 		"214 Does this place look like I'm f**kin married? The toilet seat's up man!",
 		"214 Does this place look like I'm f**kin married? The toilet seat's up man!",

+ 72 - 54
server.go

@@ -55,8 +55,6 @@ type server struct {
 	closedListener  chan (bool)
 	closedListener  chan (bool)
 	hosts           allowedHosts // stores map[string]bool for faster lookup
 	hosts           allowedHosts // stores map[string]bool for faster lookup
 	state           int
 	state           int
-	mainlog         log.Logger
-	log             log.Logger
 	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
 	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
 	logStore     atomic.Value
 	logStore     atomic.Value
 	mainlogStore atomic.Value
 	mainlogStore atomic.Value
@@ -76,24 +74,22 @@ func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, err
 		closedListener:  make(chan (bool), 1),
 		closedListener:  make(chan (bool), 1),
 		listenInterface: sc.ListenInterface,
 		listenInterface: sc.ListenInterface,
 		state:           ServerStateNew,
 		state:           ServerStateNew,
-		mainlog:         l,
 		envelopePool:    mail.NewPool(sc.MaxClients),
 		envelopePool:    mail.NewPool(sc.MaxClients),
 	}
 	}
+	server.logStore.Store(l)
 	server.backendStore.Store(b)
 	server.backendStore.Store(b)
-	var logOpenError error
-	if sc.LogFile == "" {
+	logFile := sc.LogFile
+	if logFile == "" {
 		// none set, use the same log file as mainlog
 		// none set, use the same log file as mainlog
-		server.log, logOpenError = log.GetLogger(server.mainlog.GetLogDest())
-	} else {
-		server.log, logOpenError = log.GetLogger(sc.LogFile)
+		logFile = server.mainlog().GetLogDest()
 	}
 	}
+	// set level to same level as mainlog level
+	mainlog, logOpenError := log.GetLogger(logFile, server.mainlog().GetLevel())
+	server.mainlogStore.Store(mainlog)
 	if logOpenError != nil {
 	if logOpenError != nil {
-		server.log.WithError(logOpenError).Errorf("Failed creating a logger for server [%s]", sc.ListenInterface)
+		server.log().WithError(logOpenError).Errorf("Failed creating a logger for server [%s]", sc.ListenInterface)
 	}
 	}
 
 
-	// set to same level
-	server.log.SetLevel(server.mainlog.GetLevel())
-
 	server.setConfig(sc)
 	server.setConfig(sc)
 	server.setTimeout(sc.Timeout)
 	server.setTimeout(sc.Timeout)
 	if err := server.configureSSL(); err != nil {
 	if err := server.configureSSL(); err != nil {
@@ -120,24 +116,7 @@ func (s *server) configureSSL() error {
 	return nil
 	return nil
 }
 }
 
 
-// configureLog checks to see if there is a new logger, so that the server.log can be safely changed
-// this function is not gorotine safe, although it'll read the new value safely
-func (s *server) configureLog() {
-	// when log changed
-	if l, ok := s.logStore.Load().(log.Logger); ok {
-		if l != s.log {
-			s.log = l
-		}
-	}
-	// when mainlog changed
-	if ml, ok := s.mainlogStore.Load().(log.Logger); ok {
-		if ml != s.mainlog {
-			s.mainlog = ml
-		}
-	}
-}
-
-// setBackend Sets the backend to use for processing email envelopes
+// setBackend sets the backend to use for processing email envelopes
 func (s *server) setBackend(b backends.Backend) {
 func (s *server) setBackend(b backends.Backend) {
 	s.backendStore.Store(b)
 	s.backendStore.Store(b)
 }
 }
@@ -191,27 +170,26 @@ func (server *server) Start(startWG *sync.WaitGroup) error {
 		return fmt.Errorf("[%s] Cannot listen on port: %s ", server.listenInterface, err.Error())
 		return fmt.Errorf("[%s] Cannot listen on port: %s ", server.listenInterface, err.Error())
 	}
 	}
 
 
-	server.log.Infof("Listening on TCP %s", server.listenInterface)
+	server.log().Infof("Listening on TCP %s", server.listenInterface)
 	server.state = ServerStateRunning
 	server.state = ServerStateRunning
 	startWG.Done() // start successful, don't wait for me
 	startWG.Done() // start successful, don't wait for me
 
 
 	for {
 	for {
-		server.log.Debugf("[%s] Waiting for a new client. Next Client ID: %d", server.listenInterface, clientID+1)
+		server.log().Debugf("[%s] Waiting for a new client. Next Client ID: %d", server.listenInterface, clientID+1)
 		conn, err := listener.Accept()
 		conn, err := listener.Accept()
-		server.configureLog()
 		clientID++
 		clientID++
 		if err != nil {
 		if err != nil {
 			if e, ok := err.(net.Error); ok && !e.Temporary() {
 			if e, ok := err.(net.Error); ok && !e.Temporary() {
-				server.log.Infof("Server [%s] has stopped accepting new clients", server.listenInterface)
+				server.log().Infof("Server [%s] has stopped accepting new clients", server.listenInterface)
 				// the listener has been closed, wait for clients to exit
 				// the listener has been closed, wait for clients to exit
-				server.log.Infof("shutting down pool [%s]", server.listenInterface)
+				server.log().Infof("shutting down pool [%s]", server.listenInterface)
 				server.clientPool.ShutdownState()
 				server.clientPool.ShutdownState()
 				server.clientPool.ShutdownWait()
 				server.clientPool.ShutdownWait()
 				server.state = ServerStateStopped
 				server.state = ServerStateStopped
 				server.closedListener <- true
 				server.closedListener <- true
 				return nil
 				return nil
 			}
 			}
-			server.mainlog.WithError(err).Info("Temporary error accepting client")
+			server.mainlog().WithError(err).Info("Temporary error accepting client")
 			continue
 			continue
 		}
 		}
 		go func(p Poolable, borrow_err error) {
 		go func(p Poolable, borrow_err error) {
@@ -221,14 +199,14 @@ func (server *server) Start(startWG *sync.WaitGroup) error {
 				server.envelopePool.Return(c.Envelope)
 				server.envelopePool.Return(c.Envelope)
 				server.clientPool.Return(c)
 				server.clientPool.Return(c)
 			} else {
 			} else {
-				server.log.WithError(borrow_err).Info("couldn't borrow a new client")
+				server.log().WithError(borrow_err).Info("couldn't borrow a new client")
 				// we could not get a client, so close the connection.
 				// we could not get a client, so close the connection.
 				conn.Close()
 				conn.Close()
 
 
 			}
 			}
 			// intentionally placed Borrow in args so that it's called in the
 			// intentionally placed Borrow in args so that it's called in the
 			// same main goroutine.
 			// same main goroutine.
-		}(server.clientPool.Borrow(conn, clientID, server.log, server.envelopePool))
+		}(server.clientPool.Borrow(conn, clientID, server.log(), server.envelopePool))
 
 
 	}
 	}
 }
 }
@@ -298,7 +276,7 @@ func (server *server) isShuttingDown() bool {
 // Handles an entire client SMTP exchange
 // Handles an entire client SMTP exchange
 func (server *server) handleClient(client *client) {
 func (server *server) handleClient(client *client) {
 	defer func() {
 	defer func() {
-		server.log.WithFields(map[string]interface{}{
+		server.log().WithFields(map[string]interface{}{
 			"event": "disconnect",
 			"event": "disconnect",
 			"id":    client.ID,
 			"id":    client.ID,
 		}).Info("Disconnect client")
 		}).Info("Disconnect client")
@@ -306,7 +284,7 @@ func (server *server) handleClient(client *client) {
 	}()
 	}()
 
 
 	sc := server.configStore.Load().(ServerConfig)
 	sc := server.configStore.Load().(ServerConfig)
-	server.log.WithFields(map[string]interface{}{
+	server.log().WithFields(map[string]interface{}{
 		"event": "connect",
 		"event": "connect",
 		"id":    client.ID,
 		"id":    client.ID,
 	}).Info("Handle client")
 	}).Info("Handle client")
@@ -332,11 +310,11 @@ func (server *server) handleClient(client *client) {
 	if sc.TLSAlwaysOn {
 	if sc.TLSAlwaysOn {
 		tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 		tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 		if !ok {
 		if !ok {
-			server.mainlog.Error("Failed to load *tls.Config")
+			server.mainlog().Error("Failed to load *tls.Config")
 		} else if err := client.upgradeToTLS(tlsConfig); err == nil {
 		} else if err := client.upgradeToTLS(tlsConfig); err == nil {
 			advertiseTLS = ""
 			advertiseTLS = ""
 		} else {
 		} else {
-			server.log.WithError(err).Warnf("[%s] Failed TLS handshake", client.RemoteIP)
+			server.log().WithError(err).Warnf("[%s] Failed TLS handshake", client.RemoteIP)
 			// server requires TLS, but can't handshake
 			// server requires TLS, but can't handshake
 			client.kill()
 			client.kill()
 		}
 		}
@@ -354,19 +332,19 @@ func (server *server) handleClient(client *client) {
 		case ClientCmd:
 		case ClientCmd:
 			client.bufin.setLimit(CommandLineMaxLength)
 			client.bufin.setLimit(CommandLineMaxLength)
 			input, err := server.readCommand(client, sc.MaxSize)
 			input, err := server.readCommand(client, sc.MaxSize)
-			server.log.Debugf("Client sent: %s", input)
+			server.log().Debugf("Client sent: %s", input)
 			if err == io.EOF {
 			if err == io.EOF {
-				server.log.WithError(err).Warnf("Client closed the connection: %s", client.RemoteIP)
+				server.log().WithError(err).Warnf("Client closed the connection: %s", client.RemoteIP)
 				return
 				return
 			} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
 			} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
-				server.log.WithError(err).Warnf("Timeout: %s", client.RemoteIP)
+				server.log().WithError(err).Warnf("Timeout: %s", client.RemoteIP)
 				return
 				return
 			} else if err == LineLimitExceeded {
 			} else if err == LineLimitExceeded {
 				client.sendResponse(response.Canned.FailLineTooLong)
 				client.sendResponse(response.Canned.FailLineTooLong)
 				client.kill()
 				client.kill()
 				break
 				break
 			} else if err != nil {
 			} else if err != nil {
-				server.log.WithError(err).Warnf("Read error: %s", client.RemoteIP)
+				server.log().WithError(err).Warnf("Read error: %s", client.RemoteIP)
 				client.kill()
 				client.kill()
 				break
 				break
 			}
 			}
@@ -401,6 +379,24 @@ func (server *server) handleClient(client *client) {
 				quote := response.GetQuote()
 				quote := response.GetQuote()
 				client.sendResponse("214-OK\r\n" + quote)
 				client.sendResponse("214-OK\r\n" + quote)
 
 
+			case sc.XClientOn && strings.Index(cmd, "XCLIENT ") == 0:
+				if toks := strings.Split(input[8:], " "); len(toks) > 0 {
+					for i := range toks {
+						if vals := strings.Split(toks[i], "="); len(vals) == 2 {
+							if vals[1] == "[UNAVAILABLE]" {
+								// skip
+								continue
+							}
+							if vals[0] == "ADDR" {
+								client.RemoteIP = vals[1]
+							}
+							if vals[0] == "HELO" {
+								client.Helo = vals[1]
+							}
+						}
+					}
+				}
+				client.sendResponse(response.Canned.SuccessMailCmd)
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 				if client.isInTransaction() {
 				if client.isInTransaction() {
 					client.sendResponse(response.Canned.FailNestedMailCmd)
 					client.sendResponse(response.Canned.FailNestedMailCmd)
@@ -415,7 +411,7 @@ func (server *server) handleClient(client *client) {
 						break
 						break
 					} else {
 					} else {
 						client.MailFrom = from
 						client.MailFrom = from
-						server.log.WithFields(map[string]interface{}{
+						server.log().WithFields(map[string]interface{}{
 							"event":   "mailfrom",
 							"event":   "mailfrom",
 							"helo":    client.Helo,
 							"helo":    client.Helo,
 							"domain":  from.Host,
 							"domain":  from.Host,
@@ -515,7 +511,7 @@ func (server *server) handleClient(client *client) {
 					client.sendResponse(response.Canned.FailReadErrorDataCmd, err.Error())
 					client.sendResponse(response.Canned.FailReadErrorDataCmd, err.Error())
 					client.kill()
 					client.kill()
 				}
 				}
-				server.log.WithError(err).Warn("Error reading data")
+				server.log().WithError(err).Warn("Error reading data")
 				client.resetTransaction()
 				client.resetTransaction()
 				break
 				break
 			}
 			}
@@ -523,7 +519,7 @@ func (server *server) handleClient(client *client) {
 			res := server.backend().Process(client.Envelope)
 			res := server.backend().Process(client.Envelope)
 			if res.Code() < 300 {
 			if res.Code() < 300 {
 				client.messagesSent++
 				client.messagesSent++
-				server.log.WithFields(map[string]interface{}{
+				server.log().WithFields(map[string]interface{}{
 					"helo":          client.Helo,
 					"helo":          client.Helo,
 					"remoteAddress": getRemoteAddr(client.conn),
 					"remoteAddress": getRemoteAddr(client.conn),
 					"success":       true,
 					"success":       true,
@@ -540,12 +536,12 @@ func (server *server) handleClient(client *client) {
 			if !client.TLS && sc.StartTLSOn {
 			if !client.TLS && sc.StartTLSOn {
 				tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 				tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 				if !ok {
 				if !ok {
-					server.mainlog.Error("Failed to load *tls.Config")
+					server.mainlog().Error("Failed to load *tls.Config")
 				} else if err := client.upgradeToTLS(tlsConfig); err == nil {
 				} else if err := client.upgradeToTLS(tlsConfig); err == nil {
 					advertiseTLS = ""
 					advertiseTLS = ""
 					client.resetTransaction()
 					client.resetTransaction()
 				} else {
 				} else {
-					server.log.WithError(err).Warnf("[%s] Failed TLS handshake", client.RemoteIP)
+					server.log().WithError(err).Warnf("[%s] Failed TLS handshake", client.RemoteIP)
 					// Don't disconnect, let the client decide if it wants to continue
 					// Don't disconnect, let the client decide if it wants to continue
 				}
 				}
 			}
 			}
@@ -558,15 +554,37 @@ func (server *server) handleClient(client *client) {
 		}
 		}
 
 
 		if client.bufout.Buffered() > 0 {
 		if client.bufout.Buffered() > 0 {
-			if server.log.IsDebug() {
-				server.log.Debugf("Writing response to client: \n%s", client.response.String())
+			if server.log().IsDebug() {
+				server.log().Debugf("Writing response to client: \n%s", client.response.String())
 			}
 			}
 			err := server.flushResponse(client)
 			err := server.flushResponse(client)
 			if err != nil {
 			if err != nil {
-				server.log.WithError(err).Debug("Error writing response")
+				server.log().WithError(err).Debug("Error writing response")
 				return
 				return
 			}
 			}
 		}
 		}
 
 
 	}
 	}
 }
 }
+
+func (s *server) log() log.Logger {
+	if l, ok := s.logStore.Load().(log.Logger); ok {
+		return l
+	}
+	l, err := log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
+	if err == nil {
+		s.logStore.Store(l)
+	}
+	return l
+}
+
+func (s *server) mainlog() log.Logger {
+	if l, ok := s.mainlogStore.Load().(log.Logger); ok {
+		return l
+	}
+	l, err := log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
+	if err == nil {
+		s.mainlogStore.Store(l)
+	}
+	return l
+}

+ 53 - 9
server_test.go

@@ -38,7 +38,7 @@ func getMockServerConfig() *ServerConfig {
 func getMockServerConn(sc *ServerConfig, t *testing.T) (*mocks.Conn, *server) {
 func getMockServerConn(sc *ServerConfig, t *testing.T) (*mocks.Conn, *server) {
 	var logOpenError error
 	var logOpenError error
 	var mainlog log.Logger
 	var mainlog log.Logger
-	mainlog, logOpenError = log.GetLogger(sc.LogFile)
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
 	if logOpenError != nil {
 	if logOpenError != nil {
 		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
 		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
 	}
 	}
@@ -62,7 +62,7 @@ func TestHandleClient(t *testing.T) {
 	var mainlog log.Logger
 	var mainlog log.Logger
 	var logOpenError error
 	var logOpenError error
 	sc := getMockServerConfig()
 	sc := getMockServerConfig()
-	mainlog, logOpenError = log.GetLogger(sc.LogFile)
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
 	if logOpenError != nil {
 	if logOpenError != nil {
 		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
 		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
 	}
 	}
@@ -93,12 +93,56 @@ func TestHandleClient(t *testing.T) {
 	wg.Wait() // wait for handleClient to exit
 	wg.Wait() // wait for handleClient to exit
 }
 }
 
 
+func TestXClient(t *testing.T) {
+	var mainlog log.Logger
+	var logOpenError error
+	sc := getMockServerConfig()
+	sc.XClientOn = true
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
+	if logOpenError != nil {
+		mainlog.WithError(logOpenError).Errorf("Failed creating a logger for mock conn [%s]", sc.ListenInterface)
+	}
+	conn, server := getMockServerConn(sc, t)
+	// call the serve.handleClient() func in a goroutine.
+	client := NewClient(conn.Server, 1, mainlog, mail.NewPool(5))
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		server.handleClient(client)
+		wg.Done()
+	}()
+	// Wait for the greeting from the server
+	r := textproto.NewReader(bufio.NewReader(conn.Client))
+	line, _ := r.ReadLine()
+	//	fmt.Println(line)
+	w := textproto.NewWriter(bufio.NewWriter(conn.Client))
+	w.PrintfLine("HELO test.test.com")
+	line, _ = r.ReadLine()
+	//fmt.Println(line)
+	w.PrintfLine("XCLIENT ADDR=212.96.64.216 NAME=[UNAVAILABLE]")
+	line, _ = r.ReadLine()
+
+	if client.RemoteIP != "212.96.64.216" {
+		t.Error("client.RemoteIP should be 212.96.64.216, but got:", client.RemoteIP)
+	}
+	expected := "250 2.1.0 OK"
+	if strings.Index(line, expected) != 0 {
+		t.Error("expected", expected, "but got:", line)
+	}
+
+	// try malformed input
+	w.PrintfLine("XCLIENT c")
+	line, _ = r.ReadLine()
+
+	expected = "250 2.1.0 OK"
+	if strings.Index(line, expected) != 0 {
+		t.Error("expected", expected, "but got:", line)
+	}
+
+	w.PrintfLine("QUIT")
+	line, _ = r.ReadLine()
+	wg.Wait() // wait for handleClient to exit
+}
+
 // TODO
 // TODO
 // - test github issue #44 and #42
 // - test github issue #44 and #42
-// - test other commands
-
-// also, could test
-// - test allowsHost() and allowsHost()
-// - test isInTransaction() (make sure it returns true after MAIL command, but false after HELO/EHLO/RSET/end of DATA
-// - test to make sure client envelope
-// - perhaps anything else that can be tested in server_test.go

+ 4 - 5
tests/guerrilla_test.go

@@ -33,9 +33,8 @@ import (
 	"net"
 	"net"
 	"strings"
 	"strings"
 
 
-	"os"
-
 	"github.com/flashmob/go-guerrilla/tests/testcert"
 	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"os"
 )
 )
 
 
 type TestConfig struct {
 type TestConfig struct {
@@ -63,7 +62,7 @@ func init() {
 		initErr = errors.New("Could not Unmarshal config," + err.Error())
 		initErr = errors.New("Could not Unmarshal config," + err.Error())
 	} else {
 	} else {
 		setupCerts(config)
 		setupCerts(config)
-		logger, _ = log.GetLogger(config.LogFile)
+		logger, _ = log.GetLogger(config.LogFile, "debug")
 		backend, _ := getBackend(config.BackendConfig, logger)
 		backend, _ := getBackend(config.BackendConfig, logger)
 		app, _ = guerrilla.New(&config.AppConfig, backend, logger)
 		app, _ = guerrilla.New(&config.AppConfig, backend, logger)
 	}
 	}
@@ -250,7 +249,7 @@ func TestGreeting(t *testing.T) {
 	if read, err := ioutil.ReadFile("./testlog"); err == nil {
 	if read, err := ioutil.ReadFile("./testlog"); err == nil {
 		logOutput := string(read)
 		logOutput := string(read)
 		//fmt.Println(logOutput)
 		//fmt.Println(logOutput)
-		if i := strings.Index(logOutput, "Handle client"); i < 0 {
+		if i := strings.Index(logOutput, "Handle client [127.0.0.1"); i < 0 {
 			t.Error("Server did not handle any clients")
 			t.Error("Server did not handle any clients")
 		}
 		}
 	}
 	}
@@ -309,7 +308,7 @@ func TestShutDown(t *testing.T) {
 	if read, err := ioutil.ReadFile("./testlog"); err == nil {
 	if read, err := ioutil.ReadFile("./testlog"); err == nil {
 		logOutput := string(read)
 		logOutput := string(read)
 		//	fmt.Println(logOutput)
 		//	fmt.Println(logOutput)
-		if i := strings.Index(logOutput, "Handle client"); i < 0 {
+		if i := strings.Index(logOutput, "Handle client [127.0.0.1"); i < 0 {
 			t.Error("Server did not handle any clients")
 			t.Error("Server did not handle any clients")
 		}
 		}
 	}
 	}