Prechádzať zdrojové kódy

Modularize - Ability for server to be used as a package (#27)

* Begin major refactoring. Collapsed subpackages into top level, refactored client handling code.

* Remove `go` from repo and icon name
Remove old `server` package
Refactor cmd package to work with new server
Remove `models.go`, remove parts and refactor other parts into either `server.go` or `client.go`
Change format of `allowed_hosts` to JSON array from comma separated string

* Change repo name

* add backend model to root package

* Fix ROOT in makefile
Refactor NewServer and server.run into server file
Remove requirement for servers to receive appconfig

* fix imports in cmd

* Add comments

* Add Guerrilla struct and global New/Run interface for starting and running apps

* Move backend interface to guerrilla package
Refactor MD5Hash method to not use pointers to strings (go strings are pointers under the hood already anyway)
Refactor mysql+redis backend implementation to work with new backend interface and modified client struct
Move concern of reading a configuration file to the command line interface

* add message sent tracking field to clients, update backend interface

* PR for modularize / code review

* remove changes file and remove from gitignore

* add glide dependency management

* make server private to guerrilla package

* Update sample config to use JSON array for allowed hosts

* Fix loadconfig method of dummyconfig

* * change imports back to flashmob's repo
* tls sate always goes back to command state

* * don't reply with any response after switching to TLS

* * allocate the Headers map

* * fix panic when saving to mysql (passed as a string)

* * do not allocate client until we have space for it
* added some kills for read errors

* do not process email if host is not allowed

* changed greeting to show number of active clients

* Fix issue where config file wasn't read correctly, preventing saveMail workers from being created and causing deadlock in the Redis+MySQL backend

* Move header parsing function to util for backends to use
Remove Client.Headers and replace with Subject as before

* recreate glide config for flashmob repo rather than my fork

* add vendor directory to gitignore

* * exit on unknown backend
* log invalid tls handshake
* do not send reply on invalid tls handshake

* * do not write a response if there is nothing to write
* just a tiny detail to the guerrilla mail mail backend, legacy db table

* Add section in readme for using goguerrilla as a package

* Move util functions used by backends to backends package.
Make smtpbufferedreader unexported

* add result interface for backends to return from `Process`

* add env var for vendor in go 1.5

* get travis working, revert import names back to flashmob

* update readme with BackendResult interfacce example;

* Add check that system open file limit is less then max clients at launch

* move public client fields to `MailData` struct and make client private. Move `Client#Hash` into sql+redis backend

* add ip to log message

* renamed clientHandshake state to clientGreeting

* don't advertise STARTTLS if turned off

* don't recognize STARTTLS command if turned off

* change names: EmailParts -> EmailAddress, MailData -> Envelope

* Fix naming in README

* rename MailData to Envelope

* rename Address to RemoteAddress
Jordan Schalm 8 rokov pred
rodič
commit
88d73fe561
22 zmenil súbory, kde vykonal 1014 pridanie a 707 odobranie
  1. 1 0
      .gitignore
  2. 11 0
      .travis.yml
  3. 71 29
      README.md
  4. 0 23
      backends/backend.go
  5. 12 22
      backends/dummy.go
  6. 60 80
      backends/guerrilla_db_redis.go
  7. 24 31
      backends/util.go
  8. 87 0
      client.go
  9. 1 1
      cmd/guerrillad/root.go
  10. 89 30
      cmd/guerrillad/serve.go
  11. 17 34
      config.go
  12. 0 37
      config/config.go
  13. 32 0
      glide.lock
  14. 17 0
      glide.yaml
  15. 8 2
      goguerrilla.conf.sample
  16. 40 0
      guerrilla.go
  17. 69 0
      mocks/client.go
  18. 65 46
      models.go
  19. 375 0
      server.go
  20. 0 83
      server/goguerrilla.go
  21. 0 289
      server/smtpd.go
  22. 35 0
      util.go

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
 .idea
 goguerrilla.conf
 /guerrillad
+vendor

+ 11 - 0
.travis.yml

@@ -0,0 +1,11 @@
+language: go
+
+go:
+  - 1.5
+
+install:
+  - export GO15VENDOREXPERIMENT=1
+  - go get github.com/Masterminds/glide
+  - go install github.com/Masterminds/glide
+  - glide up
+  - make guerrillad

+ 71 - 29
README.md

@@ -4,7 +4,7 @@ Go-Guerrilla SMTPd
 
 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](/GoGuerrilla.png)
 
 ### What is Go Guerrilla SMTPd?
 
@@ -18,7 +18,7 @@ and disconnect as quickly as possible.
 A typical user of this software would probably want to customize the save_mail.go source for
 their own systems.
 
-This server does not attempt to filter HTML, check for spam or do any sender 
+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.
@@ -29,35 +29,35 @@ The software is using MIT License (MIT) - contributors welcome.
 
 
 Pull requests / issue reporting & discussion / code reviews always welcome.
-To encourage more pull requests, we are now offering bounties funded 
+To encourage more pull requests, we are now offering bounties funded
 from our bitcoin donation address:
 
 `1grr11aWtbsyMUeB4EGfHvTuu7eFzkJ4A`
 
 So far we have the following bounties:
 
-- Client Pooling: When a client is finished, it should be placed into a 
-pool instead of being destroyed. Looking for a idiomatic 
+- Client Pooling: When a client is finished, it should be placed into a
+pool instead of being destroyed. Looking for a idiomatic
 Go solution with channels. (0.5 BTC for a successful merge)
-See discussion here: https://github.com/flashmob/go-guerrilla/issues/11
+See discussion here:  https://github.com/flashmob/go-guerrilla/issues/11
 
 - Modularize: Ability for the server to be used as a package. If it used
-as a package, an API would be exposed, and a new program would be able 
-to start several servers on different ports, would be possible to 
-specify a config file for each server, and specify its own 
-saveMail function (otherwise, revert to default). Would be good to 
-make it GoDep friendly too - or any other dependency tool out there. 
+as a package, an API would be exposed, and a new program would be able
+to start several servers on different ports, would be possible to
+specify a config file for each server, and specify its own
+saveMail function (otherwise, revert to default). Would be good to
+make it GoDep friendly too - or any other dependency tool out there.
 (0.5 BTC 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 senders by: IP, by domain & by HELO message. 
-Using websocket via https & password protected. 
+connections/bytes/memory used for the last 24h.
+Show the top senders by: IP, by domain & by HELO message.
+Using websocket via https & password protected.
 (1 BTC for a successful merge)
 
 - Testing: Automated test that can start the server and test end-to-end
-a few common cases, some unit tests would be good too. 
+a few common cases, some unit tests would be good too.
 (0.25 BTC for a successful merge)
 
 - Looking for someone to do a code review & possibly fix any tidbits,
@@ -74,20 +74,20 @@ us fund more bounties!
 
 ### Brief History and purpose
 
-Go-Guerrilla is used as the primary server for receiving email at 
-Guerrilla Mail. As of 2016, it's handling all connections without any 
+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).
 
-Originally, Guerrilla Mail ran Exim which piped email to a php script (2009). 
+Originally, Guerrilla Mail ran Exim which piped email to a php script (2009).
 As as the site got popular and more email came through, this approach
 eventually swamped the server.
 
-The next solution was to decrease the heavy setup into something more 
+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.
- 
+
 
 Getting started
 ===========================
@@ -131,13 +131,55 @@ in MySQL:
 	) 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 
+to query and join, while the body of the email is fetched from Redis
 if needed.
 
 You can implement your own saveMail function to use whatever storage /
 backend fits for you.
 
 
+Use as a package
+============================
+Guerrilla SMTPd can also be imported and used as a package in your project.
+
+## Import Guerrilla.
+```go
+import "github.com/flashmob/go-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
+type CustomBackend struct {...}
+
+func (cb *CustomBackend) Process(c *guerrilla.Envelope) guerrilla.BackendResult {
+  err := saveSomewhere(c.Data)
+  if err != nil {
+    return guerrilla.NewBackendResult(fmt.Sprintf("554 Error: %s", err.Error()))
+  }
+  return guerrilla.NewBackendResult("250 OK")
+}
+```
+
+## Create an app instance.
+See Configuration section below for setting configuration options.
+```go
+config := &guerrilla.AppConfig{
+  Backend: &CustomBackend{...},
+  Servers: []*guerrilla.ServerConfig{...},
+  AllowedHosts: []string{...}
+}
+app := guerrilla.New(config)
+```
+
+## Start the app.
+`Start` is non-blocking, so make sure the main goroutine is kept busy
+```go
+app.Start()
+```
+
+
 Configuration
 ============================================
 The configuration is in strict JSON format. Here is an annotated configuration.
