Răsfoiți Sursa

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 ani în urmă
părinte
comite
bfe58e2070

+ 2 - 0
.gitignore

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

+ 1 - 1
.travis.yml

@@ -23,4 +23,4 @@ install:
 script:
   - ./.travis.gofmt.sh
   - 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)
 
-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)
 
-### 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,
 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.
 
+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.
 
 ### 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.
-- 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.
-- 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
 
+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
 ============================
-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
-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
-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
 =====================
 
-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.
 
-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.
 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_.
 
 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.
 
-### 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
 
@@ -346,183 +237,56 @@ See the full documentation here:
 |Redis|Saves the email data to Redis.|
 |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 |
 |-----------|-------------|
 |[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!
 
-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
 ========
 
-(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]
 
-Major Contributors:
+Major Contributors: 
 -------------------
 
 * 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:
 ----------
 * https://github.com/dvcrn
 * https://github.com/athoune
+* https://github.com/Xeoncross
 
 ... and anyone else who opened an issue / sent a PR / gave suggestions!

+ 68 - 34
api.go

@@ -10,14 +10,23 @@ import (
 	"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 {
 	Config  *AppConfig
 	Logger  log.Logger
 	Backend backends.Backend
 
+	// Guerrilla will be managed through the API
 	g Guerrilla
 
 	configLoadTime time.Time
+	subs           []deferredSub
+}
+
+type deferredSub struct {
+	topic Event
+	fn    interface{}
 }
 
 const defaultInterface = "127.0.0.1:2525"
@@ -39,11 +48,10 @@ func (d *Daemon) Start() (err error) {
 			return err
 		}
 		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 {
 				return err
 			}
-			d.Logger.SetLevel(d.Config.LogLevel)
 		}
 		if d.Backend == nil {
 			d.Backend, err = backends.New(d.Config.BackendConfig, d.Logger)
@@ -55,6 +63,11 @@ func (d *Daemon) Start() (err error) {
 		if err != nil {
 			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()
 	if err == nil {
@@ -75,41 +88,42 @@ func (d *Daemon) Shutdown() {
 }
 
 // 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) {
+	var ac AppConfig
 	data, err := ioutil.ReadFile(path)
 	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
 // 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 {
 		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
 	}
-	d.configLoadTime = time.Now()
+	d.Config = &c
 	return nil
 }
 
 // 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
 	err := d.SetConfig(c)
 	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
 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 {
 		d.Log().WithError(err).Error("Error while reloading config from file")
 		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.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.
 // 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)
+	return nil
 }
 
 // Subscribe for subscribing to config change events
 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)
 }
 
 // for publishing config change events
 func (d *Daemon) Publish(topic Event, args ...interface{}) {
+	if d.g == nil {
+		return
+	}
 	d.g.Publish(topic, args...)
 }
 
 // for unsubscribing from config change events
 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)
 }
 
@@ -168,13 +199,16 @@ func (d *Daemon) Log() log.Logger {
 		return d.Logger
 	}
 	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
 
 }
