Explorar el Código

Added support for ENHANCEDSTATUSCODES (#51)

Support for ENHANCEDSTATUSCODES (rfc3463) - Issue #34
Philipp Resch hace 8 años
padre
commit
100f9bbd6f
Se han modificado 8 ficheros con 326 adiciones y 65 borrados
  1. 1 0
      Makefile
  2. 26 20
      cmd/guerrillad/serve_test.go
  3. 195 0
      response/enhanced.go
  4. 51 0
      response/enhanced_test.go
  5. 22 20
      server.go
  6. 4 3
      server_test.go
  7. 20 17
      tests/guerrilla_test.go
  8. 7 5
      util.go

+ 1 - 0
Makefile

@@ -28,3 +28,4 @@ test: *.go */*.go */*/*.go
 	$(GO_VARS) $(GO) test -v .
 	$(GO_VARS) $(GO) test -v .
 	$(GO_VARS) $(GO) test -v ./tests
 	$(GO_VARS) $(GO) test -v ./tests
 	$(GO_VARS) $(GO) test -v ./cmd/guerrillad
 	$(GO_VARS) $(GO) test -v ./cmd/guerrillad
+	$(GO_VARS) $(GO) test -v ./response

+ 26 - 20
cmd/guerrillad/serve_test.go

@@ -5,20 +5,22 @@ import (
 	"bytes"
 	"bytes"
 	"crypto/tls"
 	"crypto/tls"
 	"encoding/json"
 	"encoding/json"
-	log "github.com/Sirupsen/logrus"
-	"github.com/flashmob/go-guerrilla"
-	"github.com/flashmob/go-guerrilla/backends"
-	test "github.com/flashmob/go-guerrilla/tests"
-	"github.com/flashmob/go-guerrilla/tests/testcert"
-	"github.com/spf13/cobra"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
+	"runtime"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"testing"
 	"testing"
 	"time"
 	"time"
+
+	log "github.com/Sirupsen/logrus"
+	"github.com/flashmob/go-guerrilla"
+	"github.com/flashmob/go-guerrilla/backends"
+	test "github.com/flashmob/go-guerrilla/tests"
+	"github.com/flashmob/go-guerrilla/tests/testcert"
+	"github.com/spf13/cobra"
 )
 )
 
 
 var configJsonA = `
 var configJsonA = `
@@ -343,16 +345,20 @@ func TestServe(t *testing.T) {
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonB), 0644)
 	ioutil.WriteFile("configJsonA.json", []byte(configJsonB), 0644)
 
 
 	// test SIGHUP via the kill command
 	// test SIGHUP via the kill command
-	ecmd := exec.Command("kill", "-HUP", string(data))
-	_, err = ecmd.Output()
-	if err != nil {
-		t.Error("could not SIGHUP", err)
-		t.FailNow()
-	}
-	time.Sleep(time.Second) // allow sighup to do its job
-	// did the pidfile change as expected?
-	if _, err := os.Stat("./pidfile2.pid"); os.IsNotExist(err) {
-		t.Error("pidfile not changed after sighup SIGHUP", err)
+	// Would not work on windows as kill is not available.
+	// TODO: Implement an alternative test for windows.
+	if runtime.GOOS != "windows" {
+		ecmd := exec.Command("kill", "-HUP", string(data))
+		_, err = ecmd.Output()
+		if err != nil {
+			t.Error("could not SIGHUP", err)
+			t.FailNow()
+		}
+		time.Sleep(time.Second) // allow sighup to do its job
+		// did the pidfile change as expected?
+		if _, err := os.Stat("./pidfile2.pid"); os.IsNotExist(err) {
+			t.Error("pidfile not changed after sighup SIGHUP", err)
+		}
 	}
 	}
 	// send kill signal and wait for exit
 	// send kill signal and wait for exit
 	sigKill()
 	sigKill()
@@ -681,7 +687,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 				t.Error("Expected", expect, "but got", result)
 				t.Error("Expected", expect, "but got", result)
 			} else {
 			} else {
 				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
 				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
-					expect := "454 Error: Relay access denied: grr.la"
+					expect := "454 4.1.1 Error: Relay access denied: grr.la"
 					if strings.Index(result, expect) != 0 {
 					if strings.Index(result, expect) != 0 {
 						t.Error("Expected:", expect, "but got:", result)
 						t.Error("Expected:", expect, "but got:", result)
 					}
 					}
@@ -714,7 +720,7 @@ func TestAllowedHostsEvent(t *testing.T) {
 				t.Error("Expected", expect, "but got", result)
 				t.Error("Expected", expect, "but got", result)
 			} else {
 			} else {
 				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
 				if result, err = test.Command(conn, buffin, "RCPT TO:[email protected]"); err == nil {
-					expect := "250 OK"
+					expect := "250 2.1.5 OK"
 					if strings.Index(result, expect) != 0 {
 					if strings.Index(result, expect) != 0 {
 						t.Error("Expected:", expect, "but got:", result)
 						t.Error("Expected:", expect, "but got:", result)
 					}
 					}
@@ -791,7 +797,7 @@ func TestTLSConfigEvent(t *testing.T) {
 					t.Error("Expected", expect, "but got", result)
 					t.Error("Expected", expect, "but got", result)
 				} else {
 				} else {
 					if result, err = test.Command(conn, buffin, "STARTTLS"); err == nil {
 					if result, err = test.Command(conn, buffin, "STARTTLS"); err == nil {
-						expect := "220 Ready to start TLS"
+						expect := "220 2.0.0 Ready to start TLS"
 						if strings.Index(result, expect) != 0 {
 						if strings.Index(result, expect) != 0 {
 							t.Error("Expected:", expect, "but got:", result)
 							t.Error("Expected:", expect, "but got:", result)
 						} else {
 						} else {
@@ -894,7 +900,7 @@ func TestBadTLS(t *testing.T) {
 					t.Error("Expected", expect, "but got", result)
 					t.Error("Expected", expect, "but got", result)
 				} else {
 				} else {
 					if result, err = test.Command(conn, buffin, "STARTTLS"); err == nil {
 					if result, err = test.Command(conn, buffin, "STARTTLS"); err == nil {
-						expect := "220 Ready to start TLS"
+						expect := "220 2.0.0 Ready to start TLS"
 						if strings.Index(result, expect) != 0 {
 						if strings.Index(result, expect) != 0 {
 							t.Error("Expected:", expect, "but got:", result)
 							t.Error("Expected:", expect, "but got:", result)
 						} else {
 						} else {

+ 195 - 0
response/enhanced.go

@@ -0,0 +1,195 @@
+package response
+
+import (
+	"fmt"
+	"strconv"
+)
+
+const (
+	// ClassSuccess specifies that the DSN is reporting a positive delivery
+	// action.  Detail sub-codes may provide notification of
+	// transformations required for delivery.
+	ClassSuccess = 2
+	// ClassTransientFailure - a persistent transient failure is one in which the message as
+	// sent is valid, but persistence of some temporary condition has
+	// caused abandonment or delay of attempts to send the message.
+	// If this code accompanies a delivery failure report, sending in
+	// the future may be successful.
+	ClassTransientFailure = 4
+	// ClassPermanentFailure - a permanent failure is one which is not likely to be resolved
+	// by resending the message in the current form.  Some change to
+	// the message or the destination must be made for successful
+	// delivery.
+	ClassPermanentFailure = 5
+)
+
+// codeMap for mapping Enhanced Status Code to Basic Code
+// Mapping according to https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xml
+// This might not be entierly useful
+var codeMap = struct {
+	m map[string]int
+}{m: map[string]int{
+	"2.1.5":  250,
+	"2.3.0":  250,
+	"2.5.0":  250,
+	"2.6.4":  250,
+	"2.6.8":  252,
+	"2.7.0":  220,
+	"4.1.1":  451,
+	"4.1.8":  451,
+	"4.2.4":  450,
+	"4.3.0":  421,
+	"4.3.1":  452,
+	"4.3.2":  453,
+	"4.4.1":  451,
+	"4.4.2":  421,
+	"4.4.3":  451,
+	"4.4.5":  451,
+	"4.5.0":  451,
+	"4.5.1":  430,
+	"4.5.3":  452,
+	"4.5.4":  451,
+	"4.7.0":  450,
+	"4.7.1":  451,
+	"4.7.12": 422,
+	"4.7.15": 450,
+	"4.7.24": 451,
+	"5.1.1":  550,
+	"5.1.3":  501,
+	"5.1.8":  501,
+	"5.1.10": 556,
+	"5.2.2":  552,
+	"5.2.3":  552,
+	"5.3.0":  550,
+	"5.3.4":  552,
+	"5.4.3":  550,
+	"5.5.0":  501,
+	"5.5.1":  500,
+	"5.5.2":  500,
+	"5.5.4":  501,
+	"5.5.6":  500,
+	"5.6.3":  554,
+	"5.6.6":  554,
+	"5.6.7":  553,
+	"5.6.8":  550,
+	"5.6.9":  550,
+	"5.7.0":  550,
+	"5.7.1":  551,
+	"5.7.2":  550,
+	"5.7.4":  504,
+	"5.7.8":  554,
+	"5.7.9":  534,
+	"5.7.10": 523,
+	"5.7.11": 524,
+	"5.7.13": 525,
+	"5.7.14": 535,
+	"5.7.15": 550,
+	"5.7.16": 552,
+	"5.7.17": 500,
+	"5.7.18": 500,
+	"5.7.19": 500,
+	"5.7.20": 550,
+	"5.7.21": 550,
+	"5.7.22": 550,
+	"5.7.23": 550,
+	"5.7.24": 550,
+	"5.7.25": 550,
+	"5.7.26": 550,
+	"5.7.27": 550,
+}}
+
+// DefaultMap contains defined default codes (RfC 3463)
+const (
+	OtherStatus                             = ".0.0"
+	OtherAddressStatus                      = ".1.0"
+	BadDestinationMailboxAddress            = ".1.1"
+	BadDestinationSystemAddress             = ".1.2"
+	BadDestinationMailboxAddressSyntax      = ".1.3"
+	DestinationMailboxAddressAmbiguous      = ".1.4"
+	DestinationMailboxAddressValid          = ".1.5"
+	MailboxHasMoved                         = ".1.6"
+	BadSendersMailboxAddressSyntax          = ".1.7"
+	BadSendersSystemAddress                 = ".1.8"
+	OtherOrUndefinedMailboxStatus           = ".2.0"
+	MailboxDisabled                         = ".2.1"
+	MailboxFull                             = ".2.2"
+	MessageLengthExceedsAdministrativeLimit = ".2.3"
+	MailingListExpansionProblem             = ".2.4"
+	OtherOrUndefinedMailSystemStatus        = ".3.0"
+	MailSystemFull                          = ".3.1"
+	SystemNotAcceptingNetworkMessages       = ".3.2"
+	SystemNotCapableOfSelectedFeatures      = ".3.3"
+	MessageTooBigForSystem                  = ".3.4"
+	OtherOrUndefinedNetworkOrRoutingStatus  = ".4.0"
+	NoAnswerFromHost                        = ".4.1"
+	BadConnection                           = ".4.2"
+	RoutingServerFailure                    = ".4.3"
+	UnableToRoute                           = ".4.4"
+	NetworkCongestion                       = ".4.5"
+	RoutingLoopDetected                     = ".4.6"
+	DeliveryTimeExpired                     = ".4.7"
+	OtherOrUndefinedProtocolStatus          = ".5.0"
+	InvalidCommand                          = ".5.1"
+	SyntaxError                             = ".5.2"
+	TooManyRecipients                       = ".5.3"
+	InvalidCommandArguments                 = ".5.4"
+	WrongProtocolVersion                    = ".5.5"
+	OtherOrUndefinedMediaError              = ".6.0"
+	MediaNotSupported                       = ".6.1"
+	ConversionRequiredAndProhibited         = ".6.2"
+	ConversionRequiredButNotSupported       = ".6.3"
+	ConversionWithLossPerformed             = ".6.4"
+	ConversionFailed                        = ".6.5"
+)
+
+// TODO: More defaults needed....
+var defaultTexts = struct {
+	m map[string]string
+}{m: map[string]string{
+	"2.0.0": "OK",
+	"2.1.0": "OK",
+	"2.1.5": "Recipient valid",
+	"2.5.0": "OK",
+	"4.5.3": "Too many recipients",
+	"4.5.4": "Relay access denied",
+	"5.5.1": "Invalid command",
+}}
+
+// CustomString builds an enhanced status code string using your custom string and basic code
+func CustomString(enhancedCode string, basicCode, class int, comment string) string {
+	e := buildEnhancedResponseFromDefaultStatus(class, enhancedCode)
+	return fmt.Sprintf("%d %s %s", basicCode, e, comment)
+}
+
+// String builds an enhanced status code string
+func String(enhancedCode string, class int) string {
+	e := buildEnhancedResponseFromDefaultStatus(class, enhancedCode)
+	basicCode := getBasicStatusCode(e)
+	comment := defaultTexts.m[enhancedCode]
+
+	if len(comment) == 0 {
+		switch class {
+		case 2:
+			comment = "OK"
+		case 4:
+			comment = "Temporary failure."
+		case 5:
+			comment = "Permanent failure."
+		}
+	}
+	return CustomString(enhancedCode, basicCode, class, comment)
+}
+
+func getBasicStatusCode(enhancedStatusCode string) int {
+	if val, ok := codeMap.m[enhancedStatusCode]; ok {
+		return val
+	}
+	// Fallback if code is not defined
+	fb, _ := strconv.Atoi(fmt.Sprintf("%c00", enhancedStatusCode[0]))
+	return fb
+}
+
+func buildEnhancedResponseFromDefaultStatus(c int, status string) string {
+	// Construct code
+	return fmt.Sprintf("%d%s", c, status)
+}

+ 51 - 0
response/enhanced_test.go

@@ -0,0 +1,51 @@
+package response
+
+import "testing"
+
+func TestClass(t *testing.T) {
+	if ClassPermanentFailure != 5 {
+		t.Error("ClassPermanentFailure is not 5")
+	}
+	if ClassTransientFailure != 4 {
+		t.Error("ClassTransientFailure is not 4")
+	}
+	if ClassSuccess != 2 {
+		t.Error("ClassSuccess is not 2")
+	}
+}
+
+func TestGetBasicStatusCode(t *testing.T) {
+	// Known status code
+	a := getBasicStatusCode("2.5.0")
+	if a != 250 {
+		t.Errorf("getBasicStatusCode. Int \"%d\" not expected.", a)
+	}
+
+	// Unknown status code
+	b := getBasicStatusCode("2.0.0")
+	if b != 200 {
+		t.Errorf("getBasicStatusCode. Int \"%d\" not expected.", b)
+	}
+}
+
+// TestString for the String function
+func TestCustomString(t *testing.T) {
+	// Basic testing
+	a := CustomString(OtherStatus, 200, ClassSuccess, "Test")
+	if a != "200 2.0.0 Test" {
+		t.Errorf("CustomString failed. String \"%s\" not expected.", a)
+	}
+
+	// Default String
+	b := String(OtherStatus, ClassSuccess)
+	if b != "200 2.0.0 OK" {
+		t.Errorf("String failed. String \"%s\" not expected.", b)
+	}
+}
+
+func TestBuildEnhancedResponseFromDefaultStatus(t *testing.T) {
+	a := buildEnhancedResponseFromDefaultStatus(ClassPermanentFailure, InvalidCommand)
+	if a != "5.5.1" {
+		t.Errorf("buildEnhancedResponseFromDefaultStatus failed. String \"%s\" not expected.", a)
+	}
+}

+ 22 - 20
server.go

@@ -17,6 +17,7 @@ import (
 	"sync/atomic"
 	"sync/atomic"
 
 
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/response"
 )
 )
 
 
 const (
 const (
@@ -272,6 +273,7 @@ func (server *server) handleClient(client *client) {
 	messageSize := fmt.Sprintf("250-SIZE %d\r\n", sc.MaxSize)
 	messageSize := fmt.Sprintf("250-SIZE %d\r\n", sc.MaxSize)
 	pipelining := "250-PIPELINING\r\n"
 	pipelining := "250-PIPELINING\r\n"
 	advertiseTLS := "250-STARTTLS\r\n"
 	advertiseTLS := "250-STARTTLS\r\n"
+	advertiseEnhancedStatusCodes := "250-ENHANCEDSTATUSCODES\r\n"
 	// the last line doesn't need \r\n since string will be printed as a new line
 	// the last line doesn't need \r\n since string will be printed as a new line
 	help := "250 HELP"
 	help := "250 HELP"
 
 
@@ -305,7 +307,7 @@ func (server *server) handleClient(client *client) {
 				log.WithError(err).Warnf("Timeout: %s", client.RemoteAddress)
 				log.WithError(err).Warnf("Timeout: %s", client.RemoteAddress)
 				return
 				return
 			} else if err == LineLimitExceeded {
 			} else if err == LineLimitExceeded {
-				client.responseAdd("500 Line too long.")
+				client.responseAdd(response.CustomString(response.InvalidCommand, 554, response.ClassPermanentFailure, "Line too long."))
 				client.kill()
 				client.kill()
 				break
 				break
 			} else if err != nil {
 			} else if err != nil {
@@ -334,14 +336,14 @@ func (server *server) handleClient(client *client) {
 			case strings.Index(cmd, "EHLO") == 0:
 			case strings.Index(cmd, "EHLO") == 0:
 				client.Helo = strings.Trim(input[4:], " ")
 				client.Helo = strings.Trim(input[4:], " ")
 				client.resetTransaction()
 				client.resetTransaction()
-				client.responseAdd(ehlo + messageSize + pipelining + advertiseTLS + help)
+				client.responseAdd(ehlo + messageSize + pipelining + advertiseTLS + advertiseEnhancedStatusCodes + help)
 
 
 			case strings.Index(cmd, "HELP") == 0:
 			case strings.Index(cmd, "HELP") == 0:
 				client.responseAdd("214 OK\r\n" + messageSize + pipelining + advertiseTLS + help)
 				client.responseAdd("214 OK\r\n" + messageSize + pipelining + advertiseTLS + help)
 
 
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 			case strings.Index(cmd, "MAIL FROM:") == 0:
 				if client.isInTransaction() {
 				if client.isInTransaction() {
-					client.responseAdd("503 Error: nested MAIL command")
+					client.responseAdd(response.CustomString(response.InvalidCommand, 503, response.ClassPermanentFailure, "Error: nested MAIL command"))
 					break
 					break
 				}
 				}
 				from, err := extractEmail(input[10:])
 				from, err := extractEmail(input[10:])
@@ -349,12 +351,12 @@ func (server *server) handleClient(client *client) {
 					client.responseAdd(err.Error())
 					client.responseAdd(err.Error())
 				} else {
 				} else {
 					client.MailFrom = from
 					client.MailFrom = from
-					client.responseAdd("250 OK")
+					client.responseAdd(response.CustomString(response.OtherAddressStatus, 250, response.ClassSuccess, "OK"))
 				}
 				}
 
 
 			case strings.Index(cmd, "RCPT TO:") == 0:
 			case strings.Index(cmd, "RCPT TO:") == 0:
 				if len(client.RcptTo) > RFC2821LimitRecipients {
 				if len(client.RcptTo) > RFC2821LimitRecipients {
-					client.responseAdd("452 Too many recipients")
+					client.responseAdd(response.CustomString(response.TooManyRecipients, 452, response.ClassTransientFailure, "Too many recipients"))
 					break
 					break
 				}
 				}
 				to, err := extractEmail(input[8:])
 				to, err := extractEmail(input[8:])
@@ -362,48 +364,48 @@ func (server *server) handleClient(client *client) {
 					client.responseAdd(err.Error())
 					client.responseAdd(err.Error())
 				} else {
 				} else {
 					if !server.allowsHost(to.Host) {
 					if !server.allowsHost(to.Host) {
-						client.responseAdd("454 Error: Relay access denied: " + to.Host)
+						client.responseAdd(response.CustomString(response.BadDestinationMailboxAddress, 454, response.ClassTransientFailure, "Error: Relay access denied: "+to.Host))
 					} else {
 					} else {
 						client.RcptTo = append(client.RcptTo, *to)
 						client.RcptTo = append(client.RcptTo, *to)
-						client.responseAdd("250 OK")
+						client.responseAdd(response.String(response.DestinationMailboxAddressValid, response.ClassSuccess))
 					}
 					}
 				}
 				}
 
 
 			case strings.Index(cmd, "RSET") == 0:
 			case strings.Index(cmd, "RSET") == 0:
 				client.resetTransaction()
 				client.resetTransaction()
-				client.responseAdd("250 OK")
+				client.responseAdd(response.CustomString(response.OtherAddressStatus, 250, response.ClassSuccess, "OK"))
 
 
 			case strings.Index(cmd, "VRFY") == 0:
 			case strings.Index(cmd, "VRFY") == 0:
-				client.responseAdd("252 Cannot verify user")
+				client.responseAdd(response.CustomString(response.OtherOrUndefinedProtocolStatus, 252, response.ClassSuccess, "Cannot verify user"))
 
 
 			case strings.Index(cmd, "NOOP") == 0:
 			case strings.Index(cmd, "NOOP") == 0:
-				client.responseAdd("250 OK")
+				client.responseAdd(response.String(response.DestinationMailboxAddressValid, response.ClassSuccess))
 
 
 			case strings.Index(cmd, "QUIT") == 0:
 			case strings.Index(cmd, "QUIT") == 0:
-				client.responseAdd("221 Bye")
+				client.responseAdd(response.CustomString(response.OtherStatus, 221, response.ClassSuccess, "Bye"))
 				client.kill()
 				client.kill()
 
 
 			case strings.Index(cmd, "DATA") == 0:
 			case strings.Index(cmd, "DATA") == 0:
 				if client.MailFrom.IsEmpty() {
 				if client.MailFrom.IsEmpty() {
-					client.responseAdd("503 Error: No sender")
+					client.responseAdd(response.CustomString(response.InvalidCommand, 503, response.ClassPermanentFailure, "Error: No sender"))
 					break
 					break
 				}
 				}
 				if len(client.RcptTo) == 0 {
 				if len(client.RcptTo) == 0 {
-					client.responseAdd("503 Error: No recipients")
+					client.responseAdd(response.CustomString(response.InvalidCommand, 503, response.ClassPermanentFailure, "Error: No recipients"))
 					break
 					break
 				}
 				}
 				client.responseAdd("354 Enter message, ending with '.' on a line by itself")
 				client.responseAdd("354 Enter message, ending with '.' on a line by itself")
 				client.state = ClientData
 				client.state = ClientData
 
 
 			case sc.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
 			case sc.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0:
-				client.responseAdd("220 Ready to start TLS")
+				client.responseAdd(response.CustomString(response.OtherStatus, 220, response.ClassSuccess, "Ready to start TLS"))
 				client.state = ClientStartTLS
 				client.state = ClientStartTLS
 			default:
 			default:
 
 
-				client.responseAdd("500 Unrecognized command: " + cmd)
+				client.responseAdd(response.CustomString(response.SyntaxError, 500, response.ClassPermanentFailure, "Unrecognized command: "+cmd))
 				client.errors++
 				client.errors++
 				if client.errors > MaxUnrecognizedCommands {
 				if client.errors > MaxUnrecognizedCommands {
-					client.responseAdd("554 Too many unrecognized commands")
+					client.responseAdd(response.CustomString(response.InvalidCommand, 554, response.ClassPermanentFailure, "Too many unrecognized commands"))
 					client.kill()
 					client.kill()
 				}
 				}
 			}
 			}
@@ -420,14 +422,14 @@ func (server *server) handleClient(client *client) {
 			}
 			}
 			if err != nil {
 			if err != nil {
 				if err == LineLimitExceeded {
 				if err == LineLimitExceeded {
-					client.responseAdd("550 Error: " + LineLimitExceeded.Error())
+					client.responseAdd(response.CustomString(response.SyntaxError, 550, response.ClassPermanentFailure, "Error: "+LineLimitExceeded.Error()))
 					client.kill()
 					client.kill()
 				} else if err == MessageSizeExceeded {
 				} else if err == MessageSizeExceeded {
-					client.responseAdd("550 Error: " + MessageSizeExceeded.Error())
+					client.responseAdd(response.CustomString(response.SyntaxError, 550, response.ClassPermanentFailure, "Error: "+MessageSizeExceeded.Error()))
 					client.kill()
 					client.kill()
 				} else {
 				} else {
 					client.kill()
 					client.kill()
-					client.responseAdd("451 Error: " + err.Error())
+					client.responseAdd(response.CustomString(response.OtherOrUndefinedMailSystemStatus, 451, response.ClassTransientFailure, "Error: "+err.Error()))
 				}
 				}
 				log.WithError(err).Warn("Error reading data")
 				log.WithError(err).Warn("Error reading data")
 				break
 				break
@@ -456,7 +458,7 @@ func (server *server) handleClient(client *client) {
 			client.state = ClientCmd
 			client.state = ClientCmd
 		case ClientShutdown:
 		case ClientShutdown:
 			// shutdown state
 			// shutdown state
-			client.responseAdd("421 Server is shutting down. Please try again later. Sayonara!")
+			client.responseAdd(response.CustomString(response.OtherOrUndefinedMailSystemStatus, 421, response.ClassTransientFailure, "Server is shutting down. Please try again later. Sayonara!"))
 			client.kill()
 			client.kill()
 		}
 		}
 
 

+ 4 - 3
server_test.go

@@ -5,11 +5,12 @@ import (
 
 
 	"bufio"
 	"bufio"
 	"fmt"
 	"fmt"
-	"github.com/flashmob/go-guerrilla/backends"
-	"github.com/flashmob/go-guerrilla/mocks"
 	"net/textproto"
 	"net/textproto"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
+
+	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/mocks"
 )
 )
 
 
 // getMockServerConfig gets a mock ServerConfig struct used for creating a new server
 // getMockServerConfig gets a mock ServerConfig struct used for creating a new server
@@ -72,7 +73,7 @@ func TestHandleClient(t *testing.T) {
 	w.PrintfLine("QUIT")
 	w.PrintfLine("QUIT")
 	line, _ = r.ReadLine()
 	line, _ = r.ReadLine()
 	fmt.Println("line is:", line)
 	fmt.Println("line is:", line)
-	expected := "221 Bye"
+	expected := "221 2.0.0 Bye"
 	if strings.Index(line, expected) != 0 {
 	if strings.Index(line, expected) != 0 {
 		t.Error("expected", expected, "but got:", line)
 		t.Error("expected", expected, "but got:", line)
 	}
 	}

+ 20 - 17
tests/guerrilla_test.go

@@ -15,12 +15,14 @@ package test
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	log "github.com/Sirupsen/logrus"
 	"testing"
 	"testing"
 
 
+	log "github.com/Sirupsen/logrus"
+
+	"time"
+
 	"github.com/flashmob/go-guerrilla"
 	"github.com/flashmob/go-guerrilla"
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/backends"
-	"time"
 
 
 	"bufio"
 	"bufio"
 
 
@@ -28,10 +30,11 @@ import (
 	"crypto/tls"
 	"crypto/tls"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"github.com/flashmob/go-guerrilla/tests/testcert"
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
 	"strings"
 	"strings"
+
+	"github.com/flashmob/go-guerrilla/tests/testcert"
 )
 )
 
 
 type TestConfig struct {
 type TestConfig struct {
@@ -287,7 +290,7 @@ func TestShutDown(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("Help command failed", err.Error())
 				t.Error("Help command failed", err.Error())
 			}
 			}
-			expected := "421 Server is shutting down. Please try again later. Sayonara!"
+			expected := "421 4.3.0 Server is shutting down. Please try again later. Sayonara!"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not shut down with", expected, ", it said:"+response)
 				t.Error("Server did not shut down with", expected, ", it said:"+response)
 			}
 			}
@@ -346,7 +349,7 @@ func TestRFC2821LimitRecipients(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 				t.Error("rcpt command failed", err.Error())
 			}
 			}
-			expected := "452 Too many recipients"
+			expected := "452 4.5.3 Too many recipients"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -393,7 +396,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 				t.Error("rcpt command failed", err.Error())
 			}
 			}
-			expected := "501 Local part too long"
+			expected := "550 5.5.4 Local part too long"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -403,7 +406,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 				t.Error("rcpt command failed", err.Error())
 			}
 			}
-			expected = "250 OK"
+			expected = "250 2.1.5 OK"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -450,7 +453,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 				t.Error("rcpt command failed", err.Error())
 			}
 			}
-			expected := "501 Path too long"
+			expected := "550 5.5.4 Path too long"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -460,7 +463,7 @@ func TestRFC2821LimitPath(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 				t.Error("rcpt command failed", err.Error())
 			}
 			}
-			expected = "454 Error: Relay access denied"
+			expected = "454 4.1.1 Error: Relay access denied"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -502,7 +505,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("command failed", err.Error())
 				t.Error("command failed", err.Error())
 			}
 			}
-			expected := "501 Path too long"
+			expected := "550 5.5.4 Path too long"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -512,7 +515,7 @@ func TestRFC2821LimitDomain(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("command failed", err.Error())
 				t.Error("command failed", err.Error())
 			}
 			}
-			expected = "454 Error: Relay access denied"
+			expected = "454 4.1.1 Error: Relay access denied"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -558,7 +561,7 @@ func TestNestedMailCmd(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("command failed", err.Error())
 				t.Error("command failed", err.Error())
 			}
 			}
-			expected := "503 Error: nested MAIL command"
+			expected := "503 5.5.1 Error: nested MAIL command"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -570,7 +573,7 @@ func TestNestedMailCmd(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("command failed", err.Error())
 				t.Error("command failed", err.Error())
 			}
 			}
-			expected = "250 OK"
+			expected = "250 2.1.0 OK"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -579,7 +582,7 @@ func TestNestedMailCmd(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("command failed", err.Error())
 				t.Error("command failed", err.Error())
 			}
 			}
-			expected = "250 OK"
+			expected = "250 2.1.0 OK"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -588,7 +591,7 @@ func TestNestedMailCmd(t *testing.T) {
 			if err != nil {
 			if err != nil {
 				t.Error("command failed", err.Error())
 				t.Error("command failed", err.Error())
 			}
 			}
-			expected = "250 OK"
+			expected = "250 2.1.0 OK"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -633,7 +636,7 @@ func TestCommandLineMaxLength(t *testing.T) {
 				t.Error("command failed", err.Error())
 				t.Error("command failed", err.Error())
 			}
 			}
 
 
-			expected := "500 Line too long"
+			expected := "554 5.5.1 Line too long"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 				t.Error("Server did not respond with", expected, ", it said:"+response)
 			}
 			}
@@ -695,7 +698,7 @@ func TestDataMaxLength(t *testing.T) {
 					strings.Repeat("n", int(config.Servers[0].MaxSize-20))))
 					strings.Repeat("n", int(config.Servers[0].MaxSize-20))))
 
 
 			//expected := "500 Line too long"
 			//expected := "500 Line too long"
-			expected := "451 Error: Maximum DATA size exceeded"
+			expected := "451 4.3.0 Error: Maximum DATA size exceeded"
 			if strings.Index(response, expected) != 0 {
 			if strings.Index(response, expected) != 0 {
 				t.Error("Server did not respond with", expected, ", it said:"+response, err)
 				t.Error("Server did not respond with", expected, ", it said:"+response, err)
 			}
 			}

+ 7 - 5
util.go

@@ -2,9 +2,11 @@ package guerrilla
 
 
 import (
 import (
 	"errors"
 	"errors"
-	"github.com/flashmob/go-guerrilla/envelope"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
+
+	"github.com/flashmob/go-guerrilla/envelope"
+	"github.com/flashmob/go-guerrilla/response"
 )
 )
 
 
 var extractEmailRegex, _ = regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk!
 var extractEmailRegex, _ = regexp.Compile(`<(.+?)@(.+?)>`) // go home regex, you're drunk!
@@ -13,7 +15,7 @@ func extractEmail(str string) (*envelope.EmailAddress, error) {
 	email := &envelope.EmailAddress{}
 	email := &envelope.EmailAddress{}
 	var err error
 	var err error
 	if len(str) > RFC2821LimitPath {
 	if len(str) > RFC2821LimitPath {
-		return email, errors.New("501 Path too long")
+		return email, errors.New(response.CustomString(response.InvalidCommandArguments, 550, response.ClassPermanentFailure, "Path too long"))
 	}
 	}
 	if matched := extractEmailRegex.FindStringSubmatch(str); len(matched) > 2 {
 	if matched := extractEmailRegex.FindStringSubmatch(str); len(matched) > 2 {
 		email.User = matched[1]
 		email.User = matched[1]
@@ -24,11 +26,11 @@ func extractEmail(str string) (*envelope.EmailAddress, error) {
 	}
 	}
 	err = nil
 	err = nil
 	if email.User == "" || email.Host == "" {
 	if email.User == "" || email.Host == "" {
-		err = errors.New("501 Invalid address")
+		err = errors.New(response.CustomString(response.InvalidCommandArguments, 501, response.ClassPermanentFailure, "Invalid address"))
 	} else if len(email.User) > RFC2832LimitLocalPart {
 	} else if len(email.User) > RFC2832LimitLocalPart {
-		err = errors.New("501 Local part too long, cannot exceed 64 characters")
+		err = errors.New(response.CustomString(response.InvalidCommandArguments, 550, response.ClassPermanentFailure, "Local part too long, cannot exceed 64 characters"))
 	} else if len(email.Host) > RFC2821LimitDomain {
 	} else if len(email.Host) > RFC2821LimitDomain {
-		err = errors.New("501 Domain cannot exceed 255 characters")
+		err = errors.New(response.CustomString(response.InvalidCommandArguments, 501, response.ClassPermanentFailure, "Domain cannot exceed 255 characters"))
 	}
 	}
 	return email, err
 	return email, err
 }
 }