@@ -145,7 +187,7 @@ Copy goguerrilla.conf.sample to goguerrilla.conf
 
 
     {
-        "allowed_hosts": "guerrillamail.com,guerrillamailblock.com,sharklasers.com,guerrillamail.net,guerrillamail.org" // What hosts to accept 
+        "allowed_hosts": "guerrillamail.com,guerrillamailblock.com,sharklasers.com,guerrillamail.net,guerrillamail.org" // What hosts to accept
         "primary_mail_host":"sharklasers.com", // main domain
         "verbose":false, // report all events to log
         "mysql_db":"gmail_mail", // name of mysql database
@@ -192,7 +234,7 @@ Copy goguerrilla.conf.sample to goguerrilla.conf
 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/#
-	
+
 
 Releases
 =========================================================
@@ -215,7 +257,7 @@ sample (goguerrilla.conf.sample)
 
 
 1.3 14th July 2016
-- Number of saveMail workers added to config (GM_SAVE_WORKERS) 
+- Number of saveMail workers added to config (GM_SAVE_WORKERS)
 - convenience function for reading int values form config
 - advertise PIPELINING
 - added HELP command
@@ -251,7 +293,7 @@ Why proxy SMTP with Nginx?
 	                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;
@@ -266,14 +308,14 @@ Why proxy SMTP with Nginx?
 	                proxy on;
 	        }
 		}
-		
+
 		http {
-		
+
 		    # 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
-		    
+
 		    server {
                     listen 15.29.8.163:80;
                     server_name 15.29.8.163 smtpauth.local;
@@ -285,7 +327,7 @@ Why proxy SMTP with Nginx?
                         add_header Auth-Server 127.0.0.1;
                         add_header Auth-Port 2525;
                     }
-                   
+
                 }
 
 		}

+ 0 - 23
backends/backend.go

@@ -1,23 +0,0 @@
-package backends
-
-import (
-	"fmt"
-
-	guerrilla "github.com/flashmob/go-guerrilla"
-)
-
-var backends = map[string]guerrilla.Backend{}
-
-// New retrieve a backend specified by the backendName, and initialize it using
-// backendConfig
-func New(backendName string, backendConfig guerrilla.BackendConfig) (guerrilla.Backend, error) {
-	backend, found := backends[backendName]
-	if !found {
-		return nil, fmt.Errorf("backend %q not found", backendName)
-	}
-	err := backend.Initialize(backendConfig)
-	if err != nil {
-		return nil, fmt.Errorf("error while initializing the backend: %s", err)
-	}
-	return backend, nil
-}

+ 12 - 22
backends/dummy.go

@@ -1,17 +1,11 @@
 package backends
 
 import (
-	"fmt"
-
 	log "github.com/Sirupsen/logrus"
 
-	guerrilla "github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla"
 )
 
-func init() {
-	backends["dummy"] = &DummyBackend{}
-}
-
 type DummyBackend struct {
 	config dummyConfig
 }
@@ -20,26 +14,22 @@ type dummyConfig struct {
 	LogReceivedMails bool `json:"log_received_mails"`
 }
 
