瀏覽代碼

version 1.1

Clomode 13 年之前
父節點
當前提交
8f34f73d7d
共有 3 個文件被更改,包括 417 次插入215 次删除
  1. 181 19
      README.md
  2. 3 4
      goguerrilla.conf.sample
  3. 233 192
      goguerrilla.go

+ 181 - 19
README.md

@@ -1,33 +1,195 @@
 
 
 Go-Guerrilla SMTPd
 Go-Guerrilla SMTPd
+====================
+
 An minimalist SMTP server written in Go, made for receiving large volumes of mail.
 An minimalist SMTP server written in Go, made for receiving large volumes of mail.
 
 
 ![Go Guerrilla](https://raw.github.com/flashmob/go-guerrilla/master/GoGuerrilla.png)
 ![Go Guerrilla](https://raw.github.com/flashmob/go-guerrilla/master/GoGuerrilla.png)
 
 
-What is Go Guerrilla SMTPd?
-It's a small SMTP server written in Go, optimized for receiving email.
+### What is Go Guerrilla SMTPd?
+
+It's a small SMTP server written in Go, for the purpose of receiving large volume of email.
 Written for GuerrillaMail.com which processes tens of thousands of emails
 Written for GuerrillaMail.com which processes tens of thousands of emails
 every hour.
 every hour.
 
 
-Version: 1.0
-Author: Flashmob, GuerrillaMail.com
-Contact: [email protected]
-License: MIT
-Repository: https://github.com/flashmob/Go-Guerrilla-SMTPd
-Site: http://www.guerrillamail.com/
+The purpose of this daemon is to grab the email, save it to the database
+and disconnect as quickly as possible.
+
+A typical user of this software would probably want to customize the saveMail function for
+their own systems.
+
+This server does not attempt to filter HTML, check for spam or do any sender 
+verification. These steps should be performed by other programs.
+The server does NOT send any email including bounces. This should
+be performed by a separate program.
+
+
+### So what's the story?
+
+Originally, Guerrilla Mail was running the Exim mail server, and the emails
+were fetched using POP.
+
+This proved to be inefficient and unreliable, so the system was replaced in 
+favour of piping the emails directly in to a PHP script.
+
+Soon, the piping solution  became a problem too; it required a new process to 
+be started for each arriving email, and it also required a new database 
+connection every time. 
+
+So, how was the bottleneck eliminated? Conveniently, PHP has a socket 
+library which means we can use PHP to quickly prototype a simple SMTP server.
+If the server runs as a daemon, then the system doesn't need to launch a new 
+process for each incoming email. It also doesn't need to run and insane amount 
+of checks for each connection (eg, NS Lookups, white-lists, black-lists, SPF
+domain keys, Spam Assassin, etc).
+
+We only need to open a single database connection and a single process can be 
+re-used indefinitely. The PHP server was able to multiplex simultaneous 
+connections without the need for forking/threading thanks to socket_select()
+
+Therefore, we could receive, process and store email all in the one process.
+The performance improvement has been dramatic. 
+
+However, a few months later, the volume of email increased again and
+this time our server's CPU was under pressure. You see, the problem was that
+the SMTP server was checking the sockets in a loop all the time. This is fine
+if you have a few sockets, but horrible if you have 1000! A common way to solve
+this is to have a connection per thread - and a thread can sleep while it is
+waiting for a socket, not polling all the time.
+
+Threads are great if you have many CPU cores, but our server only had two.
+Besides, our process was I/O bound - there's a better way than to block.
+So how to solve this one? 
+
+Instead of polling/blocking in an infinite loop to see if there are any sockets to be 
+operated, it was more efficent to be notified when the sockets are ready.
+This Wikipedia entry explains it best, 
+http://en.wikipedia.org/wiki/Asynchronous_I/O see "Select(/poll) loops"
+
+Luck had it that an extension is available for PHP! One night later, and version 2 was made
+to use libevent http://pecl.php.net/package/libevent
+
+The improvement was superb. The load average has decreased
+substantially, freeing the CPU for other tasks. It was even surprising to see that
+PHP could handle so many emails.
+
+Fast forward to 2012, to where we are now. Golang 1.0 was released, so it was
+decided to give it a go.
+
+As a language, Go is brilliant for writing server back-ends, with support for concurrency. 
+There's a nice tutorial for getting started on the Golang.org website. 
+There were a lot of great discoveries along the way, particulary the 'channels' 
+in Go can be simple, yet very powerful and the defer statement is quite convenient. 
+It looks like there's a wealth of packages available for
+almost everything, including MySQL.
+
+
+
+HOW TO USE / Installation:
+===========================
+
+To build, you will need to install the following Go libs:
+
+$ go get github.com/ziutek/mymysql/thrsafe
+$ go get github.com/ziutek/mymysql/autorc
+$ go get github.com/ziutek/mymysql/godrv
+$ go get github.com/sloonz/go-iconv
+$ go get github.com/garyburd/redigo/redis
+
+Rename goguerrilla.conf.sample to goguerrilla.conf
+
+Setup the following database:
+
+
+
+Configuration
+============================================
+The configuration is in strict JSON format. Here is an anotated configuration
+
+	{
+	    "GM_ALLOWED_HOSTS":"example.com,sample.com,foo.com,bar.com", // which domains accept mail
+	    "GM_MAIL_TABLE":"new_mail", // name of new email table
+	    "GM_PRIMARY_MAIL_HOST":"mail.example.com", // given in the SMTP greeting
+	    "GSMTP_HOST_NAME":"mail.example.com", // given in the SMTP greeting
+	    "GSMTP_LOG_FILE":"/dev/stdout", // not used yet
+	    "GSMTP_MAX_SIZE":"131072", // max size of DATA command
+	    "GSMTP_PRV_KEY":"/etc/ssl/private/example.com.key", // private key for TLS
+	    "GSMTP_PUB_KEY":"/etc/ssl/certs/example.com.crt", // public key for TLS
+	    "GSMTP_TIMEOUT":"100", // tcp connection timeout
+	    "GSMTP_VERBOSE":"N", // set to Y for debugging
+	    "GSTMP_LISTEN_INTERFACE":"5.9.7.183:25",
+	    "MYSQL_DB":"gmail_mail", // database name
+	    "MYSQL_HOST":"127.0.0.1:3306", // database connect
+	    "MYSQL_PASS":"$ecure1t", // database connection pass
+	    "MYSQL_USER":"gmail_mail", // database username
+	    "GM_CONN_BACKLOG":"100", // connection backlog queue
+	    "GM_MAX_CLIENTS":"500", // max clients that can be handled
+	    "SGID":"508", // group id of the user from /etc/passwd
+		"GUID":"504" // uid from /etc/passwd
+	}
+
+Using Nginx as a proxy
+=========================================================
+Nginx can be used to proxy SMTP traffic for GoGuerrilla SMTPd
+
+Why proxy SMTP?
+
+ - Terminate TLS connections: Golang is not there yet when it comes to TLS.
+At present, only a partial implementation of TLS is provided (as of Nov 2012). 
+OpenSSL on the other hand, used in Nginx, has a complete implementation of
+ SSL v2/v3 and TLS protocols
+
+- Could be used for load balancing and authentication in the future.
+
+The following Nginx proxy configuration:
+
+
+	mail {
+	
+	        auth_http 127.0.0.1:8025/; # This is the URL to GoGuerrilla's http service which tells Nginx where to proxy the traffic to 
+									
+	        server {
+	                listen  5.9.7.183:25;
+	                protocol smtp;
+	                server_name  ak47.guerrillamail.com;
+	
+	                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;
+	                ssl_protocols  SSLv2 SSLv3 TLSv1;
+	                ssl_ciphers  HIGH:!aNULL:!MD5;
+	                ssl_prefer_server_ciphers   on;
+					# TLS off unless client issues STARTTLS command
+	                starttls on;
+	                proxy on;
+	
+	        }
+	
+	}
+	
+Assuming that Guerrilla SMTPd has the following configuration settings:
+	"GSMTP_MAX_SIZE"		  "15728640",
+	"NGINX_AUTH_ENABLED":     "Y",
+	"NGINX_AUTH":             "127.0.0.1:8025", 
+
+
+Starting / Command Line usage
+==========================================================
 
 
-Copyright (c) 2012 Flashmob, GuerrillaMail.com
+All command line arguments are optional
 
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
-rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to the following conditions:
+  -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]
 
 
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
-Software.
+Starting from the command line (example)
 
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+/usr/bin/nohup /home/mike/goguerrilla -config=/home/mike/goguerrilla.conf 2>&1 &
 
 
+This will place goguerrilla in the background and continue running

+ 3 - 4
goguerrilla.conf.sample

@@ -2,15 +2,14 @@
     "GM_ALLOWED_HOSTS":"example.com,sample.com,foo.com,bar.com",
     "GM_ALLOWED_HOSTS":"example.com,sample.com,foo.com,bar.com",
     "GM_MAIL_TABLE":"new_mail",
     "GM_MAIL_TABLE":"new_mail",
     "GM_PRIMARY_MAIL_HOST":"mail.example.com",
     "GM_PRIMARY_MAIL_HOST":"mail.example.com",
-    "GSMTP_GID":"",
     "GSMTP_HOST_NAME":"mail.example.com",
     "GSMTP_HOST_NAME":"mail.example.com",
-    "GSMTP_LOG_FILE":"/dev/stdout",
-    "GSMTP_MAX_SIZE":"131072",
+    "GSMTP_LOG_FILE":"",
+    "GSMTP_MAX_SIZE":"15728640",
     "GSMTP_PRV_KEY":"/etc/ssl/private/example.com.key",
     "GSMTP_PRV_KEY":"/etc/ssl/private/example.com.key",
     "GSMTP_PUB_KEY":"/etc/ssl/certs/example.com.crt",
     "GSMTP_PUB_KEY":"/etc/ssl/certs/example.com.crt",
     "GSMTP_TIMEOUT":"100",
     "GSMTP_TIMEOUT":"100",
     "GSMTP_VERBOSE":"N",
     "GSMTP_VERBOSE":"N",
-    "GSTMP_LISTEN_INTERFACE":"5.9.7.183:25",
+    "GSTMP_LISTEN_INTERFACE":"33.1.2.133:25",
     "MAX_SMTP_CLIENTS":"10000",
     "MAX_SMTP_CLIENTS":"10000",
     "MYSQL_DB":"gmail_mail",
     "MYSQL_DB":"gmail_mail",
     "MYSQL_HOST":"127.0.0.1:3306",
     "MYSQL_HOST":"127.0.0.1:3306",

+ 233 - 192
goguerrilla.go

@@ -1,6 +1,8 @@
-/*
+/** 
 Go-Guerrilla SMTPd
 Go-Guerrilla SMTPd
-An minimalist SMTP server written in Go, made for receiving large volumes of mail.
+A minimalist SMTP server written in Go, made for receiving large volumes of mail.
+Works either as a stand-alone or in conjunction with Nginx SMTP proxy.
+TO DO: add http server for nginx
 
 
 Copyright (c) 2012 Flashmob, GuerrillaMail.com
 Copyright (c) 2012 Flashmob, GuerrillaMail.com
 
 
@@ -22,7 +24,12 @@ It's a small SMTP server written in Go, optimized for receiving email.
 Written for GuerrillaMail.com which processes tens of thousands of emails
 Written for GuerrillaMail.com which processes tens of thousands of emails
 every hour.
 every hour.
 
 
-Version: 1.0
+Benchmarking:
+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
+
+Version: 1.1
 Author: Flashmob, GuerrillaMail.com
 Author: Flashmob, GuerrillaMail.com
 Contact: [email protected]
 Contact: [email protected]
 License: MIT
 License: MIT
@@ -31,15 +38,20 @@ Site: http://www.guerrillamail.com/
 
 
 See README for more details
 See README for more details
 
 
-*/
-
-/*
-Install mysql drivers
+To build
+Install the following
 $ go get github.com/ziutek/mymysql/thrsafe
 $ go get github.com/ziutek/mymysql/thrsafe
 $ go get github.com/ziutek/mymysql/autorc
 $ go get github.com/ziutek/mymysql/autorc
 $ go get github.com/ziutek/mymysql/godrv
 $ go get github.com/ziutek/mymysql/godrv
 $ go get github.com/sloonz/go-iconv
 $ go get github.com/sloonz/go-iconv
+
+TODO: after failing tls, 
+
+patch:
+rebuild all: go build -a -v new.go
+
 */
 */
+
 package main
 package main
 
 
 import (
 import (
@@ -49,7 +61,6 @@ import (
 	"crypto/md5"
 	"crypto/md5"
 	"crypto/rand"
 	"crypto/rand"
 	"crypto/tls"
 	"crypto/tls"
-	// "database/sql"
 	"encoding/base64"
 	"encoding/base64"
 	"encoding/hex"
 	"encoding/hex"
 	"encoding/json"
 	"encoding/json"
@@ -65,42 +76,15 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"log"
 	"log"
 	"net"
 	"net"
-	//"os"
+	"net/http"
+	"os"
 	"regexp"
 	"regexp"
 	"runtime"
 	"runtime"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
-	"syscall"
 	"time"
 	"time"
 )
 )
 
 
-// defaults. Overwrite any of these in the configure() function which loads them from a json file
-var gConfig = map[string]string{
-	"MAX_SMTP_CLIENTS":       "10000",
-	"GSMTP_MAX_SIZE":         "131072",
-	"GSMTP_HOST_NAME":        "server.example.com", // This should also be set to reflect your RDNS
-	"GSMTP_VERBOSE":          "Y",
-	"GSMTP_TIMEOUT":          "100", // how many seconds before timeout.
-	"MYSQL_HOST":             "127.0.0.1:3306",
-	"MYSQL_USER":             "gmail_mail",
-	"MYSQL_PASS":             "ok",
-	"MYSQL_DB":               "gmail_mail",
-	"GM_MAIL_TABLE":          "new_mail",
-	"GSMTP_USER":             "nobody",
-	"GSTMP_LISTEN_INTERFACE": "1.0.0.0:25",
-	"GSMTP_LOG_FILE":         "gosmtp.log",
-	"GSMTP_GID":              "",
-	"GSMTP_UID":              "",
-	"GSMTP_PUB_KEY":          "/etc/ssl/certs/ssl-cert-snakeoil.pem",
-	"GSMTP_PRV_KEY":          "/etc/ssl/private/ssl-cert-snakeoil.key",
-	"GM_ALLOWED_HOSTS":       "guerrillamail.de,guerrillamailblock.com",
-	"GM_PRIMARY_MAIL_HOST":   "guerrillamail.com",
-	"GM_CONN_BACKLOG":        "100",
-	"GM_MAX_CLIENTS":         "500",
-	"SGID":                   "1008", // group id
-	"SUID":                   "1008", // user id, from /etc/passwd
-}
-
 type Client struct {
 type Client struct {
 	state       int
 	state       int
 	helo        string
 	helo        string
@@ -114,7 +98,7 @@ type Client struct {
 	hash        string
 	hash        string
 	time        int64
 	time        int64
 	tls_on      bool
 	tls_on      bool
-	socket      net.Conn
+	conn        net.Conn
 	bufin       *bufio.Reader
 	bufin       *bufio.Reader
 	bufout      *bufio.Writer
 	bufout      *bufio.Writer
 	kill_time   int64
 	kill_time   int64
@@ -123,22 +107,59 @@ type Client struct {
 	savedNotify chan int
 	savedNotify chan int
 }
 }
 
 
+var TLSconfig *tls.Config
+var max_size int // max email DATA size
+var timeout time.Duration
+var allowedHosts = make(map[string]bool, 15)
+var sem chan int // currently active clients
+
+var SaveMailChan chan *Client // workers for saving mail
+// defaults. Overwrite any of these in the configure() function which loads them from a json file
+var gConfig = map[string]string{
+	"GSMTP_MAX_SIZE":         "131072",
+	"GSMTP_HOST_NAME":        "server.example.com", // This should also be set to reflect your RDNS
+	"GSMTP_VERBOSE":          "Y",
+	"GSMTP_LOG_FILE":         "",    // Eg. /var/log/goguerrilla.log or leave blank if no logging
+	"GSMTP_TIMEOUT":          "100", // how many seconds before timeout.
+	"MYSQL_HOST":             "127.0.0.1:3306",
+	"MYSQL_USER":             "gmail_mail",
+	"MYSQL_PASS":             "ok",
+	"MYSQL_DB":               "gmail_mail",
+	"GM_MAIL_TABLE":          "new_mail",
+	"GSTMP_LISTEN_INTERFACE": "0.0.0.0:25",
+	"GSMTP_PUB_KEY":          "/etc/ssl/certs/ssl-cert-snakeoil.pem",
+	"GSMTP_PRV_KEY":          "/etc/ssl/private/ssl-cert-snakeoil.key",
+	"GM_ALLOWED_HOSTS":       "guerrillamail.de,guerrillamailblock.com",
+	"GM_PRIMARY_MAIL_HOST":   "guerrillamail.com",
+	"GM_MAX_CLIENTS":         "500",
+	"NGINX_AUTH_ENABLED":     "N",              // Y or N
+	"NGINX_AUTH":             "127.0.0.1:8025", // If using Nginx proxy, ip and port to serve Auth requsts
+	"SGID":                   "1008",           // group id
+	"SUID":                   "1008",           // user id, from /etc/passwd
+}
+
 type redisClient struct {
 type redisClient struct {
 	count int
 	count int
 	conn  redis.Conn
 	conn  redis.Conn
 	time  int
 	time  int
 }
 }
 
 
-var TLSconfig *tls.Config
-var clientChan chan *Client // connection backlog
+func logln(level int, s string) {
 
 
-var sem chan int              // currently active clients
-var SaveMailChan chan *Client // workers for saving mail
-// hosts allowed in the 'to' address'
-var allowedHosts = make(map[string]bool, 15)
+	if gConfig["GSMTP_VERBOSE"] == "Y" {
+		fmt.Println(s)
+	}
+	if level == 2 {
+		log.Fatalf(s)
+	}
+	if len(gConfig["GSMTP_LOG_FILE"]) > 0 {
+		log.Println(s)
+	}
+}
 
 
 func configure() {
 func configure() {
 	var configFile, verbose, iface string
 	var configFile, verbose, iface string
+	log.SetOutput(os.Stdout)
 	// parse command line arguments
 	// parse command line arguments
 	flag.StringVar(&configFile, "config", "goguerrilla.conf", "Path to the configuration file")
 	flag.StringVar(&configFile, "config", "goguerrilla.conf", "Path to the configuration file")
 	flag.StringVar(&verbose, "v", "n", "Verbose, [y | n] ")
 	flag.StringVar(&verbose, "v", "n", "Verbose, [y | n] ")
@@ -147,16 +168,13 @@ func configure() {
 	// load in the config.
 	// load in the config.
 	b, err := ioutil.ReadFile(configFile)
 	b, err := ioutil.ReadFile(configFile)
 	if err != nil {
 	if err != nil {
-		fmt.Println("Could not read config file")
-		panic(err)
+		log.Fatalln("Could not read config file")
 	}
 	}
 	var myConfig map[string]string
 	var myConfig map[string]string
 	err = json.Unmarshal(b, &myConfig)
 	err = json.Unmarshal(b, &myConfig)
 	if err != nil {
 	if err != nil {
-		fmt.Println("Could not parse config file")
-		panic(err)
+		log.Fatalln("Could not parse config file")
 	}
 	}
-
 	for k, v := range myConfig {
 	for k, v := range myConfig {
 		gConfig[k] = v
 		gConfig[k] = v
 	}
 	}
@@ -172,161 +190,88 @@ func configure() {
 	}
 	}
 	var n int
 	var n int
 	var n_err error
 	var n_err error
-	if n, n_err = strconv.Atoi(gConfig["GM_CONN_BACKLOG"]); n_err != nil {
-		n = 50
-	}
-	// connection backlog list
-	clientChan = make(chan *Client, n)
+	// sem is an active clients channel used for counting clients
 	if n, n_err = strconv.Atoi(gConfig["GM_MAX_CLIENTS"]); n_err != nil {
 	if n, n_err = strconv.Atoi(gConfig["GM_MAX_CLIENTS"]); n_err != nil {
 		n = 50
 		n = 50
 	}
 	}
 	// currently active client list
 	// currently active client list
 	sem = make(chan int, n)
 	sem = make(chan int, n)
 	// database writing workers
 	// database writing workers
-	SaveMailChan = make(chan *Client, 4)
-
-	return
-}
-
-func logln(level int, s string) {
-	if level == 2 {
-		log.Fatalf(s)
+	SaveMailChan = make(chan *Client, 5)
+	// timeout for reads
+	if n, n_err = strconv.Atoi(gConfig["GSMTP_TIMEOUT"]); n_err != nil {
+		timeout = time.Duration(10)
+	} else {
+		timeout = time.Duration(n)
 	}
 	}
-	if gConfig["GSMTP_VERBOSE"] == "Y" {
-		fmt.Println(s)
+	// max email size
+	if max_size, n_err = strconv.Atoi(gConfig["GSMTP_MAX_SIZE"]); n_err != nil {
+		max_size = 131072
 	}
 	}
+	// custom log file
+	if len(gConfig["GSMTP_LOG_FILE"]) > 0 {
+		logfile, err := os.OpenFile(gConfig["GSMTP_LOG_FILE"], os.O_WRONLY|os.O_APPEND|os.O_CREATE|os.O_SYNC, 0600)
+		if err != nil {
+			log.Fatal("Unable to open log file ["+gConfig["GSMTP_LOG_FILE"]+"]: ", err)
+		}
+		log.SetOutput(logfile)
+	}
+
+	return
 }
 }
 
 
 func main() {
 func main() {
 	configure()
 	configure()
-	logln(1, "Loading priv:"+gConfig["GSMTP_PRV_KEY"]+" and pub:"+gConfig["GSMTP_PRV_KEY"])
 	cert, err := tls.LoadX509KeyPair(gConfig["GSMTP_PUB_KEY"], gConfig["GSMTP_PRV_KEY"])
 	cert, err := tls.LoadX509KeyPair(gConfig["GSMTP_PUB_KEY"], gConfig["GSMTP_PRV_KEY"])
 	if err != nil {
 	if err != nil {
 		logln(2, fmt.Sprintf("There was a problem with loading the certificate: %s", err))
 		logln(2, fmt.Sprintf("There was a problem with loading the certificate: %s", err))
 	}
 	}
 	TLSconfig = &tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.VerifyClientCertIfGiven, ServerName: gConfig["GSMTP_HOST_NAME"]}
 	TLSconfig = &tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.VerifyClientCertIfGiven, ServerName: gConfig["GSMTP_HOST_NAME"]}
 	TLSconfig.Rand = rand.Reader
 	TLSconfig.Rand = rand.Reader
+	// start some savemail workers
+	for i := 0; i < 3; i++ {
+		go saveMail()
+	}
+	if gConfig["NGINX_AUTH_ENABLED"] == "Y" {
+		go nginxHTTPAuth()
+	}
+	// Start listening for SMTP connections
 	listener, err := net.Listen("tcp", gConfig["GSTMP_LISTEN_INTERFACE"])
 	listener, err := net.Listen("tcp", gConfig["GSTMP_LISTEN_INTERFACE"])
 	if err != nil {
 	if err != nil {
-		logln(2, fmt.Sprintf("Cannot listen on port, %s", err))
-	}
-	gid, _ := strconv.ParseInt(gConfig["SGID"], 10, 32)
-	uid, _ := strconv.ParseInt(gConfig["SUID"], 10, 32)
-	syscall.Setgid(int(gid))
-	syscall.Setuid(int(uid))
-	logln(1, fmt.Sprintf("server listening on "+gConfig["GSTMP_LISTEN_INTERFACE"]))
-	go Serve(clientChan) // Start our SMTP client worker pool
-	go saveMail()        // start our email saving worker pool
-	clientId := int64(1)
+		logln(2, fmt.Sprintf("Cannot listen on port, %v", err))
+	} else {
+		logln(1, fmt.Sprintf("Listening on tcp %s", gConfig["GSTMP_LISTEN_INTERFACE"]))
+	}
+	var clientId int64
+	clientId = 1
 	for {
 	for {
 		conn, err := listener.Accept()
 		conn, err := listener.Accept()
 		if err != nil {
 		if err != nil {
 			logln(1, fmt.Sprintf("Accept error: %s", err))
 			logln(1, fmt.Sprintf("Accept error: %s", err))
-			break
+			continue
 		}
 		}
-		logln(1, fmt.Sprintf("server: accepted from %s", conn.RemoteAddr()))
-		// place a new client on the channel
-		clientChan <- &Client{
-			socket:      conn,
+		logln(1, fmt.Sprintf(" There are now "+strconv.Itoa(runtime.NumGoroutine())+" serving goroutines"))
+		sem <- 1 // Wait for active queue to drain.
+		go handleClient(&Client{
+			conn:        conn,
 			address:     conn.RemoteAddr().String(),
 			address:     conn.RemoteAddr().String(),
 			time:        time.Now().Unix(),
 			time:        time.Now().Unix(),
 			bufin:       bufio.NewReader(conn),
 			bufin:       bufio.NewReader(conn),
 			bufout:      bufio.NewWriter(conn),
 			bufout:      bufio.NewWriter(conn),
 			clientId:    clientId,
 			clientId:    clientId,
 			savedNotify: make(chan int),
 			savedNotify: make(chan int),
-		}
+		})
 		clientId++
 		clientId++
 	}
 	}
 }
 }
 
 
-func Serve(clientChan chan *Client) {
-	for {
-		// get new clients off the queue and pass them to the handler
-		c := <-clientChan
-		sem <- 1           // Wait for active queue to drain.
-		go handleClient(c) // Don't wait for handle to finish.
-		logln(1, fmt.Sprintf("There are now "+strconv.Itoa(runtime.NumGoroutine())+" goroutines"))
-	}
-}
-
-func closeClient(client *Client) {
-	client.socket.Close()
-	<-sem // Done; enable next client to run.
-}
-
-func readSmtp(client *Client) (input string, err error) {
-	var reply string
-	// Command state terminator by default
-	suffix := "\r\n"
-	if client.state == 2 {
-		// DATA state
-		suffix = "\r\n.\r\n"
-	}
-	for err == nil {
-		client.socket.SetDeadline(time.Now().Add(100 * time.Second))
-		reply, err = client.bufin.ReadString('\n')
-		if reply != "" {
-			input = input + reply
-			if client.state == 2 {
-				// Extract the subject while we are at it.
-				scanSubject(client, reply)
-			}
-		}
-		if err != nil {
-			break
-		}
-		if strings.HasSuffix(input, suffix) {
-			break
-		}
-	}
-	return input, err
-}
-
-// Scan the data part for a Subject line. Can be a multi-line
-func scanSubject(client *Client, reply string) {
-	if client.subject == "" && (len(reply) > 8) {
-		test := strings.ToUpper(reply[0:9])
-		if i := strings.Index(test, "SUBJECT: "); i == 0 {
-			// first line with \r\n
-			client.subject = reply[9:]
-		}
-	} else if strings.HasSuffix(client.subject, "\r\n") {
-		// chop off the \r\n
-		client.subject = client.subject[0 : len(client.subject)-2]
-		if (strings.HasPrefix(reply, " ")) || (strings.HasPrefix(reply, "\t")) {
-			// subject is multi-line
-			client.subject = client.subject + reply[1:]
-		}
-	}
-}
-
-func responseWrite(client *Client) (err error) {
-	var size int
-	client.socket.SetDeadline(time.Now().Add(100 * time.Second))
-	size, err = client.bufout.WriteString(client.response)
-	client.bufout.Flush()
-	client.response = client.response[size:]
-	return err
-}
-
-func responseAdd(client *Client, line string) {
-	client.response = line + "\r\n"
-}
-func responseClear(client *Client) {
-	client.response = ""
-}
-
-func killClient(client *Client) {
-	client.kill_time = time.Now().Unix()
-}
-
 func handleClient(client *Client) {
 func handleClient(client *Client) {
-	var input_hist string
 	defer closeClient(client)
 	defer closeClient(client)
+	//	defer closeClient(client)
 	greeting := "220 " + gConfig["GSMTP_HOST_NAME"] +
 	greeting := "220 " + gConfig["GSMTP_HOST_NAME"] +
 		" SMTP Guerrilla-SMTPd #" + strconv.FormatInt(client.clientId, 10) + " (" + strconv.Itoa(len(sem)) + ") " + time.Now().Format(time.RFC1123Z)
 		" SMTP Guerrilla-SMTPd #" + strconv.FormatInt(client.clientId, 10) + " (" + strconv.Itoa(len(sem)) + ") " + time.Now().Format(time.RFC1123Z)
 	advertiseTls := "250-STARTTLS\r\n"
 	advertiseTls := "250-STARTTLS\r\n"
-	for i := 0; i < 10; i++ {
+	for i := 0; i < 100; i++ {
 		switch client.state {
 		switch client.state {
 		case 0:
 		case 0:
 			responseAdd(client, greeting)
 			responseAdd(client, greeting)
@@ -334,6 +279,7 @@ func handleClient(client *Client) {
 		case 1:
 		case 1:
 			input, err := readSmtp(client)
 			input, err := readSmtp(client)
 			if err != nil {
 			if err != nil {
+				logln(1, fmt.Sprintf("Read error: %v", err))
 				if err == io.EOF {
 				if err == io.EOF {
 					// client closed the connection already
 					// client closed the connection already
 					return
 					return
@@ -345,7 +291,6 @@ func handleClient(client *Client) {
 				break
 				break
 			}
 			}
 			input = strings.Trim(input, " \n\r")
 			input = strings.Trim(input, " \n\r")
-			input_hist = input_hist + input + "\n"
 			cmd := strings.ToUpper(input)
 			cmd := strings.ToUpper(input)
 			switch {
 			switch {
 			case strings.Index(cmd, "HELO") == 0:
 			case strings.Index(cmd, "HELO") == 0:
@@ -357,15 +302,19 @@ func handleClient(client *Client) {
 				if len(input) > 5 {
 				if len(input) > 5 {
 					client.helo = input[5:]
 					client.helo = input[5:]
 				}
 				}
-				if client.tls_on {
-					advertiseTls = ""
-				}
 				responseAdd(client, "250-"+gConfig["GSMTP_HOST_NAME"]+" Hello "+client.helo+"["+client.address+"]"+"\r\n"+"250-SIZE "+gConfig["GSMTP_MAX_SIZE"]+"\r\n"+advertiseTls+"250 HELP")
 				responseAdd(client, "250-"+gConfig["GSMTP_HOST_NAME"]+" Hello "+client.helo+"["+client.address+"]"+"\r\n"+"250-SIZE "+gConfig["GSMTP_MAX_SIZE"]+"\r\n"+advertiseTls+"250 HELP")
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 				if len(input) > 10 {
 				if len(input) > 10 {
 					client.mail_from = input[10:]
 					client.mail_from = input[10:]
 				}
 				}
 				responseAdd(client, "250 Ok")
 				responseAdd(client, "250 Ok")
+			case strings.Index(cmd, "XCLIENT") == 0:
+				// Nginx sends this
+				// XCLIENT ADDR=212.96.64.216 NAME=[UNAVAILABLE]
+				client.address = input[13:]
+				client.address = client.address[0:strings.Index(client.address, " ")]
+				fmt.Println("client address:[" + client.address + "]")
+				responseAdd(client, "250 OK")
 			case strings.Index(cmd, "RCPT TO:") == 0:
 			case strings.Index(cmd, "RCPT TO:") == 0:
 				if len(input) > 8 {
 				if len(input) > 8 {
 					client.rcpt_to = input[8:]
 					client.rcpt_to = input[8:]
@@ -388,10 +337,10 @@ func handleClient(client *Client) {
 				responseAdd(client, "221 Bye")
 				responseAdd(client, "221 Bye")
 				killClient(client)
 				killClient(client)
 			default:
 			default:
-				responseAdd(client, fmt.Sprintf("500 unrecognized command %v", err))
+				responseAdd(client, fmt.Sprintf("500 unrecognized command"))
 				client.errors++
 				client.errors++
 				if client.errors > 3 {
 				if client.errors > 3 {
-					responseAdd(client, fmt.Sprintf("500 Too many unrecognized commands %v", err))
+					responseAdd(client, fmt.Sprintf("500 Too many unrecognized commands"))
 					killClient(client)
 					killClient(client)
 				}
 				}
 			}
 			}
@@ -404,23 +353,31 @@ func handleClient(client *Client) {
 				SaveMailChan <- client
 				SaveMailChan <- client
 				// wait for the save to complete
 				// wait for the save to complete
 				status := <-client.savedNotify
 				status := <-client.savedNotify
+
 				if status == 1 {
 				if status == 1 {
 					responseAdd(client, "250 OK : queued as "+client.hash)
 					responseAdd(client, "250 OK : queued as "+client.hash)
 				} else {
 				} else {
 					responseAdd(client, "554 Error: transaction failed, blame it on the weather")
 					responseAdd(client, "554 Error: transaction failed, blame it on the weather")
 				}
 				}
+			} else {
+				logln(1, fmt.Sprintf("DATA read error: %v", err))
 			}
 			}
 			client.state = 1
 			client.state = 1
 		case 3:
 		case 3:
 			// upgrade to TLS
 			// upgrade to TLS
 			var tlsConn *tls.Conn
 			var tlsConn *tls.Conn
-			tlsConn = tls.Server(client.socket, TLSconfig)
-			tlsConn.Handshake() // not necessary to call here, but might as well
-			client.socket = net.Conn(tlsConn)
-			client.bufin = bufio.NewReader(client.socket)
-			client.bufout = bufio.NewWriter(client.socket)
+			tlsConn = tls.Server(client.conn, TLSconfig)
+			err := tlsConn.Handshake() // not necessary to call here, but might as well
+			if err == nil {
+				client.conn = net.Conn(tlsConn)
+				client.bufin = bufio.NewReader(client.conn)
+				client.bufout = bufio.NewWriter(client.conn)
+				client.tls_on = true
+			} else {
+				logln(1, fmt.Sprintf("Could not TLS handshake:%v", err))
+			}
+			advertiseTls = ""
 			client.state = 1
 			client.state = 1
-			client.tls_on = true
 		}
 		}
 		// Send a response back to the client
 		// Send a response back to the client
 		err := responseWrite(client)
 		err := responseWrite(client)
@@ -438,6 +395,77 @@ func handleClient(client *Client) {
 			return
 			return
 		}
 		}
 	}
 	}
+
+}
+
+func responseAdd(client *Client, line string) {
+	client.response = line + "\r\n"
+}
+func closeClient(client *Client) {
+	client.conn.Close()
+	<-sem // Done; enable next client to run.
+}
+func killClient(client *Client) {
+	client.kill_time = time.Now().Unix()
+}
+
+func readSmtp(client *Client) (input string, err error) {
+	var reply string
+	// Command state terminator by default
+	suffix := "\r\n"
+	if client.state == 2 {
+		// DATA state
+		suffix = "\r\n.\r\n"
+	}
+	for err == nil {
+		client.conn.SetDeadline(time.Now().Add(timeout * time.Second))
+		reply, err = client.bufin.ReadString('\n')
+		if reply != "" {
+			input = input + reply
+			if len(input) > max_size {
+				err = errors.New("Maximum DATA size exceeded (" + strconv.Itoa(max_size) + ")")
+				return input, err
+			}
+			if client.state == 2 {
+				// Extract the subject while we are at it.
+				scanSubject(client, reply)
+			}
+		}
+		if err != nil {
+			break
+		}
+		if strings.HasSuffix(input, suffix) {
+			break
+		}
+	}
+	return input, err
+}
+
+// Scan the data part for a Subject line. Can be a multi-line
+func scanSubject(client *Client, reply string) {
+	if client.subject == "" && (len(reply) > 8) {
+		test := strings.ToUpper(reply[0:9])
+		if i := strings.Index(test, "SUBJECT: "); i == 0 {
+			// first line with \r\n
+			client.subject = reply[9:]
+		}
+	} else if strings.HasSuffix(client.subject, "\r\n") {
+		// chop off the \r\n
+		client.subject = client.subject[0 : len(client.subject)-2]
+		if (strings.HasPrefix(reply, " ")) || (strings.HasPrefix(reply, "\t")) {
+			// subject is multi-line
+			client.subject = client.subject + reply[1:]
+		}
+	}
+}
+
+func responseWrite(client *Client) (err error) {
+	var size int
+	client.conn.SetDeadline(time.Now().Add(timeout * time.Second))
+	size, err = client.bufout.WriteString(client.response)
+	client.bufout.Flush()
+	client.response = client.response[size:]
+	return err
 }
 }
 
 
 func saveMail() {
 func saveMail() {
@@ -461,7 +489,6 @@ func saveMail() {
 	if sql_err != nil {
 	if sql_err != nil {
 		logln(2, fmt.Sprintf("Sql statement incorrect: %s", sql_err))
 		logln(2, fmt.Sprintf("Sql statement incorrect: %s", sql_err))
 	}
 	}
-	//defer db.Close()
 
 
 	//  receives values from the channel repeatedly until it is closed.
 	//  receives values from the channel repeatedly until it is closed.
 	for {
 	for {
@@ -494,9 +521,8 @@ func saveMail() {
 				client.data = ""
 				client.data = ""
 				body = "redis"
 				body = "redis"
 			}
 			}
-			//fmt.Println(do_reply, do_err)
 		} else {
 		} else {
-			fmt.Println("redis err", redis_err)
+			logln(1, fmt.Sprintf("redis: %v", redis_err))
 		}
 		}
 		// bind data to cursor
 		// bind data to cursor
 		ins.Bind(
 		ins.Bind(
@@ -517,7 +543,7 @@ func saveMail() {
 			logln(1, "Email saved "+client.hash+" len:"+strconv.Itoa(length))
 			logln(1, "Email saved "+client.hash+" len:"+strconv.Itoa(length))
 			_, _, err = incr.Exec()
 			_, _, err = incr.Exec()
 			if err != nil {
 			if err != nil {
-				fmt.Println(err)
+				logln(1, fmt.Sprintf("Failed to incr count:", err))
 			}
 			}
 			client.savedNotify <- 1
 			client.savedNotify <- 1
 		}
 		}
@@ -539,15 +565,6 @@ func (c *redisClient) redisConnection() (err error) {
 	return nil
 	return nil
 }
 }
 
 
-func mysqlTest() {
-	//var mysqlCon *sql.DB
-	//mysqlCon, err := sql.Open("mymysql", gConfig["MYSQL_DB"]+"/"+gConfig["MYSQL_USER"]+"/"+gConfig["MYSQL_PASS"])
-	//if err != nil {
-	//	log.Fatalf("Cannot open Mysql connection: %s", err)
-	//}
-	// defer mysqlCon.Close()
-}
-
 func validateEmailData(client *Client) (user string, host string, addr_err error) {
 func validateEmailData(client *Client) (user string, host string, addr_err error) {
 	if user, host, addr_err = extractEmail(client.mail_from); addr_err != nil {
 	if user, host, addr_err = extractEmail(client.mail_from); addr_err != nil {
 		return user, host, addr_err
 		return user, host, addr_err
@@ -570,9 +587,9 @@ func extractEmail(str string) (name string, host string, err error) {
 		host = validHost(matched[2])
 		host = validHost(matched[2])
 		name = matched[1]
 		name = matched[1]
 	} else {
 	} else {
-		if res := strings.Split(name, "@"); len(res) > 1 {
-			name = matched[0]
-			host = validHost(matched[1])
+		if res := strings.Split(str, "@"); len(res) > 1 {
+			name = res[0]
+			host = validHost(res[1])
 		}
 		}
 	}
 	}
 	if host == "" || name == "" {
 	if host == "" || name == "" {
@@ -689,3 +706,27 @@ func md5hex(str string) string {
 	sum := h.Sum([]byte{})
 	sum := h.Sum([]byte{})
 	return hex.EncodeToString(sum)
 	return hex.EncodeToString(sum)
 }
 }
+
+// If running Nginx as a proxy, give Nginx the IP address and port for the SMTP server
+// Primary use of Nginx is to terminate TLS so that Go doesn't need to deal with it.
+// This could perform auth and load balancing too
+// See http://wiki.nginx.org/MailCoreModule
+func nginxHTTPAuth() {
+	parts := strings.Split(gConfig["GSTMP_LISTEN_INTERFACE"], ":")
+	gConfig["HTTP_AUTH_HOST"] = parts[0]
+	gConfig["HTTP_AUTH_PORT"] = parts[1]
+	fmt.Println(parts)
+	http.HandleFunc("/", nginxHTTPAuthHandler)
+	err := http.ListenAndServe(gConfig["NGINX_AUTH"], nil)
+	if err != nil {
+		log.Fatal("ListenAndServe: ", err)
+	}
+
+}
+
+func nginxHTTPAuthHandler(w http.ResponseWriter, r *http.Request) {
+	w.Header().Add("Auth-Status", "OK")
+	w.Header().Add("Auth-Server", gConfig["HTTP_AUTH_HOST"])
+	w.Header().Add("Auth-Port", gConfig["HTTP_AUTH_PORT"])
+	fmt.Fprint(w, "")
+}