diff --git a/conn.go b/conn.go index 587af432..508c9aa4 100644 --- a/conn.go +++ b/conn.go @@ -61,11 +61,11 @@ func (c *Conn) init() { } func (c *Conn) unrecognizedCommand(cmd string) { - c.WriteResponse(500, fmt.Sprintf("Syntax error, %v command unrecognized", cmd)) + c.WriteResponse(500, EnhancedCode{5, 5, 2}, fmt.Sprintf("Syntax error, %v command unrecognized", cmd)) c.nbrErrors++ if c.nbrErrors > 3 { - c.WriteResponse(500, "Too many unrecognized commands") + c.WriteResponse(500, EnhancedCode{5, 5, 2}, "Too many unrecognized commands") c.Close() } } @@ -73,7 +73,7 @@ func (c *Conn) unrecognizedCommand(cmd string) { // Commands are dispatched to the appropriate handler functions. func (c *Conn) handle(cmd string, arg string) { if cmd == "" { - c.WriteResponse(500, "Speak up") + c.WriteResponse(500, EnhancedCode{5, 5, 2}, "Speak up") return } @@ -81,15 +81,15 @@ func (c *Conn) handle(cmd string, arg string) { switch cmd { case "SEND", "SOML", "SAML", "EXPN", "HELP", "TURN": // These commands are not implemented in any state - c.WriteResponse(502, fmt.Sprintf("%v command not implemented", cmd)) + c.WriteResponse(502, EnhancedCode{5, 5, 1}, fmt.Sprintf("%v command not implemented", cmd)) case "HELO", "EHLO", "LHLO": lmtp := cmd == "LHLO" enhanced := lmtp || cmd == "EHLO" if c.server.LMTP && !lmtp { - c.WriteResponse(500, "This is a LMTP server, use LHLO") + c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is a LMTP server, use LHLO") } if !c.server.LMTP && lmtp { - c.WriteResponse(500, "This is not a LMTP server") + c.WriteResponse(500, EnhancedCode{5, 5, 1}, "This is not a LMTP server") } c.handleGreet(enhanced, arg) case "MAIL": @@ -97,16 +97,16 @@ func (c *Conn) handle(cmd string, arg string) { case "RCPT": c.handleRcpt(arg) case "VRFY": - c.WriteResponse(252, "Cannot VRFY user, but will accept message") + c.WriteResponse(252, EnhancedCode{2, 5, 0}, "Cannot VRFY user, but will accept message") case "NOOP": - c.WriteResponse(250, "I have sucessfully done nothing") + c.WriteResponse(250, EnhancedCode{2, 0, 0}, "I have sucessfully done nothing") case "RSET": // Reset session c.reset() - c.WriteResponse(250, "Session reset") + c.WriteResponse(250, EnhancedCode{2, 0, 0}, "Session reset") case "DATA": c.handleData(arg) case "QUIT": - c.WriteResponse(221, "Goodnight and good luck") + c.WriteResponse(221, EnhancedCode{2, 0, 0}, "Goodnight and good luck") c.Close() case "AUTH": if c.server.AuthDisabled { @@ -179,16 +179,16 @@ func (c *Conn) handleGreet(enhanced bool, arg string) { if !enhanced { domain, err := parseHelloArgument(arg) if err != nil { - c.WriteResponse(501, "Domain/address argument required for HELO") + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for HELO") return } c.helo = domain - c.WriteResponse(250, fmt.Sprintf("Hello %s", domain)) + c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain)) } else { domain, err := parseHelloArgument(arg) if err != nil { - c.WriteResponse(501, "Domain/address argument required for EHLO") + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Domain/address argument required for EHLO") return } @@ -213,14 +213,14 @@ func (c *Conn) handleGreet(enhanced bool, arg string) { args := []string{"Hello " + domain} args = append(args, caps...) - c.WriteResponse(250, args...) + c.WriteResponse(250, NoEnhancedCode, args...) } } // READY state -> waiting for MAIL func (c *Conn) handleMail(arg string) { if c.helo == "" { - c.WriteResponse(502, "Please introduce yourself first.") + c.WriteResponse(502, EnhancedCode{2, 5, 1}, "Please introduce yourself first.") return } @@ -228,7 +228,11 @@ func (c *Conn) handleMail(arg string) { state := c.State() session, err := c.server.Backend.AnonymousLogin(&state) if err != nil { - c.WriteResponse(502, err.Error()) + if smtpErr, ok := err.(*SMTPError); ok { + c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) + } else { + c.WriteResponse(502, EnhancedCode{5, 7, 0}, err.Error()) + } return } @@ -236,19 +240,19 @@ func (c *Conn) handleMail(arg string) { } if len(arg) < 6 || strings.ToUpper(arg[0:5]) != "FROM:" { - c.WriteResponse(501, "Was expecting MAIL arg syntax of FROM:
") + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } fromArgs := strings.Split(strings.Trim(arg[5:], " "), " ") if c.server.Strict { if !strings.HasPrefix(fromArgs[0], "<") || !strings.HasSuffix(fromArgs[0], ">") { - c.WriteResponse(501, "Was expecting MAIL arg syntax of FROM:
") + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } } from := strings.Trim(fromArgs[0], "<> ") if from == "" { - c.WriteResponse(501, "Was expecting MAIL arg syntax of FROM:
") + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") return } @@ -257,19 +261,19 @@ func (c *Conn) handleMail(arg string) { if len(fromArgs) > 1 { args, err := parseArgs(fromArgs[1:]) if err != nil { - c.WriteResponse(501, "Unable to parse MAIL ESMTP parameters") + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse MAIL ESMTP parameters") return } if args["SIZE"] != "" { size, err := strconv.ParseInt(args["SIZE"], 10, 32) if err != nil { - c.WriteResponse(501, "Unable to parse SIZE as an integer") + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse SIZE as an integer") return } if c.server.MaxMessageBytes > 0 && int(size) > c.server.MaxMessageBytes { - c.WriteResponse(552, "Max message size exceeded") + c.WriteResponse(552, EnhancedCode{5, 3, 4}, "Max message size exceeded") return } } @@ -277,26 +281,26 @@ func (c *Conn) handleMail(arg string) { if err := c.Session().Mail(from); err != nil { if smtpErr, ok := err.(*SMTPError); ok { - c.WriteResponse(smtpErr.Code, smtpErr.Message) + c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) return } - c.WriteResponse(451, err.Error()) + c.WriteResponse(451, EnhancedCode{4, 0, 0}, err.Error()) return } - c.WriteResponse(250, fmt.Sprintf("Roger, accepting mail from <%v>", from)) + c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Roger, accepting mail from <%v>", from)) c.fromReceived = true } // MAIL state -> waiting for RCPTs followed by DATA func (c *Conn) handleRcpt(arg string) { if !c.fromReceived { - c.WriteResponse(502, "Missing MAIL FROM command.") + c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Missing MAIL FROM command.") return } if (len(arg) < 4) || (strings.ToUpper(arg[0:3]) != "TO:") { - c.WriteResponse(501, "Was expecting RCPT arg syntax of TO:
") + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:
") return } @@ -304,30 +308,30 @@ func (c *Conn) handleRcpt(arg string) { recipient := strings.Trim(arg[3:], "<> ") if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients { - c.WriteResponse(552, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients)) + c.WriteResponse(552, EnhancedCode{5, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients)) return } if err := c.Session().Rcpt(recipient); err != nil { if smtpErr, ok := err.(*SMTPError); ok { - c.WriteResponse(smtpErr.Code, smtpErr.Message) + c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) return } - c.WriteResponse(451, err.Error()) + c.WriteResponse(451, EnhancedCode{4, 0, 0}, err.Error()) return } c.recipients = append(c.recipients, recipient) - c.WriteResponse(250, fmt.Sprintf("I'll make sure <%v> gets this", recipient)) + c.WriteResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("I'll make sure <%v> gets this", recipient)) } func (c *Conn) handleAuth(arg string) { if c.helo == "" { - c.WriteResponse(502, "Please introduce yourself first.") + c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Please introduce yourself first.") return } if arg == "" { - c.WriteResponse(502, "Missing parameter") + c.WriteResponse(502, EnhancedCode{5, 5, 4}, "Missing parameter") return } @@ -346,7 +350,7 @@ func (c *Conn) handleAuth(arg string) { newSasl, ok := c.server.auths[mechanism] if !ok { - c.WriteResponse(504, "Unsupported authentication mechanism") + c.WriteResponse(504, EnhancedCode{5, 7, 4}, "Unsupported authentication mechanism") return } @@ -356,7 +360,7 @@ func (c *Conn) handleAuth(arg string) { for { challenge, done, err := sasl.Next(response) if err != nil { - c.WriteResponse(454, err.Error()) + c.WriteResponse(454, EnhancedCode{4, 7, 0}, err.Error()) return } @@ -368,7 +372,7 @@ func (c *Conn) handleAuth(arg string) { if len(challenge) > 0 { encoded = base64.StdEncoding.EncodeToString(challenge) } - c.WriteResponse(334, encoded) + c.WriteResponse(334, NoEnhancedCode, encoded) encoded, err = c.ReadLine() if err != nil { @@ -377,35 +381,35 @@ func (c *Conn) handleAuth(arg string) { response, err = base64.StdEncoding.DecodeString(encoded) if err != nil { - c.WriteResponse(454, "Invalid base64 data") + c.WriteResponse(454, EnhancedCode{4, 7, 0}, "Invalid base64 data") return } } if c.Session() != nil { - c.WriteResponse(235, "Authentication succeeded") + c.WriteResponse(235, EnhancedCode{2, 0, 0}, "Authentication succeeded") } } func (c *Conn) handleStartTLS() { if _, isTLS := c.TLSConnectionState(); isTLS { - c.WriteResponse(502, "Already running in TLS") + c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Already running in TLS") return } if c.server.TLSConfig == nil { - c.WriteResponse(502, "TLS not supported") + c.WriteResponse(502, EnhancedCode{5, 5, 1}, "TLS not supported") return } - c.WriteResponse(220, "Ready to start TLS") + c.WriteResponse(220, EnhancedCode{2, 0, 0}, "Ready to start TLS") // Upgrade to TLS var tlsConn *tls.Conn tlsConn = tls.Server(c.conn, c.server.TLSConfig) if err := tlsConn.Handshake(); err != nil { - c.WriteResponse(550, "Handshake error") + c.WriteResponse(550, EnhancedCode{5, 0, 0}, "Handshake error") } c.conn = tlsConn @@ -418,21 +422,22 @@ func (c *Conn) handleStartTLS() { // DATA func (c *Conn) handleData(arg string) { if arg != "" { - c.WriteResponse(501, "DATA command should not have any arguments") + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "DATA command should not have any arguments") return } if !c.fromReceived || len(c.recipients) == 0 { - c.WriteResponse(502, "Missing RCPT TO command.") + c.WriteResponse(502, EnhancedCode{5, 5, 1}, "Missing RCPT TO command.") return } // We have recipients, go to accept data - c.WriteResponse(354, "Go ahead. End your data with .") + c.WriteResponse(354, EnhancedCode{2, 0, 0}, "Go ahead. End your data with .") var ( - code int - msg string + code int + enhancedCode EnhancedCode + msg string ) r := newDataReader(c) err := c.Session().Data(r) @@ -440,47 +445,66 @@ func (c *Conn) handleData(arg string) { if err != nil { if smtperr, ok := err.(*SMTPError); ok { code = smtperr.Code + enhancedCode = smtperr.EnhancedCode msg = smtperr.Message } else { code = 554 + enhancedCode = EnhancedCode{5, 0, 0} msg = "Error: transaction failed, blame it on the weather: " + err.Error() } } else { code = 250 + enhancedCode = EnhancedCode{2, 0, 0} msg = "OK: queued" } if c.server.LMTP { // TODO: support per-recipient responses for _, rcpt := range c.recipients { - c.WriteResponse(code, "<"+rcpt+"> "+msg) + c.WriteResponse(code, enhancedCode, "<"+rcpt+"> "+msg) } } else { - c.WriteResponse(code, msg) + c.WriteResponse(code, enhancedCode, msg) } c.resetMessage() } func (c *Conn) Reject() { - c.WriteResponse(421, "Too busy. Try again later.") + c.WriteResponse(421, EnhancedCode{4, 4, 5}, "Too busy. Try again later.") c.Close() } func (c *Conn) greet() { - c.WriteResponse(220, fmt.Sprintf("%v ESMTP Service Ready", c.server.Domain)) + c.WriteResponse(220, NoEnhancedCode, fmt.Sprintf("%v ESMTP Service Ready", c.server.Domain)) } -func (c *Conn) WriteResponse(code int, text ...string) { +func (c *Conn) WriteResponse(code int, enhCode EnhancedCode, text ...string) { // TODO: error handling if c.server.WriteTimeout != 0 { c.conn.SetWriteDeadline(time.Now().Add(c.server.WriteTimeout)) } + // All responses must include an enhanced code, if it is missing - use + // a generic code X.0.0. + if enhCode == EnhancedCodeNotSet { + cat := code / 100 + switch cat { + case 2, 4, 5: + enhCode = EnhancedCode{cat, 0, 0} + default: + enhCode = NoEnhancedCode + } + } + for i := 0; i < len(text)-1; i++ { c.text.PrintfLine("%v-%v", code, text[i]) } - c.text.PrintfLine("%v %v", code, text[len(text)-1]) + if enhCode == NoEnhancedCode { + c.text.PrintfLine("%v %v", code, text[len(text)-1]) + } else { + c.text.PrintfLine("%v %v.%v.%v %v", code, enhCode[0], enhCode[1], enhCode[2], text[len(text)-1]) + } } // Reads a line of input diff --git a/data.go b/data.go index ae09f669..d72be0c0 100644 --- a/data.go +++ b/data.go @@ -4,19 +4,36 @@ import ( "io" ) +type EnhancedCode [3]int + // SMTPError specifies the error code and message that needs to be returned to the client type SMTPError struct { - Code int - Message string + Code int + EnhancedCode EnhancedCode + Message string } +// NoEnhancedCode is used to indicate that enhanced error code should not be +// included in response. +// +// Note that RFC 2034 requires an enhanced code to be included in all 2xx, 4xx +// and 5xx responses. This constant is exported for use by extensions, you +// should probably use EnhancedCodeNotSet instead. +var NoEnhancedCode = EnhancedCode{-1, -1, -1} + +// EnhancedCodeNotSet is a nil value of EnhancedCode field in SMTPError, used +// to indicate that backend failed to provide enhanced status code. X.0.0 will +// be used (X is derived from error code). +var EnhancedCodeNotSet = EnhancedCode{0, 0, 0} + func (err *SMTPError) Error() string { return err.Message } var ErrDataTooLarge = &SMTPError{ - Code: 552, - Message: "Maximum message size exceeded", + Code: 552, + EnhancedCode: EnhancedCode{5, 3, 4}, + Message: "Maximum message size exceeded", } type dataReader struct { diff --git a/server.go b/server.go index 2d4b4037..05441b61 100755 --- a/server.go +++ b/server.go @@ -54,7 +54,7 @@ type Server struct { func NewServer(be Backend) *Server { return &Server{ Backend: be, - caps: []string{"PIPELINING", "8BITMIME"}, + caps: []string{"PIPELINING", "8BITMIME", "ENHANCEDSTATUSCODES"}, auths: map[string]SaslServerFactory{ sasl.Plain: func(conn *Conn) sasl.Server { return sasl.NewPlainServer(func(identity, username, password string) error { @@ -113,7 +113,7 @@ func (s *Server) handleConn(c *Conn) error { cmd, arg, err := parseCmd(line) if err != nil { c.nbrErrors++ - c.WriteResponse(501, "Bad command") + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Bad command") continue } @@ -124,11 +124,11 @@ func (s *Server) handleConn(c *Conn) error { } if neterr, ok := err.(net.Error); ok && neterr.Timeout() { - c.WriteResponse(221, "Idle timeout, bye bye") + c.WriteResponse(221, EnhancedCode{2, 4, 2}, "Idle timeout, bye bye") return nil } - c.WriteResponse(221, "Connection error, sorry") + c.WriteResponse(221, EnhancedCode{2, 4, 0}, "Connection error, sorry") return err } } diff --git a/server_test.go b/server_test.go index 46fd8525..f446c681 100644 --- a/server_test.go +++ b/server_test.go @@ -345,7 +345,7 @@ func TestServer_authDisabled(t *testing.T) { io.WriteString(c, "AUTH PLAIN\r\n") scanner.Scan() - if scanner.Text() != "500 Syntax error, AUTH command unrecognized" { + if scanner.Text() != "500 5.5.2 Syntax error, AUTH command unrecognized" { t.Fatal("Invalid AUTH response with auth disabled:", scanner.Text()) } } @@ -436,7 +436,7 @@ func TestServer_anonymousUserError(t *testing.T) { io.WriteString(c, "MAIL FROM:\r\n") scanner.Scan() - if scanner.Text() != "502 Please authenticate first" { + if scanner.Text() != "502 5.7.0 Please authenticate first" { t.Fatal("Backend refused anonymous mail but client was permitted:", scanner.Text()) } } diff --git a/smtp.go b/smtp.go index 7ed1f5eb..13556346 100644 --- a/smtp.go +++ b/smtp.go @@ -2,9 +2,10 @@ // // It also implements the following extensions: // -// 8BITMIME RFC 1652 -// AUTH RFC 2554 -// STARTTLS RFC 3207 +// 8BITMIME RFC 1652 +// AUTH RFC 2554 +// STARTTLS RFC 3207 +// ENHANCEDSTATUSCODES RFC 2034 // // LMTP (RFC 2033) is also supported. //