@@ -199,7 +233,7 @@ func (d *Daemon) configureDefaults() error {
 // then attaches to the logs once the config is loaded.
 // This will propagate down to the servers / backend too.
 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 {
 		return err
 	}

+ 8 - 8
api_test.go

@@ -257,13 +257,13 @@ func TestReopenLog(t *testing.T) {
 func TestSetConfig(t *testing.T) {
 
 	os.Truncate("test/testlog", 0)
-	cfg := &AppConfig{LogFile: "tests/testlog"}
+	cfg := AppConfig{LogFile: "tests/testlog"}
 	sc := ServerConfig{
 		ListenInterface: "127.0.0.1:2526",
 		IsEnabled:       true,
 	}
 	cfg.Servers = append(cfg.Servers, sc)
-	d := Daemon{Config: cfg}
+	d := Daemon{Config: &cfg}
 
 	// lets add a new server
 	sc.ListenInterface = "127.0.0.1:2527"
@@ -301,13 +301,13 @@ func TestSetConfig(t *testing.T) {
 func TestSetConfigError(t *testing.T) {
 
 	os.Truncate("tests/testlog", 0)
-	cfg := &AppConfig{LogFile: "tests/testlog"}
+	cfg := AppConfig{LogFile: "tests/testlog"}
 	sc := ServerConfig{
 		ListenInterface: "127.0.0.1:2526",
 		IsEnabled:       true,
 	}
 	cfg.Servers = append(cfg.Servers, sc)
-	d := Daemon{Config: cfg}
+	d := Daemon{Config: &cfg}
 
 	// lets add a new server with bad TLS
 	sc.ListenInterface = "127.0.0.1:2527"
@@ -444,7 +444,7 @@ func TestReloadConfig(t *testing.T) {
 	d := Daemon{}
 	d.Start()
 
-	cfg := &AppConfig{
+	cfg := AppConfig{
 		LogFile:      "tests/testlog",
 		AllowedHosts: []string{"grr.la"},
 		BackendConfig: backends.BackendConfig{
@@ -466,7 +466,7 @@ func TestPubSubAPI(t *testing.T) {
 	d.Start()
 
 	// new config
-	cfg := &AppConfig{
+	cfg := AppConfig{
 		PidFile:      "tests/pidfilex.pid",
 		LogFile:      "tests/testlog",
 		AllowedHosts: []string{"grr.la"},
@@ -490,7 +490,7 @@ func TestPubSubAPI(t *testing.T) {
 
 	d.Unsubscribe(EventConfigPidFile, pidEvHandler)
 	cfg.PidFile = "tests/pidfile2.pid"
-	d.Publish(EventConfigPidFile, cfg)
+	d.Publish(EventConfigPidFile, &cfg)
 	d.ReloadConfig(cfg)
 
 	b, err := ioutil.ReadFile("tests/testlog")
@@ -510,7 +510,7 @@ func TestAPILog(t *testing.T) {
 	d := Daemon{}
 	l := d.Log()
 	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())
 	}
 	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 {
 		return v
 	}
-	l, _ := log.GetLogger(log.OutputStderr.String())
+	l, _ := log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
 	return l
 }
 

+ 128 - 66
backends/gateway.go

@@ -44,10 +44,10 @@ type GatewayConfig struct {
 	SaveProcess string `json:"save_process,omitempty"`
 	// ValidateProcess is like ProcessorStack, but for recipient validation tasks
 	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
@@ -109,18 +109,37 @@ func New(backendConfig BackendConfig, l log.Logger) (Backend, error) {
 	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 {
 	if gw.State != BackendStateRunning {
 		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
-	savedNotify := make(chan *notifyMsg)
-	gw.conveyor <- &workerMsg{e, savedNotify, TaskSaveMail}
+	gw.conveyor <- workerMsg
 	// wait for the save to complete
 	// or timeout
 	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 {
 			return NewResult(response.Canned.FailBackendTransaction + status.err.Error())
 		}
@@ -138,13 +157,18 @@ func (gw *BackendGateway) ValidateRcpt(e *mail.Envelope) RcptError {
 	if gw.State != BackendStateRunning {
 		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
-	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
 	// or timeout
 	select {
-	case status := <-notify:
+	case status := <-workerMsg.notifyMe:
 		if status.err != nil {
 			return status.err
 		}
@@ -179,7 +203,7 @@ func (gw *BackendGateway) Reinitialize() error {
 	if gw.State != BackendStateShuttered {
 		return errors.New("backend must be in BackendStateshuttered state to Reinitialize")
 	}
-	//
+	// clear the Initializers and Shutdowners
 	Svc.reset()
 
 	err := gw.Initialize(gw.config)
@@ -191,7 +215,7 @@ func (gw *BackendGateway) Reinitialize() error {
 	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_*
 // 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
@@ -199,7 +223,8 @@ func (gw *BackendGateway) newStack(stackConfig string) (Processor, error) {
 	var decorators []Decorator
 	cfg := strings.ToLower(strings.TrimSpace(stackConfig))
 	if len(cfg) == 0 {
-		cfg = strings.ToLower(defaultProcessor)
+		//cfg = strings.ToLower(defaultProcessor)
+		return NoopProcessor{}, nil
 	}
 	items := strings.Split(cfg, "|")
 	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")
 	}
 	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
-			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
 			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
@@ -293,12 +318,18 @@ func (gw *BackendGateway) Start() error {
 			stop := make(chan bool)
 			go func(workerId int, stop chan bool) {
 				// 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()
 			}(i, 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
 // Returns 1 if no config value was set
 func (gw *BackendGateway) workersSize() int {
-	if gw.gwConfig.WorkersSize == 0 {
+	if gw.gwConfig.WorkersSize <= 0 {
 		return 1
 	}
 	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
 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 time.Duration(gw.gwConfig.TimeoutSave)
+	return t
 }
 
 // validateRcptTimeout returns the maximum amount of seconds to wait before timing out a recipient validation  task
 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 time.Duration(gw.gwConfig.TimeoutValidateRcpt)
+	return t
 }
 
+type dispatcherState int
+
+const (
+	dispatcherStateStopped dispatcherState = iota
+	dispatcherStateIdle
+	dispatcherStateWorking
+	dispatcherStateNotify
+	dispatcherStatePanic
+)
+
 func (gw *BackendGateway) workDispatcher(
 	workIn chan *workerMsg,
 	save Processor,
 	validate Processor,
 	workerId int,
-	stop chan bool) {
+	stop chan bool) (state dispatcherState) {
+
+	var msg *workerMsg
 
 	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 {
-			// 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)
 	for {
 		select {
 		case <-stop:
+			state = dispatcherStateStopped
 			Log().Infof("stop signal for worker (#%d)", workerId)
 			return
-		case msg := <-workIn:
-			if msg == nil {
-				Log().Debugf("worker stopped (#%d)", workerId)
-				return
-			}
+		case msg = <-workIn:
 			msg.e.Lock()
+			state = dispatcherStateWorking
 			if msg.task == TaskSaveMail {
 				// process the email here
-				// TODO we should check the err
 				result, _ := save.Process(msg.e, TaskSaveMail)
+				state = dispatcherStateNotify
 				if result.Code() < 300 {
 					// if all good, let the gateway know that it was queued
 					msg.notifyMe <- &notifyMsg{nil, msg.e.QueuedId}
@@ -376,6 +436,7 @@ func (gw *BackendGateway) workDispatcher(
 				}
 			} else if msg.task == TaskValidateRcpt {
 				_, err := validate.Process(msg.e, TaskValidateRcpt)
+				state = dispatcherStateNotify
 				if err != nil {
 					// validation failed
 					msg.notifyMe <- &notifyMsg{err: err}
@@ -386,6 +447,7 @@ func (gw *BackendGateway) workDispatcher(
 			}
 			msg.e.Unlock()
 		}
+		state = dispatcherStateIdle
 	}
 }
 

+ 1 - 1
backends/gateway_test.go

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

+ 106 - 41
backends/p_mysql.go

@@ -2,6 +2,7 @@ package backends
 
 import (
 	"database/sql"
+	"fmt"
 	"strings"
 	"time"
 
@@ -9,6 +10,8 @@ import (
 	"github.com/go-sql-driver/mysql"
 
 	"github.com/flashmob/go-guerrilla/response"
+	"math/big"
+	"net"
 	"runtime/debug"
 )
 
@@ -43,7 +46,7 @@ const procMySQLReadTimeout = time.Second * 10
 const procMySQLWriteTimeout = time.Second * 10
 
 type MysqlProcessorConfig struct {
-	MysqlTable  string `json:"mail_table"`
+	MysqlTable  string `json:"mysql_mail_table"`
 	MysqlDB     string `json:"mysql_db"`
 	MysqlHost   string `json:"mysql_host"`
 	MysqlPass   string `json:"mysql_pass"`
@@ -74,9 +77,9 @@ func (m *MysqlProcessor) connect(config *MysqlProcessorConfig) (*sql.DB, error)
 		return nil, err
 	}
 	// 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 {
-		Log().Error("cannot select table", err)
+		//Log().Error("cannot select table", err)
 		return nil, err
 	}
 	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]
 	}
 	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
 	comma := ""
 	for i := 0; i < rows; i++ {
@@ -112,8 +117,7 @@ func (g *MysqlProcessor) prepareInsertQuery(rows int, db *sql.DB) *sql.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() {
 		if r := recover(); r != nil {
 			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 {
 		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 {
@@ -140,8 +168,9 @@ func MySql() Decorator {
 	var config *MysqlProcessorConfig
 	var vals []interface{}
 	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 {
 		configType := BaseConfig(&MysqlProcessorConfig{})
 		bcfg, err := Svc.ExtractConfig(backendConfig, configType)
@@ -149,16 +178,15 @@ func MySql() Decorator {
 			return err
 		}
 		config = bcfg.(*MysqlProcessorConfig)
-		mp.config = config
-		db, err = mp.connect(config)
+		m.config = config
+		db, err = m.connect(config)
 		if err != nil {
-			Log().Errorf("cannot open mysql: %s", err)
 			return err
 		}
 		return nil
 	}))
 
-	// shutdown
+	// shutdown will close the database connection
 	Svc.AddShutdowner(ShutdownWith(func() error {
 		if db != nil {
 			return db.Close()
@@ -171,9 +199,10 @@ func MySql() Decorator {
 
 			if task == TaskSaveMail {
 				var to, body string
-				to = trimToLimit(strings.TrimSpace(e.RcptTo[0].User)+"@"+config.PrimaryHost, 255)
+
 				hash := ""
 				if len(e.Hashes) > 0 {
+					// if saved in redis, hash will be the redis key
 					hash = e.Hashes[0]
 					e.QueuedId = e.Hashes[0]
 				}
@@ -189,33 +218,68 @@ func MySql() Decorator {
 					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
 				return p.Process(e, task)
 			} else if task == TaskValidateRcpt {
@@ -225,10 +289,11 @@ func MySql() Decorator {
 					// validate only the _last_ recipient that was appended
 					last := e.RcptTo[len(e.RcptTo)-1]
 					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)
 			} else {
 				return p.Process(e, task)

+ 10 - 11
backends/p_redis.go

@@ -92,7 +92,6 @@ func Redis() Decorator {
 				if len(e.Hashes) > 0 {
 					e.QueuedId = e.Hashes[0]
 					hash = e.Hashes[0]
-
 					var stringer fmt.Stringer
 					// a compressor was set
 					if c, ok := e.Values["zlib-compressor"]; ok {
@@ -101,22 +100,22 @@ func Redis() Decorator {
 						stringer = e
 					}
 					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 {
-						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)
 						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 {
 					Log().Error("Redis needs a Hash() process before it")
+					result := NewResult(response.Canned.FailBackendTransaction)
+					return result, StorageError
 				}
 
 				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) {
 	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.
 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 {
 	var headerSectionEnds int
 	for i, char := range mailData[:len(mailData)-4] {
@@ -25,7 +26,6 @@ func ParseHeaders(mailData string) map[string]string {
 		}
 	}
 	headers := make(map[string]string)
-	// TODO header comments and textproto Reader instead of regex
 	matches := headerRegex.FindAllStringSubmatch(mailData[:headerSectionEnds], -1)
 	for _, h := range matches {
 		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 (
 	NoSuchUser          = RcptError(errors.New("no such user"))
 	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"))
 	UserSuspended       = RcptError(errors.New("user suspended"))
+	StorageError        = RcptError(errors.New("storage error"))
 )

+ 2 - 3
client.go

@@ -5,13 +5,12 @@ import (
 	"bytes"
 	"crypto/tls"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/flashmob/go-guerrilla/mail"
 	"net"
 	"net/textproto"
 	"sync"
 	"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.

+ 2 - 2
cmd/guerrillad/root.go

@@ -7,8 +7,8 @@ import (
 
 var rootCmd = &cobra.Command{
 	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.`,
 	Run: nil,
 }

+ 26 - 42
cmd/guerrillad/serve.go

@@ -1,8 +1,10 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/log"
+	"github.com/spf13/cobra"
 	"os"
 	"os/exec"
 	"os/signal"
@@ -10,10 +12,6 @@ import (
 	"strings"
 	"syscall"
 	"time"
-
-	"github.com/flashmob/go-guerrilla"
-	"github.com/flashmob/go-guerrilla/log"
-	"github.com/spf13/cobra"
 )
 
 const (
@@ -26,11 +24,10 @@ var (
 
 	serveCmd = &cobra.Command{
 		Use:   "serve",
-		Short: "start the small SMTP server",
+		Short: "start the daemon and start all available servers",
 		Run:   serve,
 	}
 
-	cmdConfig     = CmdConfig{}
 	signalChannel = make(chan os.Signal, 1) // for trapping SIGHUP and friends
 	mainlog       log.Logger
 
@@ -40,12 +37,16 @@ var (
 func init() {
 	// log to stderr on startup
 	var err error
-	mainlog, err = log.GetLogger(log.OutputStderr.String())
+	mainlog, err = log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
 	if err != nil {
 		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",
-		"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
 	serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
 		"", "Path to the pid file")
@@ -63,7 +64,11 @@ func sigHandler() {
 	)
 	for sig := range signalChannel {
 		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 {
 			d.ReopenLogs()
 		} else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT {
@@ -89,22 +94,22 @@ func sigHandler() {
 func serve(cmd *cobra.Command, args []string) {
 	logVersion()
 	d = guerrilla.Daemon{Logger: mainlog}
-	err := readConfig(configPath, pidFile)
+	ac, err := readConfig(configPath, pidFile)
 	if err != nil {
 		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.
 	fileLimit := getFileLimit()
 
 	if fileLimit > 0 {
 		maxClients := 0
-		for _, s := range cmdConfig.Servers {
+		for _, s := range ac.Servers {
 			maxClients += s.MaxClients
 		}
 		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)
 		}
 	}
@@ -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.
 	// Note here is the only place we can make an exception to the
 	// "treat config values as immutable". For example, here the
 	// command line flags can override config values
 	appConfig, err := d.LoadConfig(path)
 	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
 	if len(pidFile) > 0 {
@@ -157,8 +139,10 @@ func readConfig(path string, pidFile string) error {
 	} else if len(appConfig.PidFile) == 0 {
 		appConfig.PidFile = defaultPidFile
 	}
-	d.SetConfig(&appConfig)
-	return nil
+	if verbose {
+		appConfig.LogLevel = "debug"
+	}
+	return &appConfig, nil
 }
 
 func getFileLimit() int {

+ 51 - 51
cmd/guerrillad/serve_test.go

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

+ 5 - 3
config.go

@@ -5,13 +5,12 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
 	"os"
 	"reflect"
 	"strings"
 
-	"github.com/flashmob/go-guerrilla/backends"
-	"github.com/flashmob/go-guerrilla/log"
-
 	"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".
 	// defaults to AppConfig.Log file setting
 	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
 	_privateKeyFile_mtime int

+ 3 - 3
config_test.go

@@ -196,7 +196,7 @@ func TestConfigChangeEvents(t *testing.T) {
 
 	oldconf := &AppConfig{}
 	oldconf.Load([]byte(configJsonA))
-	logger, _ := log.GetLogger(oldconf.LogFile)
+	logger, _ := log.GetLogger(oldconf.LogFile, oldconf.LogLevel)
 	bcfg := backends.BackendConfig{"log_received_mails": true}
 	backend, err := backends.New(bcfg, logger)
 	if err != nil {
@@ -213,8 +213,8 @@ func TestConfigChangeEvents(t *testing.T) {
 	os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now())
 	newconf := &AppConfig{}
 	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"
 	expectedEvents := map[Event]bool{
 		EventConfigPidFile:         false,

+ 2 - 0
event.go

@@ -41,6 +41,8 @@ const (
 	EventConfigServerMaxClients
 	// when a server's TLS config changed
 	EventConfigServerTLSConfig
+	//
+	EventConfigPostLoad
 )
 
 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:
 - name: github.com/asaskevich/EventBus
   version: ab9e5ceb2cc1ca6f36a5813c928c534e837681c2
@@ -23,11 +23,11 @@ imports:
   subpackages:
   - fs
 - name: github.com/Sirupsen/logrus
-  version: 0208149b40d863d2c1a2f8fe5753096a9cf2cc8b
+  version: ba1b36c82c5e05c4f912a88eab0dcd91a171688f
 - name: github.com/spf13/cobra
-  version: 16c014f1a19d865b765b420e74508f80eb831ada
+  version: b62566898a99f2db9c68ed0026aa0a052e59678d
 - name: github.com/spf13/pflag
-  version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7
+  version: 25f8b5b07aece3207895bf19f7ab517eb3b22a40
 - name: golang.org/x/sys
   version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea
   subpackages:

+ 8 - 8
goguerrilla.conf.sample

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

+ 22 - 21
guerrilla.go

@@ -3,13 +3,11 @@ package guerrilla
 import (
 	"errors"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/log"
 	"os"
 	"sync"
 	"sync/atomic"
-
-	"github.com/flashmob/go-guerrilla/backends"
-	"github.com/flashmob/go-guerrilla/dashboard"
-	"github.com/flashmob/go-guerrilla/log"
 )
 
 const (
@@ -69,7 +67,7 @@ func (ls *logStore) mainlog() log.Logger {
 	if v, ok := ls.Load().(log.Logger); ok {
 		return v
 	}
-	l, _ := log.GetLogger(log.OutputStderr.String())
+	l, _ := log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
 	return l
 }
 
@@ -88,7 +86,11 @@ func New(ac *AppConfig, b backends.Backend, l log.Logger) (Guerrilla, error) {
 	g.setMainlog(l)
 
 	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
@@ -206,7 +208,7 @@ func (g *guerrilla) subscribeEvents() {
 	g.Subscribe(EventConfigLogFile, func(c *AppConfig) {
 		var err error
 		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.mapServers(func(server *server) {
 				// 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
 	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
@@ -322,7 +327,8 @@ func (g *guerrilla) subscribeEvents() {
 		if server, err := g.findServer(sc.ListenInterface); err == nil {
 			var err error
 			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)
 				backends.Svc.SetMainlog(l)
 				// 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
 	g.Subscribe(EventConfigServerLogReopen, func(sc *ServerConfig) {
 		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)
 		}
 	})
 	// when the backend changes
 	g.Subscribe(EventConfigBackendConfig, func(appConfig *AppConfig) {
-		logger, _ := log.GetLogger(appConfig.LogFile)
+		logger, _ := log.GetLogger(appConfig.LogFile, appConfig.LogLevel)
 		// shutdown the backend first.
 		var err error
 		if err = g.backend().Shutdown(); err != nil {
@@ -437,10 +443,6 @@ func (g *guerrilla) Start() error {
 	// wait for all servers to start (or fail)
 	startWG.Wait()
 
-	if g.Config.Dashboard.Enabled {
-		go dashboard.Run(&g.Config.Dashboard)
-	}
-
 	// close, then read any errors
 	close(errs)
 	for err := range errs {
@@ -456,7 +458,7 @@ func (g *guerrilla) Start() error {
 
 func (g *guerrilla) Shutdown() {
 
-	// shot down the servers first
+	// shut down the servers first
 	g.mapServers(func(s *server) {
 		if s.state == ServerStateRunning {
 			s.Shutdown()
@@ -478,7 +480,6 @@ func (g *guerrilla) Shutdown() {
 
 // SetLogger sets the logger for the app and propagates it to sub-packages (eg.
 func (g *guerrilla) SetLogger(l log.Logger) {
-	l.SetLevel(g.Config.LogLevel)
 	g.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
 
 import (
-	"bufio"
+	log "github.com/Sirupsen/logrus"
 	"io"
 	"io/ioutil"
 	"net"
 	"os"
-	"strings"
 	"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 {
 	log.FieldLogger
 	WithConn(conn net.Conn) *log.Entry
@@ -33,9 +70,18 @@ type HookedLogger struct {
 	*log.Logger
 
 	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
 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
 // 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()
 	defer loggers.Unlock()
+	key := loggerKey{dest, level}
 	if loggers.cache == nil {
 		loggers.cache = make(loggerCache, 1)
 	} else {
-		if l, ok := loggers.cache[dest]; ok {
+		if l, ok := loggers.cache[key]; ok {
 			// return the one we found in the cache
 			return l, nil
 		}
 	}
-	logrus := log.New()
-	// we'll use the hook to output instead
-	logrus.Out = ioutil.Discard
-
-	l := &HookedLogger{}
+	o := parseOutputOption(dest)
+	logrus, err := newLogrus(o, level)
+	if err != nil {
+		return nil, err
+	}
+	l := &HookedLogger{dest: dest}
 	l.Logger = logrus
 
 	// 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
 	if h, err := NewLogrusHook(dest); err != nil {
 		// revert back to stderr
@@ -86,13 +139,40 @@ func GetLogger(dest string) (Logger, error) {
 		l.h = h
 	}
 
-	// add the dashboard hook
-	logrus.Hooks.Add(dashboard.LogHook)
-
 	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
 func (l *HookedLogger) AddHook(h log.Hook) {
 	log.AddHook(h)
@@ -109,7 +189,6 @@ func (l *HookedLogger) SetLevel(level string) {
 	if logLevel, err = log.ParseLevel(level); err != nil {
 		return
 	}
-	l.Level = logLevel
 	log.SetLevel(logLevel)
 }
 
@@ -120,12 +199,15 @@ func (l *HookedLogger) GetLevel() string {
 
 // Reopen closes the log file and re-opens it
 func (l *HookedLogger) Reopen() error {
+	if l.h == nil {
+		return nil
+	}
 	return l.h.Reopen()
 }
 
-// Fgetname Gets the file name
+// GetLogDest Gets the file name
 func (l *HookedLogger) GetLogDest() string {
-	return l.h.GetLogDest()
+	return l.dest
 }
 
 // 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)
 }
-
-// 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/ioutil"
 	"mime/quotedprintable"
+	"net/mail"
 	"net/textproto"
 	"regexp"
 	"strings"
@@ -18,7 +19,7 @@ import (
 	"time"
 )
 
-const maxHeaderChunk = iota + (3 << 10) // 3KB
+const maxHeaderChunk = 1 + (3 << 10) // 3KB
 
 // Address encodes an email address of the form `<user@host>`
 type Address struct {
@@ -34,6 +35,26 @@ func (ep *Address) IsEmpty() bool {
 	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.
 type Envelope struct {
 	// Remote IP address
@@ -130,25 +151,27 @@ func (e *Envelope) String() 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() {
 	e.MailFrom = Address{}
 	e.RcptTo = []Address{}
 	// reset the data buffer, keep it allocated
 	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.RemoteIP = RemoteIP
-	e.Helo = ""
 	e.Header = nil
-	e.TLS = false
 	e.Hashes = make([]string, 0)
 	e.DeliveryHeader = ""
 	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.Helo = ""
+	e.TLS = false
 }
 
 // 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)
 	}
 }
+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) {
 	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.
 func (p *Pool) Return(c Poolable) {
+	p.activeClientsRemove(c)
 	select {
 	case p.pool <- c:
 	default:
 		// hasta la vista, baby...
 	}
-	p.activeClientsRemove(c)
+
 	<-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 Walter Sobchak: Okay then.",
 	14: "214-Private Snoop: you see what happens lebowski?" + CRLF +
-		"214-The Dude: nobody calls me lebowski, you got the wrong guy, I'm the the dude, man." + CRLF +
+		"214-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-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!",

+ 72 - 54
server.go

@@ -55,8 +55,6 @@ type server struct {
 	closedListener  chan (bool)
 	hosts           allowedHosts // stores map[string]bool for faster lookup
 	state           int
-	mainlog         log.Logger
-	log             log.Logger
 	// If log changed after a config reload, newLogStore stores the value here until it's safe to change it
 	logStore     atomic.Value
 	mainlogStore atomic.Value
@@ -76,24 +74,22 @@ func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, err
 		closedListener:  make(chan (bool), 1),
 		listenInterface: sc.ListenInterface,
 		state:           ServerStateNew,
-		mainlog:         l,
 		envelopePool:    mail.NewPool(sc.MaxClients),
 	}
+	server.logStore.Store(l)
 	server.backendStore.Store(b)
-	var logOpenError error
-	if sc.LogFile == "" {
+	logFile := sc.LogFile
+	if logFile == "" {
 		// none set, use the same log file as mainlog
-		server.log, logOpenError = log.GetLogger(server.mainlog.GetLogDest())
-	} else {
-		server.log, logOpenError = log.GetLogger(sc.LogFile)
+		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 {
-		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.setTimeout(sc.Timeout)
 	if err := server.configureSSL(); err != nil {
@@ -120,24 +116,7 @@ func (s *server) configureSSL() error {
 	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) {
 	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())
 	}
 
-	server.log.Infof("Listening on TCP %s", server.listenInterface)
+	server.log().Infof("Listening on TCP %s", server.listenInterface)
 	server.state = ServerStateRunning
 	startWG.Done() // start successful, don't wait for me
 
 	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()
-		server.configureLog()
 		clientID++
 		if err != nil {
 			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
-				server.log.Infof("shutting down pool [%s]", server.listenInterface)
+				server.log().Infof("shutting down pool [%s]", server.listenInterface)
 				server.clientPool.ShutdownState()
 				server.clientPool.ShutdownWait()
 				server.state = ServerStateStopped
 				server.closedListener <- true
 				return nil
 			}
-			server.mainlog.WithError(err).Info("Temporary error accepting client")
+			server.mainlog().WithError(err).Info("Temporary error accepting client")
 			continue
 		}
 		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.clientPool.Return(c)
 			} 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.
 				conn.Close()
 
 			}
 			// intentionally placed Borrow in args so that it's called in the
 			// 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
 func (server *server) handleClient(client *client) {
 	defer func() {
-		server.log.WithFields(map[string]interface{}{
+		server.log().WithFields(map[string]interface{}{
 			"event": "disconnect",
 			"id":    client.ID,
 		}).Info("Disconnect client")
@@ -306,7 +284,7 @@ func (server *server) handleClient(client *client) {
 	}()
 
 	sc := server.configStore.Load().(ServerConfig)
-	server.log.WithFields(map[string]interface{}{
+	server.log().WithFields(map[string]interface{}{
 		"event": "connect",
 		"id":    client.ID,
 	}).Info("Handle client")
@@ -332,11 +310,11 @@ func (server *server) handleClient(client *client) {
 	if sc.TLSAlwaysOn {
 		tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 		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 {
 			advertiseTLS = ""
 		} 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
 			client.kill()
 		}
@@ -354,19 +332,19 @@ func (server *server) handleClient(client *client) {
 		case ClientCmd:
 			client.bufin.setLimit(CommandLineMaxLength)
 			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 {
-				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
 			} 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
 			} else if err == LineLimitExceeded {
 				client.sendResponse(response.Canned.FailLineTooLong)
 				client.kill()
 				break
 			} 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()
 				break
 			}
@@ -401,6 +379,24 @@ func (server *server) handleClient(client *client) {
 				quote := response.GetQuote()
 				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:
 				if client.isInTransaction() {
 					client.sendResponse(response.Canned.FailNestedMailCmd)
@@ -415,7 +411,7 @@ func (server *server) handleClient(client *client) {
 						break
 					} else {
 						client.MailFrom = from
-						server.log.WithFields(map[string]interface{}{
+						server.log().WithFields(map[string]interface{}{
 							"event":   "mailfrom",
 							"helo":    client.Helo,
 							"domain":  from.Host,
@@ -515,7 +511,7 @@ func (server *server) handleClient(client *client) {
 					client.sendResponse(response.Canned.FailReadErrorDataCmd, err.Error())
 					client.kill()
 				}
-				server.log.WithError(err).Warn("Error reading data")
+				server.log().WithError(err).Warn("Error reading data")
 				client.resetTransaction()
 				break
 			}
@@ -523,7 +519,7 @@ func (server *server) handleClient(client *client) {
 			res := server.backend().Process(client.Envelope)
 			if res.Code() < 300 {
 				client.messagesSent++
-				server.log.WithFields(map[string]interface{}{
+				server.log().WithFields(map[string]interface{}{
 					"helo":          client.Helo,
 					"remoteAddress": getRemoteAddr(client.conn),
 					"success":       true,
@@ -540,12 +536,12 @@ func (server *server) handleClient(client *client) {
 			if !client.TLS && sc.StartTLSOn {
 				tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config)
 				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 {
 					advertiseTLS = ""
 					client.resetTransaction()
 				} 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
 				}
 			}
@@ -558,15 +554,37 @@ func (server *server) handleClient(client *client) {
 		}
 
 		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)
 			if err != nil {
-				server.log.WithError(err).Debug("Error writing response")
+				server.log().WithError(err).Debug("Error writing response")
 				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) {
 	var logOpenError error
 	var mainlog log.Logger
-	mainlog, logOpenError = log.GetLogger(sc.LogFile)
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
 	if logOpenError != nil {
 		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 logOpenError error
 	sc := getMockServerConfig()
-	mainlog, logOpenError = log.GetLogger(sc.LogFile)
+	mainlog, logOpenError = log.GetLogger(sc.LogFile, "debug")
 	if logOpenError != nil {
 		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
 }
 
+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
 // - 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"
 	"strings"
 
-	"os"
-
 	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"os"
 )
 
 type TestConfig struct {
@@ -63,7 +62,7 @@ func init() {
 		initErr = errors.New("Could not Unmarshal config," + err.Error())
 	} else {
 		setupCerts(config)
-		logger, _ = log.GetLogger(config.LogFile)
+		logger, _ = log.GetLogger(config.LogFile, "debug")
 		backend, _ := getBackend(config.BackendConfig, 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 {
 		logOutput := string(read)
 		//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")
 		}
 	}
@@ -309,7 +308,7 @@ func TestShutDown(t *testing.T) {
 	if read, err := ioutil.ReadFile("./testlog"); err == nil {
 		logOutput := string(read)
 		//	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")
 		}
 	}