-func (b *DummyBackend) loadConfig(backendConfig guerrilla.BackendConfig) error {
-	var converted bool
-	b.config.LogReceivedMails, converted = backendConfig["log_received_mails"].(bool)
-	if !converted {
-		return fmt.Errorf("failed to load backend config (%v)", backendConfig)
+func (b *DummyBackend) loadConfig(config map[string]interface{}) {
+	willLog, ok := config["log_received_mails"].(bool)
+	if !ok {
+		b.config = dummyConfig{false}
+	} else {
+		b.config = dummyConfig{willLog}
 	}
-	return nil
-}
-
-func (b *DummyBackend) Initialize(backendConfig guerrilla.BackendConfig) error {
-	return b.loadConfig(backendConfig)
 }
 
-func (b *DummyBackend) Finalize() error {
-	return nil
+func (b *DummyBackend) Initialize(config map[string]interface{}) {
+	b.loadConfig(config)
 }
 
-func (b *DummyBackend) Process(client *guerrilla.Client, from *guerrilla.EmailParts, to []*guerrilla.EmailParts) string {
+func (b *DummyBackend) Process(mail *guerrilla.Envelope) guerrilla.BackendResult {
 	if b.config.LogReceivedMails {
-		log.Infof("Mail from: %s / to: %v data:[%s]", from, to, client.Data)
+		log.Infof("Mail from: %s / to: %v", mail.MailFrom.String(), mail.RcptTo)
 	}
-	return fmt.Sprintf("250 OK : queued as %s", client.Hash)
+	return guerrilla.NewBackendResult("250 OK")
 }

+ 60 - 80
backends/guerrilla_db_redis.go

@@ -1,26 +1,20 @@
 package backends
 
 import (
+	"encoding/json"
+	"errors"
 	"fmt"
 	"sync"
 	"time"
 
 	log "github.com/Sirupsen/logrus"
-
+	"github.com/flashmob/go-guerrilla"
 	"github.com/garyburd/redigo/redis"
+
 	"github.com/ziutek/mymysql/autorc"
 	_ "github.com/ziutek/mymysql/godrv"
-
-	guerrilla "github.com/flashmob/go-guerrilla"
-	"github.com/flashmob/go-guerrilla/util"
-	"reflect"
-	"strings"
 )
 
-func init() {
-	backends["guerrilla-db-redis"] = &GuerrillaDBAndRedisBackend{}
-}
-
 type GuerrillaDBAndRedisBackend struct {
 	config       guerrillaDBAndRedisConfig
 	saveMailChan chan *savePayload
@@ -46,45 +40,21 @@ func convertError(name string) error {
 // Load the backend config for the backend. It has already been unmarshalled
 // from the main config file 'backend' config "backend_config"
 // Now we need to convert each type and copy into the guerrillaDBAndRedisConfig struct
-func (g *GuerrillaDBAndRedisBackend) loadConfig(backendConfig guerrilla.BackendConfig) error {
-	// Use reflection so that we can provide a nice error message
-	g.config = guerrillaDBAndRedisConfig{}
-	s := reflect.ValueOf(&g.config).Elem(); // so that we can set the values
-	typeOfT := s.Type()
-	tags := reflect.TypeOf(g.config) // read the tags of the config struct
-	for i := 0; i < s.NumField(); i++ {
-		f := s.Field(i)
-		field_name := tags.Field(i).Tag.Get("json")
-		if len(field_name) > 0 {
-			// get the field name from struct tag
-			split := strings.Split(field_name, ",")
-			field_name = split[0]
-		} else {
-			// could have no tag
-			// so use the reflected field name
-			field_name = typeOfT.Field(i).Name
-		}
+func (g *GuerrillaDBAndRedisBackend) loadConfig(backendConfig map[string]interface{}) error {
+	data, err := json.Marshal(backendConfig)
+	if err != nil {
+		return err
+	}
 
-		if f.Type().Name() == "int" {
-			if intVal, converted := backendConfig[field_name].(float64); converted {
-				s.Field(i).SetInt(int64(intVal))
-			} else {
-				return convertError("property missing/invalid: '"+field_name +"' of expected type: "+f.Type().Name())
-			}
-		}
-		if f.Type().Name() == "string" {
-			if stringVal, converted := backendConfig[field_name].(string); converted {
-				s.Field(i).SetString(stringVal)
-			} else {
-				return convertError("missing/invalid: '"+field_name +"' of type: "+f.Type().Name())
-			}
-		}
+	err = json.Unmarshal(data, &g.config)
+	if g.config.NumberOfWorkers < 1 {
+		return errors.New("Must have more than 1 worker")
 	}
 
-	return nil
+	return err
 }
 
-func (g *GuerrillaDBAndRedisBackend) Initialize(backendConfig guerrilla.BackendConfig) error {
+func (g *GuerrillaDBAndRedisBackend) Initialize(backendConfig map[string]interface{}) error {
 	err := g.loadConfig(backendConfig)
 	if err != nil {
 		return err
@@ -111,33 +81,42 @@ func (g *GuerrillaDBAndRedisBackend) Finalize() error {
 	return nil
 }
 
-func (g *GuerrillaDBAndRedisBackend) Process(client *guerrilla.Client, from *guerrilla.EmailParts, to []*guerrilla.EmailParts) string {
+func (g *GuerrillaDBAndRedisBackend) Process(mail *guerrilla.Envelope) guerrilla.BackendResult {
+	to := mail.RcptTo
+	from := mail.MailFrom
 	if len(to) == 0 {
-		return "554 Error: no recipient"
+		return guerrilla.NewBackendResult("554 Error: no recipient")
 	}
 
 	// to do: timeout when adding to SaveMailChan
 	// place on the channel so that one of the save mail workers can pick it up
 	// TODO: support multiple recipients
-	g.saveMailChan <- &savePayload{client: client, from: from, recipient: to[0]}
+	savedNotify := make(chan *saveStatus)
+	g.saveMailChan <- &savePayload{mail, from, to[0], savedNotify}
 	// wait for the save to complete
 	// or timeout
 	select {
-	case status := <-client.SavedNotify:
-		if status == 1 {
-			return fmt.Sprintf("250 OK : queued as %s", client.Hash)
+	case status := <-savedNotify:
+		if status.err != nil {
+			return guerrilla.NewBackendResult("554 Error: " + status.err.Error())
 		}
-		return "554 Error: transaction failed, blame it on the weather"
+		return guerrilla.NewBackendResult(fmt.Sprintf("250 OK : queued as %s", status.hash))
 	case <-time.After(time.Second * 30):
 		log.Debug("timeout")
-		return "554 Error: transaction timeout"
+		return guerrilla.NewBackendResult("554 Error: transaction timeout")
 	}
 }
 
 type savePayload struct {
-	client    *guerrilla.Client
-	from      *guerrilla.EmailParts
-	recipient *guerrilla.EmailParts
+	mail        *guerrilla.Envelope
+	from        *guerrilla.EmailAddress
+	recipient   *guerrilla.EmailAddress
+	savedNotify chan *saveStatus
+}
+
+type saveStatus struct {
+	err  error
+	hash string
 }
 
 type redisClient struct {
@@ -147,7 +126,7 @@ type redisClient struct {
 }
 
 func (g *GuerrillaDBAndRedisBackend) saveMail() {
-	var to, recipient, body string
+	var to, body string
 	var err error
 
 	var redisErr error
@@ -183,28 +162,28 @@ func (g *GuerrillaDBAndRedisBackend) saveMail() {
 			return
 		}
 		to = payload.recipient.User + "@" + g.config.PrimaryHost
-		length = len(payload.client.Data)
+		length = len(payload.mail.Data)
 		ts := fmt.Sprintf("%d", time.Now().UnixNano())
-		payload.client.Subject = util.MimeHeaderDecode(payload.client.Subject)
-		payload.client.Hash = util.MD5Hex(
-			&to,
-			&payload.client.MailFrom,
-			&payload.client.Subject,
-			&ts)
+		payload.mail.Subject = MimeHeaderDecode(payload.mail.Subject)
+		hash := MD5Hex(
+			to,
+			payload.mail.MailFrom.String(),
+			payload.mail.Subject,
+			ts)
 		// Add extra headers
 		var addHead string
 		addHead += "Delivered-To: " + to + "\r\n"
-		addHead += "Received: from " + payload.client.Helo + " (" + payload.client.Helo + "  [" + payload.client.Address + "])\r\n"
-		addHead += "	by " + payload.recipient.Host + " with SMTP id " + payload.client.Hash + "@" + payload.recipient.Host + ";\r\n"
+		addHead += "Received: from " + payload.mail.Helo + " (" + payload.mail.Helo + "  [" + payload.mail.RemoteAddress + "])\r\n"
+		addHead += "	by " + payload.recipient.Host + " with SMTP id " + hash + "@" + payload.recipient.Host + ";\r\n"
 		addHead += "	" + time.Now().Format(time.RFC1123Z) + "\r\n"
 		// compress to save space
-		payload.client.Data = util.Compress(&addHead, &payload.client.Data)
+		payload.mail.Data = Compress(addHead, payload.mail.Data)
 		body = "gzencode"
 		redisErr = redisClient.redisConnection(g.config.RedisInterface)
 		if redisErr == nil {
-			_, doErr := redisClient.conn.Do("SETEX", payload.client.Hash, g.config.RedisExpireSeconds, payload.client.Data)
+			_, doErr := redisClient.conn.Do("SETEX", hash, g.config.RedisExpireSeconds, payload.mail.Data)
 			if doErr == nil {
-				payload.client.Data = ""
+				payload.mail.Data = ""
 				body = "redis"
 			}
 		} else {
@@ -213,28 +192,29 @@ func (g *GuerrillaDBAndRedisBackend) saveMail() {
 		// bind data to cursor
 		ins.Bind(
 			to,
-			payload.client.MailFrom,
-			payload.client.Subject,
+			payload.mail.MailFrom.String(),
+			payload.mail.Subject,
 			body,
-			payload.client.Data,
-			payload.client.Hash,
-			recipient,
-			payload.client.Address,
-			payload.client.MailFrom,
-			payload.client.TLS,
+			payload.mail.Data,
+			hash,
+			to,
+			payload.mail.RemoteAddress,
+			payload.mail.MailFrom.String(),
+			payload.mail.TLS,
 		)
 		// save, discard result
 		_, _, err = ins.Exec()
 		if err != nil {
-			log.WithError(err).Warn("Database error while inster")
-			payload.client.SavedNotify <- -1
+			errMsg := "Database error while inserting"
+			log.WithError(err).Warn(errMsg)
+			payload.savedNotify <- &saveStatus{errors.New(errMsg), hash}
 		} else {
-			log.Debugf("Email saved %s (len=%d)", payload.client.Hash, length)
+			log.Debugf("Email saved %s (len=%d)", hash, length)
 			_, _, err = incr.Exec()
 			if err != nil {
 				log.WithError(err).Warn("Database error while incr count")
 			}
-			payload.client.SavedNotify <- 1
+			payload.savedNotify <- &saveStatus{nil, hash}
 		}
 	}
 }

+ 24 - 31
util/util.go → backends/util.go

@@ -1,40 +1,43 @@
-package util
+package backends
 
 import (
 	"bytes"
 	"compress/zlib"
 	"crypto/md5"
 	"encoding/base64"
-	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
+	"net/textproto"
 	"regexp"
 	"strings"
 
 	"github.com/sloonz/go-qprintable"
 	"gopkg.in/iconv.v1"
-
-	guerrilla "github.com/flashmob/go-guerrilla"
 )
 
-var extractEmailRegex, _ = regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk!
+// First capturing group is header name, second is header value.
+// Accounts for folding headers.
+var headerRegex, _ = regexp.Compile(`^([\S ]+):([\S ]+(?:\r\n\s[\S ]+)?)`)
 
-func ExtractEmail(str string) (email *guerrilla.EmailParts, err error) {
-	email = &guerrilla.EmailParts{}
-	if matched := extractEmailRegex.FindStringSubmatch(str); len(matched) > 2 {
-		email.User = matched[1]
-		email.Host = validHost(matched[2])
-	} else {
-		if res := strings.Split(str, "@"); len(res) > 1 {
-			email.User = res[0]
-			email.Host = validHost(res[1])
+func ParseHeaders(mailData string) map[string]string {
+	var headerSectionEnds int
+	for i, char := range mailData[:len(mailData)-4] {
+		if char == '\r' {
+			if mailData[i+1] == '\n' && mailData[i+2] == '\r' && mailData[i+3] == '\n' {
+				headerSectionEnds = i + 2
+			}
 		}
 	}
-	if email.User == "" || email.Host == "" {
-		err = errors.New("Invalid address, [" + email.User + "@" + email.Host + "] address:" + str)
+	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)))
+		val := strings.TrimSpace(strings.Replace(h[2], "\r\n", "", -1))
+		headers[name] = val
 	}
-	return
+	return headers
 }
 
 var mimeRegex, _ = regexp.Compile(`=\?(.+?)\?([QBqp])\?(.+?)\?=`)
@@ -71,16 +74,6 @@ func MimeHeaderDecode(str string) string {
 	return str
 }
 
-var valihostRegex, _ = regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
-
-func validHost(host string) string {
-	host = strings.Trim(host, " ")
-	if valihostRegex.MatchString(host) {
-		return host
-	}
-	return ""
-}
-
 // decode from 7bit to 8bit UTF-8
 // encodingType can be "base64" or "quoted-printable"
 func MailTransportDecode(str string, encodingType string, charset string) string {
@@ -152,11 +145,11 @@ func fixCharset(charset string) string {
 }
 
 // returns an md5 hash as string of hex characters
-func MD5Hex(stringArguments ...*string) string {
+func MD5Hex(stringArguments ...string) string {
 	h := md5.New()
 	var r *strings.Reader
 	for i := 0; i < len(stringArguments); i++ {
-		r = strings.NewReader(*stringArguments[i])
+		r = strings.NewReader(stringArguments[i])
 		io.Copy(h, r)
 	}
 	sum := h.Sum([]byte{})
@@ -164,12 +157,12 @@ func MD5Hex(stringArguments ...*string) string {
 }
 
 // concatenate & compress all strings  passed in
-func Compress(stringArguments ...*string) string {
+func Compress(stringArguments ...string) string {
 	var b bytes.Buffer
 	var r *strings.Reader
 	w, _ := zlib.NewWriterLevel(&b, zlib.BestSpeed)
 	for i := 0; i < len(stringArguments); i++ {
-		r = strings.NewReader(*stringArguments[i])
+		r = strings.NewReader(stringArguments[i])
 		io.Copy(w, r)
 	}
 	w.Close()

+ 87 - 0
client.go

@@ -0,0 +1,87 @@
+package guerrilla
+
+import (
+	"bufio"
+	"net"
+	"strings"
+	"time"
+)
+
+// ClientState indicates which part of the SMTP transaction a given client is in.
+type ClientState int
+
+const (
+	// The client has connected, and is awaiting our first response
+	ClientGreeting = iota
+	// We have responded to the client's connection and are awaiting a command
+	ClientCmd
+	// We have received the sender and recipient information
+	ClientData
+	// We have agreed with the client to secure the connection over TLS
+	ClientStartTLS
+)
+
+type client struct {
+	*Envelope
+	ID          int64
+	ConnectedAt time.Time
+	KilledAt    time.Time
+	// Number of errors encountered during session with this client
+	errors       int
+	state        ClientState
+	messagesSent int
+	// Response to be written to the client
+	response string
+	conn     net.Conn
+	bufin    *smtpBufferedReader
+	bufout   *bufio.Writer
+}
+
+// Email represents a single SMTP message.
+type Envelope struct {
+	// Remote IP address
+	RemoteAddress string
+	// Message sent in EHLO command
+	Helo          string
+	// Sender
+	MailFrom      *EmailAddress
+	// Recipients
+	RcptTo        []*EmailAddress
+	Data          string
+	Subject       string
+	TLS           bool
+}
+
+func (c *client) responseAdd(r string) {
+	c.response = c.response + r + "\r\n"
+}
+
+func (c *client) reset() {
+	c.MailFrom = &EmailAddress{}
+	c.RcptTo = []*EmailAddress{}
+}
+
+func (c *client) kill() {
+	c.KilledAt = time.Now()
+}
+
+func (c *client) isAlive() bool {
+	return c.KilledAt.IsZero()
+}
+
+func (c *client) scanSubject(reply string) {
+	if c.Subject == "" && (len(reply) > 8) {
+		test := strings.ToUpper(reply[0:9])
+		if i := strings.Index(test, "SUBJECT: "); i == 0 {
+			// first line with \r\n
+			c.Subject = reply[9:]
+		}
+	} else if strings.HasSuffix(c.Subject, "\r\n") {
+		// chop off the \r\n
+		c.Subject = c.Subject[0 : len(c.Subject)-2]
+		if (strings.HasPrefix(reply, " ")) || (strings.HasPrefix(reply, "\t")) {
+			// subject is multi-line
+			c.Subject = c.Subject + reply[1:]
+		}
+	}
+}

+ 1 - 1
cmd/guerrillad/root.go

@@ -8,7 +8,7 @@ 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 volume of email.
+	Long: `It's a small SMTP server 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,
 }

+ 89 - 30
cmd/guerrillad/serve.go

@@ -1,24 +1,28 @@
 package main
 
 import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
 	"os"
+	"os/exec"
 	"os/signal"
+	"strconv"
+	"strings"
 	"syscall"
+	"time"
 
 	log "github.com/Sirupsen/logrus"
 	"github.com/spf13/cobra"
 
-	"fmt"
-
-	guerrilla "github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla"
 	"github.com/flashmob/go-guerrilla/backends"
-	"github.com/flashmob/go-guerrilla/config"
-	"github.com/flashmob/go-guerrilla/server"
 )
 
 var (
 	iface      string
-	configFile string
+	configPath string
 	pidFile    string
 
 	serveCmd = &cobra.Command{
@@ -27,16 +31,14 @@ var (
 		Run:   serve,
 	}
 
-	mainConfig    = guerrilla.Config{}
+	cmdConfig     = CmdConfig{}
 	signalChannel = make(chan os.Signal, 1) // for trapping SIG_HUP
 )
 
 func init() {
-	serveCmd.PersistentFlags().StringVarP(&iface, "if", "", "",
-		"Interface and port to listen on, eg. 127.0.0.1:2525 ")
-	serveCmd.PersistentFlags().StringVarP(&configFile, "config", "c",
+	serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
 		"goguerrilla.conf", "Path to the configuration file")
-	serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
+	serveCmd.PersistentFlags().StringVarP(&pidFile, "pid-file", "p",
 		"/var/run/go-guerrilla.pid", "Path to the pid file")
 
 	rootCmd.AddCommand(serveCmd)
@@ -48,7 +50,7 @@ func sigHandler() {
 
 	for sig := range signalChannel {
 		if sig == syscall.SIGHUP {
-			err := config.ReadConfig(configFile, iface, verbose, &mainConfig)
+			err := readConfig(configPath, verbose, &cmdConfig)
 			if err != nil {
 				log.WithError(err).Error("Error while ReadConfig (reload)")
 			} else {
@@ -64,12 +66,26 @@ func sigHandler() {
 func serve(cmd *cobra.Command, args []string) {
 	logVersion()
 
-	err := config.ReadConfig(configFile, iface, verbose, &mainConfig)
+	err := readConfig(configPath, verbose, &cmdConfig)
 	if err != nil {
-		log.WithError(err).Fatal("Error while ReadConfig")
+		log.WithError(err).Fatal("Error while reading config")
+	}
+
+	// Check that max clients is not greater than system open file limit.
+	fileLimit := getFileLimit()
+
+	if fileLimit > 0 {
+		maxClients := 0
+		for _, s := range cmdConfig.Servers {
+			maxClients += s.MaxClients
+		}
+		if maxClients > fileLimit {
+			log.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)
+		}
 	}
 
-	// write out our PID
+	// Write out our PID
 	if len(pidFile) > 0 {
 		if f, err := os.Create(pidFile); err == nil {
 			defer f.Close()
@@ -83,24 +99,67 @@ func serve(cmd *cobra.Command, args []string) {
 		}
 	}
 
-	backend, err := backends.New(mainConfig.BackendName, mainConfig.BackendConfig)
+	switch cmdConfig.BackendName {
+	case "dummy":
+		b := &backends.DummyBackend{}
+		b.Initialize(cmdConfig.BackendConfig)
+		cmdConfig.Backend = b
+	case "guerrilla-db-redis":
+		b := &backends.GuerrillaDBAndRedisBackend{}
+		err = b.Initialize(cmdConfig.BackendConfig)
+		if err != nil {
+			log.WithError(err).Errorf("Initalization of %s backend failed", cmdConfig.BackendName)
+		}
+		cmdConfig.Backend = b
+	default:
+		log.Fatalf("Unknown backend: %s", cmdConfig.BackendName)
+	}
+
+	app := guerrilla.New(&cmdConfig.AppConfig)
+	go app.Start()
+	sigHandler()
+}
+
+// Superset of `guerrilla.AppConfig` containing options specific
+// the the command line interface.
+type CmdConfig struct {
+	guerrilla.AppConfig
+	BackendName   string                 `json:"backend_name"`
+	BackendConfig map[string]interface{} `json:"backend_config"`
+}
+
+// ReadConfig which should be called at startup, or when a SIG_HUP is caught
+func readConfig(path string, verbose bool, config *CmdConfig) error {
+	// load in the config.
+	data, err := ioutil.ReadFile(path)
 	if err != nil {
-		log.WithError(err).Fatalf("Error while loading the backend %q",
-			mainConfig.BackendName)
+		return fmt.Errorf("Could not read config file: %s", err.Error())
 	}
 
-	// run our servers
-	for _, serverConfig := range mainConfig.Servers {
-		if serverConfig.IsEnabled {
-			log.Infof("Starting server on %s", serverConfig.ListenInterface)
-			go func(sConfig guerrilla.ServerConfig) {
-				err := server.RunServer(mainConfig, sConfig, backend)
-				if err != nil {
-					log.WithError(err).Fatalf("Error while starting server on %s", serverConfig.ListenInterface)
-				}
-			}(serverConfig)
-		}
+	err = json.Unmarshal(data, &config)
+	if err != nil {
+		return fmt.Errorf("Could not parse config file: %s", err.Error())
 	}
 
-	sigHandler()
+	if len(config.AllowedHosts) == 0 {
+		return errors.New("Empty `allowed_hosts` is not allowed")
+	}
+
+	guerrilla.ConfigLoadTime = time.Now()
+	return nil
+}
+
+func getFileLimit() int {
+	cmd := exec.Command("ulimit", "-n")
+	out, err := cmd.Output()
+	if err != nil {
+		return -1
+	}
+
+	limit, err := strconv.Atoi(strings.TrimSpace(string(out)))
+	if err != nil {
+		return -1
+	}
+
+	return limit
 }

+ 17 - 34
config.go

@@ -1,40 +1,23 @@
 package guerrilla
 
-import "strings"
-
-type BackendConfig map[string]interface{}
-
-// Config is the holder of the configuration of the app
-type Config struct {
-	BackendName   string         `json:"backend_name"`
-	BackendConfig BackendConfig  `json:"backend_config,omitempty"`
-	Servers       []ServerConfig `json:"servers"`
-	AllowedHosts  string         `json:"allowed_hosts"`
-
-	_allowedHosts map[string]bool
-}
-
-func (c *Config) IsAllowed(host string) bool {
-	if c._allowedHosts == nil {
-		arr := strings.Split(c.AllowedHosts, ",")
-		c._allowedHosts = make(map[string]bool, len(arr))
-		for _, h := range arr {
-			c._allowedHosts[strings.ToLower(h)] = true
-		}
-	}
-	return c._allowedHosts[strings.ToLower(host)]
+// AppConfig is the holder of the configuration of the app
+type AppConfig struct {
+	Backend      Backend
+	Servers      []*ServerConfig `json:"servers"`
+	AllowedHosts []string        `json:"allowed_hosts"`
 }
 
-// ServerConfig is the holder of the configuration of a server
+// ServerConfig specifies config options for a single server
 type ServerConfig struct {
-	IsEnabled       bool   `json:"is_enabled"`
-	Hostname        string `json:"host_name"`
-	MaxSize         int    `json:"max_size"`
-	PrivateKeyFile  string `json:"private_key_file"`
-	PublicKeyFile   string `json:"public_key_file"`
-	Timeout         int    `json:"timeout"`
-	ListenInterface string `json:"listen_interface"`
-	StartTLS        bool   `json:"start_tls_on,omitempty"`
-	TLSAlwaysOn     bool   `json:"tls_always_on,omitempty"`
-	MaxClients      int    `json:"max_clients"`
+	IsEnabled       bool     `json:"is_enabled"`
+	Hostname        string   `json:"host_name"`
+	AllowedHosts    []string `json:"allowed_hosts"`
+	MaxSize         int64    `json:"max_size"`
+	PrivateKeyFile  string   `json:"private_key_file"`
+	PublicKeyFile   string   `json:"public_key_file"`
+	Timeout         int      `json:"timeout"`
+	ListenInterface string   `json:"listen_interface"`
+	StartTLSOn      bool     `json:"start_tls_on,omitempty"`
+	TLSAlwaysOn     bool     `json:"tls_always_on,omitempty"`
+	MaxClients      int      `json:"max_clients"`
 }

+ 0 - 37
config/config.go

@@ -1,37 +0,0 @@
-package config
-
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"time"
-
-	guerrilla "github.com/flashmob/go-guerrilla"
-)
-
-// ReadConfig which should be called at startup, or when a SIG_HUP is caught
-func ReadConfig(configFile, iface string, verbose bool, mainConfig *guerrilla.Config) error {
-	// load in the config.
-	b, err := ioutil.ReadFile(configFile)
-	if err != nil {
-		return fmt.Errorf("could not read config file: %s", err)
-	}
-
-	err = json.Unmarshal(b, &mainConfig)
-	if err != nil {
-		return fmt.Errorf("could not parse config file: %s", err)
-	}
-
-	if len(mainConfig.AllowedHosts) == 0 {
-		return errors.New("empty AllowedHosts is not allowed")
-	}
-
-	// TODO: deprecate
-	if len(iface) > 0 && len(mainConfig.Servers) > 0 {
-		mainConfig.Servers[0].ListenInterface = iface
-	}
-
-	guerrilla.ConfigLoadTime = time.Now()
-	return nil
-}

+ 32 - 0
glide.lock

@@ -0,0 +1,32 @@
+hash: d96dd7a4b78faacec2eb40ef44e4e6598abf0be703021cae6dacd8080b6f4f54
+updated: 2016-12-16T15:58:00.591876692-08:00
+imports:
+- name: github.com/garyburd/redigo
+  version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13
+  subpackages:
+  - internal
+  - redis
+- name: github.com/inconshreveable/mousetrap
+  version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
+- name: github.com/Sirupsen/logrus
+  version: d26492970760ca5d33129d2d799e34be5c4782eb
+- name: github.com/sloonz/go-qprintable
+  version: 775b3a4592d5bfc47b0ba398ec0d4dba018e5926
+- name: github.com/spf13/cobra
+  version: b62566898a99f2db9c68ed0026aa0a052e59678d
+- name: github.com/spf13/pflag
+  version: 25f8b5b07aece3207895bf19f7ab517eb3b22a40
+- name: github.com/ziutek/mymysql
+  version: e08c2f35356576b3c3690c252fe5dca728ae9292
+  subpackages:
+  - autorc
+  - godrv
+  - mysql
+  - native
+- name: golang.org/x/sys
+  version: 478fcf54317e52ab69f40bb4c7a1520288d7f7ea
+  subpackages:
+  - unix
+- name: gopkg.in/iconv.v1
+  version: 16a760eb7e186ae0e3aedda00d4a1daa4d0701d8
+testImports: []

+ 17 - 0
glide.yaml

@@ -0,0 +1,17 @@
+package: github.com/flashmob/go-guerrilla
+import:
+- package: github.com/Sirupsen/logrus
+  version: ~0.11.0
+- package: github.com/garyburd/redigo
+  version: ~1.0.0
+  subpackages:
+  - redis
+- package: github.com/sloonz/go-qprintable
+- package: github.com/spf13/cobra
+- package: github.com/ziutek/mymysql
+  version: ~1.5.4
+  subpackages:
+  - autorc
+  - godrv
+- package: gopkg.in/iconv.v1
+  version: ~1.1.1

+ 8 - 2
goguerrilla.conf.sample

@@ -1,6 +1,12 @@
 {
-    "allowed_hosts": "guerrillamail.com,guerrillamailblock.com,sharklasers.com,guerrillamail.net,guerrillamail.org",
-    "primary_mail_host":"sharklasers.com",
+    "allowed_hosts": [
+      "guerrillamail.com",
+      "guerrillamailblock.com"
+      "sharklasers.com",
+      "guerrillamail.net",
+      "guerrillamail.org"
+    ],
+    "primary_mail_host": "sharklasers.com",
     "backend_name": "dummy",
     "backend_config": {
         "log_received_mails": true

+ 40 - 0
guerrilla.go

@@ -0,0 +1,40 @@
+package guerrilla
+
+import log "github.com/Sirupsen/logrus"
+
+type Guerrilla interface {
+	Start()
+}
+
+type guerrilla struct {
+	Config  *AppConfig
+	servers []*server
+}
+
+// Returns a new instance of Guerrilla with the given config, not yet running.
+func New(ac *AppConfig) Guerrilla {
+	g := &guerrilla{ac, []*server{}}
+
+	// Instantiate servers
+	for _, sc := range ac.Servers {
+		if !sc.IsEnabled {
+			continue
+		}
+
+		// Add relevant app-wide config options to each server
+		sc.AllowedHosts = ac.AllowedHosts
+		server, err := newServer(sc, ac.Backend)
+		if err != nil {
+			log.WithError(err).Error("Failed to create server")
+		}
+		g.servers = append(g.servers, server)
+	}
+	return g
+}
+
+// Entry point for the application. Starts all servers.
+func (g *guerrilla) Start() {
+	for _, s := range g.servers {
+		go s.Start()
+	}
+}

+ 69 - 0
mocks/client.go

@@ -0,0 +1,69 @@
+package main
+
+import (
+	"fmt"
+	"net/smtp"
+	"time"
+)
+
+const (
+	URL = "127.0.0.1:2500"
+)
+
+func lastWords(message string, err error) {
+	fmt.Println(message, err.Error())
+	return
+	// panic(err)
+}
+
+// Sends a single SMTP message, for testing.
+func main() {
+	for i := 0; i < 100; i++ {
+		go sendMail(i)
+	}
+	time.Sleep(time.Minute / 10)
+}
+
+func sendMail(i int) {
+	fmt.Printf("Sending %d mail\n", i)
+	c, err := smtp.Dial(URL)
+	if err != nil {
+		lastWords("Dial ", err)
+	}
+	defer c.Close()
+
+	from := "[email protected]"
+	to := "[email protected]"
+
+	if err = c.Mail(from); err != nil {
+		lastWords("Mail ", err)
+	}
+
+	if err = c.Rcpt(to); err != nil {
+		lastWords("Rcpt ", err)
+	}
+
+	wr, err := c.Data()
+	if err != nil {
+		lastWords("Data ", err)
+	}
+	defer wr.Close()
+
+	msg := fmt.Sprint("Subject: something\n")
+	msg += "From: " + from + "\n"
+	msg += "To: " + to + "\n"
+	msg += "\n\n"
+	msg += "hello\n"
+
+	_, err = fmt.Fprint(wr, msg)
+	if err != nil {
+		lastWords("Send ", err)
+	}
+
+	fmt.Printf("About to quit %d\n", i)
+	err = c.Quit()
+	if err != nil {
+		lastWords("Quit ", err)
+	}
+	fmt.Printf("Finished sending %d mail\n", i)
+}

+ 65 - 46
models.go

@@ -5,50 +5,70 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"net"
+	"strconv"
+	"strings"
 )
 
-type EmailParts struct {
-	User string
-	Host string
+var (
+	LineLimitExceeded   = errors.New("Maximum line length exceeded")
+	MessageSizeExceeded = errors.New("Maximum message size exceeded")
+)
+
+// Backends process received mail. Depending on the implementation, that can
+// be storing in a database, retransmitting to another server, etc.
+// Must return an SMTP message (i.e. "250 OK") and a boolean indicating
+// whether the message was processed successfully.
+type Backend interface {
+	Process(*Envelope) BackendResult
 }
 
-func (ep *EmailParts) String() string {
-	return fmt.Sprintf("%s@%s", ep.User, ep.Host)
+// BackendResult represents a response to an SMTP client after receiving DATA.
+// The String method should return an SMTP message ready to send back to the
+// client, for example `250 OK: Message received`.
+type BackendResult interface {
+	fmt.Stringer
+	// Code should return the SMTP code associated with this response, ie. `250`
+	Code() int
 }
 
-// Backend accepts the received messages, and store/deliver/process them
-type Backend interface {
-	Initialize(BackendConfig) error
-	Process(client *Client, from *EmailParts, to []*EmailParts) string
-	Finalize() error
+// Internal implementation of BackendResult for use by backend implementations.
+type backendResult string
+
+func (br backendResult) String() string {
+	return string(br)
+}
+
+// Parses the SMTP code from the first 3 characters of the SMTP message.
+// Returns 554 if code cannot be parsed.
+func (br backendResult) Code() int {
+	trimmed := strings.TrimSpace(string(br))
+	if len(trimmed) < 3 {
+		return 554
+	}
+	code, err := strconv.Atoi(trimmed[:3])
+	if err != nil {
+		return 554
+	}
+	return code
+}
+
+func NewBackendResult(message string) BackendResult {
+	return backendResult(message)
+}
+
+// EmailAddress encodes an email address of the form `<user@host>`
+type EmailAddress struct {
+	User string
+	Host string
 }
 
-const CommandMaxLength = 1024
-
-// TODO: cleanup
-type Client struct {
-	State       int
-	Helo        string
-	MailFrom    string
-	RcptTo      string
-	Response    string
-	Address     string
-	Data        string
-	Subject     string
-	Hash        string
-	Time        int64
-	TLS         bool
-	Conn        net.Conn
-	Bufin       *SMTPBufferedReader
-	Bufout      *bufio.Writer
-	KillTime    int64
-	Errors      int
-	ClientID    int64
-	SavedNotify chan int
+func (ep *EmailAddress) String() string {
+	return fmt.Sprintf("%s@%s", ep.User, ep.Host)
 }
 
-var InputLimitExceeded = errors.New("Line too long") // 500 Line too long.
+func (ep *EmailAddress) isEmpty() bool {
+	return ep.User == "" && ep.Host == ""
+}
 
 // we need to adjust the limit, so we embed io.LimitedReader
 type adjustableLimitedReader struct {
@@ -60,14 +80,13 @@ func (alr *adjustableLimitedReader) setLimit(n int64) {
 	alr.R.N = n
 }
 
-// this just delegates to the underlying reader in order to satisfy the Reader interface
-// Since the vanilla limited reader returns io.EOF when the limit is reached, we need a more specific
-// error so that we can distinguish when a limit is reached
+// Returns a specific error when a limit is reached, that can be differentiated
+// from an EOF error from the standard io.Reader.
 func (alr *adjustableLimitedReader) Read(p []byte) (n int, err error) {
 	n, err = alr.R.Read(p)
 	if err == io.EOF && alr.R.N <= 0 {
-		// return our custom error since std lib returns EOF
-		err = InputLimitExceeded
+		// return our custom error since io.Reader returns EOF
+		err = LineLimitExceeded
 	}
 	return
 }
@@ -80,19 +99,19 @@ func newAdjustableLimitedReader(r io.Reader, n int64) *adjustableLimitedReader {
 
 // This is a bufio.Reader what will use our adjustable limit reader
 // We 'extend' buffio to have the limited reader feature
-type SMTPBufferedReader struct {
+type smtpBufferedReader struct {
 	*bufio.Reader
 	alr *adjustableLimitedReader
 }
 
-// delegate to the adjustable limited reader
-func (sbr *SMTPBufferedReader) SetLimit(n int64) {
+// Delegate to the adjustable limited reader
+func (sbr *smtpBufferedReader) setLimit(n int64) {
 	sbr.alr.setLimit(n)
 }
 
-// allocate a new smtpBufferedReader
-func NewSMTPBufferedReader(rd io.Reader) *SMTPBufferedReader {
-	alr := newAdjustableLimitedReader(rd, CommandMaxLength)
-	s := &SMTPBufferedReader{bufio.NewReader(alr), alr}
+// Allocate a new SMTPBufferedReader
+func newSMTPBufferedReader(rd io.Reader) *smtpBufferedReader {
+	alr := newAdjustableLimitedReader(rd, CommandLineMaxLength)
+	s := &smtpBufferedReader{bufio.NewReader(alr), alr}
 	return s
 }

+ 375 - 0
server.go

@@ -0,0 +1,375 @@
+package guerrilla
+
+import (
+	"bufio"
+	"crypto/rand"
+	"crypto/tls"
+	"fmt"
+	"io"
+	"net"
+	"strings"
+	"time"
+
+	"errors"
+
+	"runtime"
+
+	log "github.com/Sirupsen/logrus"
+)
+
+const (
+	CommandVerbMaxLength = 16
+	CommandLineMaxLength = 1024
+	// Number of allowed unrecognized commands before we terminate the connection
+	MaxUnrecognizedCommands = 5
+)
+
+// Server listens for SMTP clients on the port specified in its config
+type server struct {
+	config    *ServerConfig
+	backend   Backend
+	tlsConfig *tls.Config
+	maxSize   int64
+	timeout   time.Duration
+	sem       chan int
+}
+
+// Creates and returns a new ready-to-run Server from a configuration
+func newServer(sc *ServerConfig, b Backend) (*server, error) {
+	server := &server{
+		config:  sc,
+		backend: b,
+		maxSize: sc.MaxSize,
+		sem:     make(chan int, sc.MaxClients),
+	}
+
+	if server.config.TLSAlwaysOn || server.config.StartTLSOn {
+		cert, err := tls.LoadX509KeyPair(server.config.PublicKeyFile, server.config.PrivateKeyFile)
+		if err != nil {
+			return nil, fmt.Errorf("Error loading TLS certificate: %s", err.Error())
+		}
+
+		server.tlsConfig = &tls.Config{
+			Certificates: []tls.Certificate{cert},
+			ClientAuth:   tls.VerifyClientCertIfGiven,
+			ServerName:   server.config.Hostname,
+			Rand:         rand.Reader,
+		}
+	}
+
+	server.timeout = time.Duration(server.config.Timeout) * time.Second
+
+	return server, nil
+}
+
+// Begin accepting SMTP clients
+func (server *server) Start() error {
+	listener, err := net.Listen("tcp", server.config.ListenInterface)
+	if err != nil {
+		return fmt.Errorf("Cannot listen on port: %s", err.Error())
+	}
+
+	log.Infof("Listening on TCP %s", server.config.ListenInterface)
+	var clientID int64
+	clientID = 1
+	for {
+		log.Debugf("Waiting for a new client. Client ID: %d", clientID)
+		conn, err := listener.Accept()
+		if err != nil {
+			log.WithError(err).Info("Error accepting client")
+			continue
+		}
+		server.sem <- 1
+		go server.handleClient(&client{
+			Envelope: &Envelope{
+				RemoteAddress: conn.RemoteAddr().String(),
+			},
+			conn:        conn,
+			ConnectedAt: time.Now(),
+			bufin:       newSMTPBufferedReader(conn),
+			bufout:      bufio.NewWriter(conn),
+			ID:          clientID,
+		})
+		clientID++
+	}
+}
+
+// Verifies that the host is a valid recipient.
+func (server *server) allowsHost(host string) bool {
+	for _, allowed := range server.config.AllowedHosts {
+		if host == allowed {
+			return true
+		}
+	}
+	return false
+}
+
+// Upgrades a client connection to TLS
+func (server *server) upgradeToTLS(client *client) bool {
+	tlsConn := tls.Server(client.conn, server.tlsConfig)
+	err := tlsConn.Handshake()
+	if err != nil {
+		log.WithError(err).Warn("[%s] Failed TLS handshake", client.RemoteAddress)
+		return false
+	}
+	client.conn = net.Conn(tlsConn)
+	client.bufin = newSMTPBufferedReader(client.conn)
+	client.bufout = bufio.NewWriter(client.conn)
+	client.TLS = true
+
+	return true
+}
+
+// Closes a client connection
+func (server *server) closeConn(client *client) {
+	client.conn.Close()
+	client.conn = nil
+	<-server.sem
+}
+
+// Reads from the client until a terminating sequence is encountered,
+// or until a timeout occurs.
+func (server *server) read(client *client) (string, error) {
+	var input, reply string
+	var err error
+
+	// In command state, stop reading at line breaks
+	suffix := "\r\n"
+	if client.state == ClientData {
+		// In data state, stop reading at solo periods
+		suffix = "\r\n.\r\n"
+	}
+
+	for {
+		client.conn.SetDeadline(time.Now().Add(server.timeout))
+		reply, err = client.bufin.ReadString('\n')
+		input = input + reply
+		if client.state == ClientData && reply != "" {
+			// Extract the subject while we're at it
+			client.scanSubject(reply)
+		}
+		if int64(len(input)) > server.config.MaxSize {
+			return input, fmt.Errorf("Maximum DATA size exceeded (%d)", server.config.MaxSize)
+		}
+		if err != nil {
+			break
+		}
+		if strings.HasSuffix(input, suffix) {
+			break
+		}
+	}
+	return input, err
+}
+
+// Writes a response to the client.
+func (server *server) writeResponse(client *client) error {
+	client.conn.SetDeadline(time.Now().Add(server.timeout))
+	size, err := client.bufout.WriteString(client.response)
+	if err != nil {
+		return err
+	}
+	err = client.bufout.Flush()
+	if err != nil {
+		return err
+	}
+	client.response = client.response[size:]
+	return nil
+}
+
+// Handles an entire client SMTP exchange
+func (server *server) handleClient(client *client) {
+	defer server.closeConn(client)
+	log.Infof("Handle client [%s], id: %d", client.RemoteAddress, client.ID)
+
+	// Initial greeting
+	greeting := fmt.Sprintf("220 %s SMTP Guerrilla(%s) #%d (%d) %s gr:%d",
+		server.config.Hostname, Version, client.ID,
+		len(server.sem), time.Now().Format(time.RFC3339), runtime.NumGoroutine())
+
+	helo := fmt.Sprintf("250 %s Hello", server.config.Hostname)
+	ehlo := fmt.Sprintf("250-%s Hello", server.config.Hostname)
+
+	// Extended feature advertisements
+	messageSize := fmt.Sprintf("250-SIZE %d\r\n", server.config.MaxSize)
+	pipelining := "250-PIPELINING\r\n"
+	advertiseTLS := "250-STARTTLS\r\n"
+	help := "250 HELP"
+
+	if server.config.TLSAlwaysOn {
+		success := server.upgradeToTLS(client)
+		if !success {
+			client.kill()
+		}
+		advertiseTLS = ""
+	}
+	if !server.config.StartTLSOn {
+		// STARTTLS turned off, don't advertise it
+		advertiseTLS = ""
+	}
+
+	for client.isAlive() {
+		switch client.state {
+		case ClientGreeting:
+			client.responseAdd(greeting)
+			client.state = ClientCmd
+
+		case ClientCmd:
+			client.bufin.setLimit(CommandLineMaxLength)
+			input, err := server.read(client)
+			log.Debugf("Client sent: %s", input)
+			if err == io.EOF {
+				log.WithError(err).Warnf("Client closed the connection: %s", client.RemoteAddress)
+				return
+			} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
+				log.WithError(err).Warnf("Timeout: %s", client.RemoteAddress)
+				return
+			} else if err == LineLimitExceeded {
+				client.responseAdd("500 Line too long.")
+				client.kill()
+				break
+			} else if err != nil {
+				log.WithError(err).Warnf("Read error: %s", client.RemoteAddress)
+				client.kill()
+				break
+			}
+
+			input = strings.Trim(input, " \r\n")
+			cmdLen := len(input)
+			if cmdLen > CommandVerbMaxLength {
+				cmdLen = CommandVerbMaxLength
+			}
+			cmd := strings.ToUpper(input[:cmdLen])
+
+			switch {
+			case strings.Index(cmd, "HELO") == 0:
+				client.Helo = strings.Trim(input[4:], " ")
+				client.responseAdd(helo)
+
+			case strings.Index(cmd, "EHLO") == 0:
+				client.Helo = strings.Trim(input[4:], " ")
+				client.responseAdd(ehlo + messageSize + pipelining + advertiseTLS + help)
+
+			case strings.Index(cmd, "HELP") == 0:
+				client.responseAdd("214 OK\r\n" + messageSize + pipelining + advertiseTLS + help)
+
+			case strings.Index(cmd, "MAIL FROM:") == 0:
+				client.reset()
+				from, err := extractEmail(input[10:])
+				if err != nil {
+					client.responseAdd("550 Error: Invalid Sender")
+				} else {
+					client.MailFrom = from
+					client.responseAdd("250 OK")
+				}
+
+			case strings.Index(cmd, "RCPT TO:") == 0:
+				to, err := extractEmail(input[8:])
+				if err != nil {
+					client.responseAdd("550 Error: Invalid Recipient")
+				} else {
+					client.RcptTo = append(client.RcptTo, to)
+					client.responseAdd("250 OK")
+				}
+
+			case strings.Index(cmd, "RSET") == 0:
+				client.reset()
+				client.responseAdd("250 OK")
+
+			case strings.Index(cmd, "VRFY") == 0:
+				client.responseAdd("252 Cannot verify user")
+
+			case strings.Index(cmd, "NOOP") == 0:
+				client.responseAdd("250 OK")
+
+			case strings.Index(cmd, "QUIT") == 0:
+				client.responseAdd("221 Bye")
+				client.kill()
+
+			case strings.Index(cmd, "DATA") == 0:
+				client.responseAdd("354 Enter message, ending with '.' on a line by itself")
+				client.state = ClientData
+
+			case server.config.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
+				client.responseAdd("220 Ready to start TLS")
+				client.state = ClientStartTLS
+			default:
+
+				client.responseAdd("500 Unrecognized command: " + cmd)
+				client.errors++
+				if client.errors > MaxUnrecognizedCommands {
+					client.responseAdd("554 Too many unrecognized commands")
+					client.kill()
+				}
+			}
+
+		case ClientData:
+			var err error
+
+			client.bufin.setLimit(server.config.MaxSize)
+			client.Data, err = server.read(client)
+			if err != nil {
+				if err == LineLimitExceeded {
+					client.responseAdd("550 Error: " + LineLimitExceeded.Error())
+					client.kill()
+				} else if err == MessageSizeExceeded {
+					client.responseAdd("550 Error: " + MessageSizeExceeded.Error())
+					client.kill()
+				} else {
+					client.kill()
+					client.responseAdd("451 Error: " + err.Error())
+				}
+				log.WithError(err).Warn("Error reading data")
+				continue
+			}
+			client.state = ClientCmd
+
+			if client.MailFrom.isEmpty() {
+				client.responseAdd("550 Error: No sender")
+				continue
+			}
+			if len(client.RcptTo) == 0 {
+				client.responseAdd("550 Error: No recipients")
+				continue
+			}
+
+			if rcptErr := server.checkRcpt(client.RcptTo); rcptErr == nil {
+				res := server.backend.Process(client.Envelope)
+				if res.Code() < 300 {
+					client.messagesSent++
+				}
+				client.responseAdd(res.String())
+			} else {
+				client.responseAdd("550 Error: " + rcptErr.Error())
+			}
+
+		case ClientStartTLS:
+			if !client.TLS && server.config.StartTLSOn {
+				if server.upgradeToTLS(client) {
+					advertiseTLS = ""
+					client.reset()
+				}
+			}
+			// change to command state
+			client.state = ClientCmd
+		}
+
+		if len(client.response) > 0 {
+			log.Debugf("Writing response to client: \n%s", client.response)
+			err := server.writeResponse(client)
+			if err != nil {
+				log.WithError(err).Debug("Error writing response")
+				return
+			}
+		}
+
+	}
+}
+
+func (s *server) checkRcpt(RcptTo []*EmailAddress) error {
+	for _, rcpt := range RcptTo {
+		if !s.allowsHost(rcpt.Host) {
+			return errors.New("550 Error: Host not allowed: " + rcpt.Host)
+		}
+	}
+	return nil
+}

+ 0 - 83
server/goguerrilla.go

@@ -1,83 +0,0 @@
-/**
-Go-Guerrilla SMTPd
-
-Version: 1.5
-Author: Flashmob, GuerrillaMail.com
-Contact: [email protected]
-License: MIT
-Repository: https://github.com/flashmob/Go-Guerrilla-SMTPd
-Site: http://www.guerrillamail.com/
-
-See README for more details
-*/
-
-package server
-
-import (
-	"bufio"
-	"crypto/rand"
-	"crypto/tls"
-	"fmt"
-	"net"
-	"runtime"
-	"time"
-
-	log "github.com/Sirupsen/logrus"
-
-	guerrilla "github.com/flashmob/go-guerrilla"
-)
-
-func RunServer(mainConfig guerrilla.Config, sConfig guerrilla.ServerConfig, backend guerrilla.Backend) (err error) {
-	server := SmtpdServer{
-		mainConfig: mainConfig,
-		config:     sConfig,
-		sem:        make(chan int, sConfig.MaxClients),
-	}
-
-	// configure ssl
-	if sConfig.TLSAlwaysOn || sConfig.StartTLS {
-		cert, err := tls.LoadX509KeyPair(sConfig.PublicKeyFile, sConfig.PrivateKeyFile)
-		if err != nil {
-			return fmt.Errorf("error while loading the certificate: %s", err)
-		}
-		server.tlsConfig = &tls.Config{
-			Certificates: []tls.Certificate{cert},
-			ClientAuth:   tls.VerifyClientCertIfGiven,
-			ServerName:   sConfig.Hostname,
-		}
-		server.tlsConfig.Rand = rand.Reader
-	}
-
-	// configure timeout
-	server.timeout = time.Duration(sConfig.Timeout)
-
-	// Start listening for SMTP connections
-	listener, err := net.Listen("tcp", sConfig.ListenInterface)
-	if err != nil {
-		return fmt.Errorf("cannot listen on port, %v", err)
-	}
-
-	log.Infof("Listening on tcp %s", sConfig.ListenInterface)
-
-	var clientID int64
-	clientID = 1
-	for {
-		conn, err := listener.Accept()
-		if err != nil {
-			log.WithError(err).Infof("Accept error")
-			continue
-		}
-		log.Debugf("Number of serving goroutines: %d", runtime.NumGoroutine())
-		server.sem <- 1 // Wait for active queue to drain.
-		go server.handleClient(&guerrilla.Client{
-			Conn:        conn,
-			Address:     conn.RemoteAddr().String(),
-			Time:        time.Now().Unix(),
-			Bufin:       guerrilla.NewSMTPBufferedReader(conn),
-			Bufout:      bufio.NewWriter(conn),
-			ClientID:    clientID,
-			SavedNotify: make(chan int),
-		}, backend)
-		clientID++
-	}
-}

+ 0 - 289
server/smtpd.go

@@ -1,289 +0,0 @@
-package server
-
-import (
-	"bufio"
-	"crypto/tls"
-	"fmt"
-	"io"
-	"net"
-	"strings"
-	"time"
-
-	log "github.com/Sirupsen/logrus"
-
-	guerrilla "github.com/flashmob/go-guerrilla"
-	"github.com/flashmob/go-guerrilla/util"
-)
-
-type SmtpdServer struct {
-	mainConfig guerrilla.Config
-	config     guerrilla.ServerConfig
-	tlsConfig  *tls.Config
-	maxSize    int // max email DATA size
-	timeout    time.Duration
-	sem        chan int // currently active client list
-}
-
-// Upgrades the connection to TLS
-// Sets up buffers with the upgraded connection
-func (server *SmtpdServer) upgradeToTls(client *guerrilla.Client) bool {
-	var tlsConn *tls.Conn
-	tlsConn = tls.Server(client.Conn, server.tlsConfig)
-	err := tlsConn.Handshake()
-	if err == nil {
-		client.Conn = net.Conn(tlsConn)
-		client.Bufin = guerrilla.NewSMTPBufferedReader(client.Conn)
-		client.Bufout = bufio.NewWriter(client.Conn)
-		client.TLS = true
-
-		return true
-	}
-
-	log.WithError(err).Warn("Failed to TLS handshake")
-	return false
-}
-
-func (server *SmtpdServer) handleClient(client *guerrilla.Client, backend guerrilla.Backend) {
-	defer server.closeClient(client)
-	advertiseTLS := "250-STARTTLS\r\n"
-	if server.config.TLSAlwaysOn {
-		if server.upgradeToTls(client) {
-			advertiseTLS = ""
-		}
-	}
-	greeting := fmt.Sprintf("220 %s SMTP guerrillad(%s) #%d (%d) %s",
-		server.config.Hostname, guerrilla.Version, client.ClientID,
-		len(server.sem), time.Now().Format(time.RFC1123Z))
-
-	if !server.config.StartTLS {
-		// STARTTLS turned off
-		advertiseTLS = ""
-	}
-	for i := 0; i < 100; i++ {
-		switch client.State {
-		case 0:
-			responseAdd(client, greeting)
-			client.State = 1
-		case 1:
-			client.Bufin.SetLimit(guerrilla.CommandMaxLength)
-			input, err := server.readSmtp(client)
-			if err != nil {
-				if err == io.EOF {
-					log.WithError(err).Debugf("Client closed the connection already: %s", client.Address)
-					return
-				} else if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
-					log.WithError(err).Debugf("Timeout: %s", client.Address)
-					return
-				} else if err == guerrilla.InputLimitExceeded {
-					responseAdd(client, "500 Line too long.")
-					// kill it so that another one can connect
-					killClient(client)
-				}
-				log.WithError(err).Warnf("Read error: %s", client.Address)
-				break
-			}
-			input = strings.Trim(input, " \n\r")
-			bound := len(input)
-			if bound > 16 {
-				bound = 16
-			}
-			cmd := strings.ToUpper(input[0:bound])
-			switch {
-			case strings.Index(cmd, "HELO") == 0:
-				if len(input) > 5 {
-					client.Helo = input[5:]
-				}
-				responseAdd(client, "250 "+server.config.Hostname+" Hello ")
-			case strings.Index(cmd, "EHLO") == 0:
-				if len(input) > 5 {
-					client.Helo = input[5:]
-				}
-				responseAdd(client, fmt.Sprintf(
-					"250-%s Hello %s[%s]\r\n"+
-					"250-SIZE %d\r\n" +
-					"250-PIPELINING\r\n" +
-					"%s250 HELP",
-					server.config.Hostname, client.Helo, client.Address,
-					server.config.MaxSize, advertiseTLS))
-			case strings.Index(cmd, "HELP") == 0:
-				responseAdd(client, "250 Help! I need somebody...")
-			case strings.Index(cmd, "MAIL FROM:") == 0:
-				if len(input) > 10 {
-					client.MailFrom = input[10:]
-				}
-				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:
-				if len(input) > 8 {
-					client.RcptTo = input[8:]
-				}
-				responseAdd(client, "250 Accepted")
-			case strings.Index(cmd, "NOOP") == 0:
-				responseAdd(client, "250 OK")
-			case strings.Index(cmd, "RSET") == 0:
-				client.MailFrom = ""
-				client.RcptTo = ""
-				responseAdd(client, "250 OK")
-			case strings.Index(cmd, "DATA") == 0:
-				responseAdd(client, "354 Enter message, ending with \".\" on a line by itself")
-				client.State = 2
-			case (strings.Index(cmd, "STARTTLS") == 0) &&
-				!client.TLS &&
-				server.config.StartTLS:
-				responseAdd(client, "220 Ready to start TLS")
-				// go to start TLS state
-				client.State = 3
-			case strings.Index(cmd, "QUIT") == 0:
-				responseAdd(client, "221 Bye")
-				killClient(client)
-			default:
-				responseAdd(client, "500 unrecognized command: "+cmd)
-				client.Errors++
-				if client.Errors > 3 {
-					responseAdd(client, "500 Too many unrecognized commands")
-					killClient(client)
-				}
-			}
-		case 2:
-			var err error
-			client.Bufin.SetLimit(int64(server.config.MaxSize) + 1024000) // This is a hard limit.
-			client.Data, err = server.readSmtp(client)
-			if err == nil {
-				from, mailErr := util.ExtractEmail(client.MailFrom)
-				if mailErr != nil {
-					responseAdd(client, fmt.Sprintf("550 Error: invalid from: ", mailErr.Error()))
-				} else {
-					// TODO: support multiple RcptTo
-					to, mailErr := util.ExtractEmail(client.RcptTo)
-					if mailErr != nil {
-						responseAdd(client, fmt.Sprintf("550 Error: invalid from: ", mailErr.Error()))
-					} else {
-						client.MailFrom = from.String()
-						client.RcptTo = to.String()
-						if !server.mainConfig.IsAllowed(to.Host) {
-							responseAdd(client, "550 Error: not allowed")
-						} else {
-							toArray := []*guerrilla.EmailParts{to}
-							resp := backend.Process(client, from, toArray)
-							responseAdd(client, resp)
-						}
-					}
-				}
-
-			} else {
-				if err == guerrilla.InputLimitExceeded {
-					// hard limit reached, end to make room for other clients
-					responseAdd(client, "550 Error: DATA limit exceeded by more than a megabyte!")
-					killClient(client)
-				} else {
-					responseAdd(client, "550 Error: "+err.Error())
-				}
-
-				log.WithError(err).Warn("DATA read error")
-			}
-			client.State = 1
-		case 3:
-			// upgrade to TLS
-			if server.upgradeToTls(client) {
-				advertiseTLS = ""
-				client.State = 1
-			}
-		}
-		// Send a response back to the client
-		err := server.responseWrite(client)
-		if err != nil {
-			if err == io.EOF {
-				// client closed the connection already
-				return
-			}
-			if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
-				// too slow, timeout
-				return
-			}
-		}
-		if client.KillTime > 1 {
-			return
-		}
-	}
-
-}
-
-// add a response on the response buffer
-func responseAdd(client *guerrilla.Client, line string) {
-	client.Response = line + "\r\n"
-}
-func (server *SmtpdServer) closeClient(client *guerrilla.Client) {
-	client.Conn.Close()
-	<-server.sem // Done; enable next client to run.
-}
-func killClient(client *guerrilla.Client) {
-	client.KillTime = time.Now().Unix()
-}
-
-// Reads from the smtpBufferedReader, can be in command state or data state.
-func (server *SmtpdServer) readSmtp(client *guerrilla.Client) (input string, err error) {
-	var reply string
-	// Command state terminator by default
-	suffix := "\r\n"
-	if client.State == 2 {
-		// DATA state ends with a dot on a line by itself
-		suffix = "\r\n.\r\n"
-	}
-	for err == nil {
-		client.Conn.SetDeadline(time.Now().Add(server.timeout * time.Second))
-		reply, err = client.Bufin.ReadString('\n')
-		if reply != "" {
-			input = input + reply
-			if len(input) > server.config.MaxSize {
-				err = fmt.Errorf("Maximum DATA size exceeded (%d)", server.config.MaxSize)
-				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) {
-			// discard the suffix and stop reading
-			input = input[0:len(input)-len(suffix)]
-			break
-		}
-	}
-	return input, err
-}
-
-// Scan the data part for a Subject line. Can be a multi-line
-func scanSubject(client *guerrilla.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 (server *SmtpdServer) responseWrite(client *guerrilla.Client) (err error) {
-	var size int
-	client.Conn.SetDeadline(time.Now().Add(server.timeout * time.Second))
-	size, err = client.Bufout.WriteString(client.Response)
-	client.Bufout.Flush()
-	client.Response = client.Response[size:]
-	return err
-}

+ 35 - 0
util.go

@@ -0,0 +1,35 @@
+package guerrilla
+
+import (
+	"errors"
+	"regexp"
+	"strings"
+)
+
+var extractEmailRegex, _ = regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk!
+
+func extractEmail(str string) (*EmailAddress, error) {
+	email := &EmailAddress{}
+	var err error
+	if matched := extractEmailRegex.FindStringSubmatch(str); len(matched) > 2 {
+		email.User = matched[1]
+		email.Host = validHost(matched[2])
+	} else if res := strings.Split(str, "@"); len(res) > 1 {
+		email.User = res[0]
+		email.Host = validHost(res[1])
+	}
+	if email.User == "" || email.Host == "" {
+		err = errors.New("Invalid address, [" + email.User + "@" + email.Host + "] address:" + str)
+	}
+	return email, err
+}
+
+var validhostRegex, _ = regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
+
+func validHost(host string) string {
+	host = strings.Trim(host, " ")
+	if validhostRegex.MatchString(host) {
+		return host
+	}
+	return ""
+}