author | Alberto Bertogli
<albertito@blitiri.com.ar> 2019-01-09 23:43:11 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2019-01-10 15:44:25 UTC |
parent | 78937aca9325e99b2da19b669497610dd693309d |
internal/smtpsrv/conn.go | +55 | -57 |
internal/smtpsrv/server_test.go | +2 | -2 |
test/t-12-minor_dialogs/auth_multi_dialog.cmy | +4 | -4 |
test/t-12-minor_dialogs/auth_not_tls.cmy | +1 | -1 |
test/t-12-minor_dialogs/auth_too_many_failures.cmy | +5 | -5 |
test/t-12-minor_dialogs/bad_data.cmy | +3 | -3 |
test/t-12-minor_dialogs/bad_mail_from.cmy | +3 | -3 |
test/t-12-minor_dialogs/bad_rcpt_to.cmy | +4 | -4 |
test/t-12-minor_dialogs/unknown_command.cmy | +1 | -2 |
diff --git a/internal/smtpsrv/conn.go b/internal/smtpsrv/conn.go index 31c205b..479fb59 100644 --- a/internal/smtpsrv/conn.go +++ b/internal/smtpsrv/conn.go @@ -232,14 +232,14 @@ loop: case "AUTH": code, msg = c.AUTH(params) case "QUIT": - c.writeResponse(221, "Be seeing you...") + c.writeResponse(221, "2.0.0 Be seeing you...") break loop default: // Sanitize it a bit to avoid filling the logs and events with // noisy data. Keep the first 6 bytes for debugging. cmd = fmt.Sprintf("unknown<%.6s>", cmd) code = 500 - msg = "unknown command" + msg = "5.5.1 Unknown command" } commandCount.Add(cmd, 1) @@ -254,7 +254,7 @@ loop: if errCount > 10 { // https://tools.ietf.org/html/rfc5321#section-4.3.2 c.tr.Errorf("too many errors, breaking connection") - c.writeResponse(421, "too many errors, bye") + c.writeResponse(421, "4.5.0 Too many errors, bye") break } } @@ -302,6 +302,7 @@ func (c *Conn) EHLO(params string) (code int, msg string) { fmt.Fprintf(buf, "8BITMIME\n") fmt.Fprintf(buf, "PIPELINING\n") fmt.Fprintf(buf, "SMTPUTF8\n") + fmt.Fprintf(buf, "ENHANCEDSTATUSCODES\n") fmt.Fprintf(buf, "SIZE %d\n", c.maxDataSize) if c.onTLS { fmt.Fprintf(buf, "AUTH PLAIN\n") @@ -314,7 +315,7 @@ func (c *Conn) EHLO(params string) (code int, msg string) { // HELP SMTP command handler. func (c *Conn) HELP(params string) (code int, msg string) { - return 214, "hoy por ti, mañana por mi" + return 214, "2.0.0 Hoy por ti, mañana por mi" } // RSET SMTP command handler. @@ -327,28 +328,24 @@ func (c *Conn) RSET(params string) (code int, msg string) { "Your mind releases itself from mundane concerns.", "As your mind turns inward on itself, you forget everything else.", } - return 250, msgs[rand.Int()%len(msgs)] + return 250, "2.0.0 " + msgs[rand.Int()%len(msgs)] } // VRFY SMTP command handler. func (c *Conn) VRFY(params string) (code int, msg string) { - // 252 can be used for cases like ours, when we don't really want to - // confirm or deny anything. - // See https://tools.ietf.org/html/rfc2821#section-3.5.3. - return 252, "You have a strange feeling for a moment, then it passes." + // We intentionally don't implement this command. + return 502, "5.5.1 You have a strange feeling for a moment, then it passes." } // EXPN SMTP command handler. func (c *Conn) EXPN(params string) (code int, msg string) { - // 252 can be used for cases like ours, when we don't really want to - // confirm or deny anything. - // See https://tools.ietf.org/html/rfc2821#section-3.5.3. - return 252, "You feel disoriented for a moment." + // We intentionally don't implement this command. + return 502, "5.5.1 You feel disoriented for a moment." } // NOOP SMTP command handler. func (c *Conn) NOOP(params string) (code int, msg string) { - return 250, "You hear a faint typing noise." + return 250, "2.0.0 You hear a faint typing noise." } // MAIL SMTP command handler. @@ -357,16 +354,16 @@ func (c *Conn) MAIL(params string) (code int, msg string) { // options such as "BODY=8BITMIME" (which we ignore). // Check that it begins with "FROM:" first, it's mandatory. if !strings.HasPrefix(strings.ToLower(params), "from:") { - return 500, "unknown command" + return 500, "5.5.2 Unknown command" } if c.mode.IsSubmission && !c.completedAuth { - return 550, "mail to submission port must be authenticated" + return 550, "5.7.9 Mail to submission port must be authenticated" } rawAddr := "" _, err := fmt.Sscanf(params[5:], "%s ", &rawAddr) if err != nil { - return 500, "malformed command - " + err.Error() + return 500, "5.5.4 Malformed command: " + err.Error() } // Note some servers check (and fail) if we had a previous MAIL command, @@ -383,17 +380,17 @@ func (c *Conn) MAIL(params string) (code int, msg string) { } else { e, err := mail.ParseAddress(rawAddr) if err != nil || e.Address == "" { - return 501, "malformed address" + return 501, "5.1.7 Sender address malformed" } addr = e.Address if !strings.Contains(addr, "@") { - return 501, "sender address must contain a domain" + return 501, "5.1.8 Sender address must contain a domain" } // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.3 if len(addr) > 256 { - return 501, "address too long" + return 501, "5.1.7 Sender address too long" } // SPF check - https://tools.ietf.org/html/rfc7208#section-2.4 @@ -405,25 +402,25 @@ func (c *Conn) MAIL(params string) (code int, msg string) { maillog.Rejected(c.conn.RemoteAddr(), addr, nil, fmt.Sprintf("failed SPF: %v", c.spfError)) return 550, fmt.Sprintf( - "SPF check failed: %v", c.spfError) + "5.7.23 SPF check failed: %v", c.spfError) } if !c.secLevelCheck(addr) { maillog.Rejected(c.conn.RemoteAddr(), addr, nil, "security level check failed") - return 550, "security level check failed" + return 550, "5.7.3 Security level check failed" } addr, err = normalize.DomainToUnicode(addr) if err != nil { maillog.Rejected(c.conn.RemoteAddr(), addr, nil, fmt.Sprintf("malformed address: %v", err)) - return 501, "malformed address (IDNA conversion failed)" + return 501, "5.1.8 Malformed sender domain (IDNA conversion failed)" } } c.mailFrom = addr - return 250, "You feel like you are being watched" + return 250, "2.1.5 You feel like you are being watched" } // checkSPF for the given address, based on the current connection. @@ -487,45 +484,45 @@ func (c *Conn) RCPT(params string) (code int, msg string) { // such as "NOTIFY=SUCCESS,DELAY" (which we ignore). // Check that it begins with "TO:" first, it's mandatory. if !strings.HasPrefix(strings.ToLower(params), "to:") { - return 500, "unknown command" + return 500, "5.5.2 Unknown command" } if c.mailFrom == "" { - return 503, "sender not yet given" + return 503, "5.5.1 Sender not yet given" } rawAddr := "" _, err := fmt.Sscanf(params[3:], "%s ", &rawAddr) if err != nil { - return 500, "malformed command - " + err.Error() + return 500, "5.5.4 Malformed command: " + err.Error() } // RFC says 100 is the minimum limit for this, but it seems excessive. // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.8 if len(c.rcptTo) > 100 { - return 452, "too many recipients" + return 452, "4.5.3 Too many recipients" } e, err := mail.ParseAddress(rawAddr) if err != nil || e.Address == "" { - return 501, "malformed address" + return 501, "5.1.3 Malformed destination address" } addr, err := normalize.DomainToUnicode(e.Address) if err != nil { - return 501, "malformed address (IDNA conversion failed)" + return 501, "5.1.2 Malformed destination domain (IDNA conversion failed)" } // https://tools.ietf.org/html/rfc5321#section-4.5.3.1.3 if len(addr) > 256 { - return 501, "address too long" + return 501, "5.1.3 Destination address too long" } localDst := envelope.DomainIn(addr, c.localDomains) if !localDst && !c.completedAuth { maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, []string{addr}, "relay not allowed") - return 503, "relay not allowed" + return 503, "5.7.1 Relay not allowed" } if localDst { @@ -533,36 +530,36 @@ func (c *Conn) RCPT(params string) (code int, msg string) { if err != nil { maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, []string{addr}, fmt.Sprintf("invalid address: %v", err)) - return 550, "recipient invalid, please check the address for typos" + return 550, "5.1.3 Destination address is invalid" } if !c.userExists(addr) { maillog.Rejected(c.conn.RemoteAddr(), c.mailFrom, []string{addr}, "local user does not exist") - return 550, "recipient unknown, please check the address for typos" + return 550, "5.1.1 Destination address is unknown (user does not exist)" } } c.rcptTo = append(c.rcptTo, addr) - return 250, "You have an eerie feeling..." + return 250, "2.1.5 You have an eerie feeling..." } // DATA SMTP command handler. func (c *Conn) DATA(params string) (code int, msg string) { if c.ehloAddress == "" { - return 503, "Invisible customers are not welcome!" + return 503, "5.5.1 Invisible customers are not welcome!" } if c.mailFrom == "" { - return 503, "sender not yet given" + return 503, "5.5.1 Sender not yet given" } if len(c.rcptTo) == 0 { - return 503, "need an address to send to" + return 503, "5.5.1 Need an address to send to" } // We're going ahead. err := c.writeResponse(354, "You suddenly realize it is unnaturally quiet") if err != nil { - return 554, fmt.Sprintf("error writing DATA response: %v", err) + return 554, fmt.Sprintf("5.4.0 Error writing DATA response: %v", err) } c.tr.Debugf("<- 354 You experience a strange sense of peace") @@ -579,7 +576,7 @@ func (c *Conn) DATA(params string) (code int, msg string) { dotr := io.LimitReader(c.tc.DotReader(), c.maxDataSize) c.data, err = ioutil.ReadAll(dotr) if err != nil { - return 554, fmt.Sprintf("error reading DATA: %v", err) + return 554, fmt.Sprintf("5.4.0 Error reading DATA: %v", err) } c.tr.Debugf("-> ... %d bytes of data", len(c.data)) @@ -605,7 +602,7 @@ func (c *Conn) DATA(params string) (code int, msg string) { // individual deliveries fail, we report via email. msgID, err := c.queue.Put(c.mailFrom, c.rcptTo, c.data) if err != nil { - return 554, fmt.Sprintf("Failed to enqueue message: %v", err) + return 554, fmt.Sprintf("5.3.0 Failed to queue message: %v", err) } c.tr.Printf("Queued from %s to %s - %s", c.mailFrom, c.rcptTo, msgID) @@ -622,7 +619,7 @@ func (c *Conn) DATA(params string) (code int, msg string) { "In return to thy service, I grant thee the gift of Immortality!", "You ascend to the status of Demigod(dess)...", } - return 250, msgs[rand.Int()%len(msgs)] + return 250, "2.0.0 " + msgs[rand.Int()%len(msgs)] } func (c *Conn) addReceivedHeader() { @@ -715,7 +712,7 @@ func addrLiteral(addr net.Addr) string { func checkData(data []byte) error { msg, err := mail.ReadMessage(bytes.NewBuffer(data)) if err != nil { - return fmt.Errorf("error parsing message: %v", err) + return fmt.Errorf("5.6.0 Error parsing message: %v", err) } // This serves as a basic form of loop prevention. It's not infallible but @@ -723,7 +720,7 @@ func checkData(data []byte) error { // https://tools.ietf.org/html/rfc5321#section-6.3 if len(msg.Header["Received"]) > *maxReceivedHeaders { loopsDetected.Add(1) - return fmt.Errorf("email passed through more than %d MTAs, looping?", + return fmt.Errorf("5.4.6 Loop detected (%d hops)", *maxReceivedHeaders) } @@ -858,12 +855,12 @@ func boolToStr(b bool) string { // STARTTLS SMTP command handler. func (c *Conn) STARTTLS(params string) (code int, msg string) { if c.onTLS { - return 503, "You are already wearing that!" + return 503, "5.5.1 You are already wearing that!" } - err := c.writeResponse(220, "You experience a strange sense of peace") + err := c.writeResponse(220, "2.0.0 You experience a strange sense of peace") if err != nil { - return 554, fmt.Sprintf("error writing STARTTLS response: %v", err) + return 554, fmt.Sprintf("5.4.0 Error writing STARTTLS response: %v", err) } c.tr.Debugf("<- 220 You experience a strange sense of peace") @@ -871,7 +868,7 @@ func (c *Conn) STARTTLS(params string) (code int, msg string) { server := tls.Server(c.conn, c.tlsConfig) err = server.Handshake() if err != nil { - return 554, fmt.Sprintf("error in TLS handshake: %v", err) + return 554, fmt.Sprintf("5.5.0 Error in TLS handshake: %v", err) } c.tr.Debugf("<> ... jump to TLS was successful") @@ -903,18 +900,18 @@ func (c *Conn) STARTTLS(params string) (code int, msg string) { // AUTH SMTP command handler. func (c *Conn) AUTH(params string) (code int, msg string) { if !c.onTLS { - return 503, "You feel vulnerable" + return 503, "5.7.10 You feel vulnerable" } if c.completedAuth { // After a successful AUTH command completes, a server MUST reject // any further AUTH commands with a 503 reply. // https://tools.ietf.org/html/rfc4954#section-4 - return 503, "You are already wearing that!" + return 503, "5.5.1 You are already wearing that!" } if c.authAttempts > 3 { - return 503, "Too many attempts - go away" + return 503, "5.7.8 Too many attempts, go away" } c.authAttempts++ @@ -926,7 +923,7 @@ func (c *Conn) AUTH(params string) (code int, msg string) { sp := strings.SplitN(params, " ", 2) if len(sp) < 1 || sp[0] != "PLAIN" { // As we only offer plain, this should not really happen. - return 534, "Asmodeus demands 534 zorkmids for safe passage" + return 534, "5.7.9 Asmodeus demands 534 zorkmids for safe passage" } // Note we use more "serious" error messages from now own, as these may @@ -943,18 +940,19 @@ func (c *Conn) AUTH(params string) (code int, msg string) { // https://tools.ietf.org/html/rfc4954#section-4 err := c.writeResponse(334, "") if err != nil { - return 554, fmt.Sprintf("error writing AUTH 334: %v", err) + return 554, fmt.Sprintf("5.4.0 Error writing AUTH 334: %v", err) } response, err = c.readLine() if err != nil { - return 554, fmt.Sprintf("error reading AUTH response: %v", err) + return 554, fmt.Sprintf("5.4.0 Error reading AUTH response: %v", err) } } user, domain, passwd, err := auth.DecodeResponse(response) if err != nil { - return 535, fmt.Sprintf("error decoding AUTH response: %v", err) + // https://tools.ietf.org/html/rfc4954#section-4 + return 501, fmt.Sprintf("5.5.2 Error decoding AUTH response: %v", err) } authOk, err := c.authr.Authenticate(user, domain, passwd) @@ -966,11 +964,11 @@ func (c *Conn) AUTH(params string) (code int, msg string) { c.authDomain = domain c.completedAuth = true maillog.Auth(c.conn.RemoteAddr(), user+"@"+domain, true) - return 235, "" + return 235, "2.7.0 Authentication successful" } maillog.Auth(c.conn.RemoteAddr(), user+"@"+domain, false) - return 535, "Incorrect user or password" + return 535, "5.7.8 Incorrect user or password" } func (c *Conn) resetEnvelope() { diff --git a/internal/smtpsrv/server_test.go b/internal/smtpsrv/server_test.go index ff8598b..c1109e3 100644 --- a/internal/smtpsrv/server_test.go +++ b/internal/smtpsrv/server_test.go @@ -272,8 +272,8 @@ func TestSimpleCommands(t *testing.T) { defer c.Close() simpleCmd(t, c, "HELP", 214) simpleCmd(t, c, "NOOP", 250) - simpleCmd(t, c, "VRFY", 252) - simpleCmd(t, c, "EXPN", 252) + simpleCmd(t, c, "VRFY", 502) + simpleCmd(t, c, "EXPN", 502) } func TestReset(t *testing.T) { diff --git a/test/t-12-minor_dialogs/auth_multi_dialog.cmy b/test/t-12-minor_dialogs/auth_multi_dialog.cmy index 1e60f4a..53dbee0 100644 --- a/test/t-12-minor_dialogs/auth_multi_dialog.cmy +++ b/test/t-12-minor_dialogs/auth_multi_dialog.cmy @@ -11,17 +11,17 @@ c <~ 534 c -> AUTH PLAIN c <~ 334 c -> dXNlckB0ZXN0c2VydmVyAHlalala== -c <~ 535 error decoding AUTH response +c <~ 501 5.5.2 Error decoding AUTH response c -> AUTH PLAIN c <~ 334 c -> dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgB3cm9uZ3Bhc3N3b3Jk -c <~ 535 Incorrect user or password +c <~ 535 5.7.8 Incorrect user or password c -> AUTH PLAIN c <~ 334 c -> dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgBzZWNyZXRwYXNzd29yZA== -c <~ 235 +c <~ 235 2.7.0 Authentication successful c -> AUTH PLAIN -c <~ 503 You are already wearing that! +c <~ 503 5.5.1 You are already wearing that! diff --git a/test/t-12-minor_dialogs/auth_not_tls.cmy b/test/t-12-minor_dialogs/auth_not_tls.cmy index 4a16c0d..500a4f9 100644 --- a/test/t-12-minor_dialogs/auth_not_tls.cmy +++ b/test/t-12-minor_dialogs/auth_not_tls.cmy @@ -5,4 +5,4 @@ c <~ 220 c -> EHLO localhost c <... 250 HELP c -> AUTH PLAIN -c <- 503 You feel vulnerable +c <- 503 5.7.10 You feel vulnerable diff --git a/test/t-12-minor_dialogs/auth_too_many_failures.cmy b/test/t-12-minor_dialogs/auth_too_many_failures.cmy index 8ea8195..76d5cbe 100644 --- a/test/t-12-minor_dialogs/auth_too_many_failures.cmy +++ b/test/t-12-minor_dialogs/auth_too_many_failures.cmy @@ -6,13 +6,13 @@ c -> EHLO localhost c <... 250 HELP c -> AUTH PLAIN something -c <~ 535 +c <~ 501 c -> AUTH PLAIN something -c <~ 535 +c <~ 501 c -> AUTH PLAIN something -c <~ 535 +c <~ 501 c -> AUTH PLAIN something -c <~ 535 +c <~ 501 c -> AUTH PLAIN something -c <~ 503 Too many attempts - go away +c <~ 503 5.7.8 Too many attempts, go away diff --git a/test/t-12-minor_dialogs/bad_data.cmy b/test/t-12-minor_dialogs/bad_data.cmy index 302bc0f..6fdcac3 100644 --- a/test/t-12-minor_dialogs/bad_data.cmy +++ b/test/t-12-minor_dialogs/bad_data.cmy @@ -4,12 +4,12 @@ c tcp_connect localhost:1025 c <~ 220 c -> DATA -c <- 503 Invisible customers are not welcome! +c <- 503 5.5.1 Invisible customers are not welcome! c -> HELO localhost c <~ 250 c -> DATA -c <- 503 sender not yet given +c <- 503 5.5.1 Sender not yet given c -> MAIL FROM:<a@b> c <~ 250 @@ -25,7 +25,7 @@ c -> Muahahahaha c -> c -> c -> . -c <~ 554 error parsing message +c <~ 554 5.6.0 Error parsing message c -> QUIT c <~ 221 diff --git a/test/t-12-minor_dialogs/bad_mail_from.cmy b/test/t-12-minor_dialogs/bad_mail_from.cmy index 4853895..fbb430e 100644 --- a/test/t-12-minor_dialogs/bad_mail_from.cmy +++ b/test/t-12-minor_dialogs/bad_mail_from.cmy @@ -5,7 +5,7 @@ c <~ 220 c -> HELO localhost c <~ 250 c -> MAIL LALA: <> -c <- 500 unknown command +c <- 500 5.5.2 Unknown command c -> MAIL FROM: c <~ 500 @@ -14,7 +14,7 @@ c -> MAIL FROM:<pepe> c <~ 501 c -> MAIL FROM:<a@xn---> -c <- 501 malformed address (IDNA conversion failed) +c <- 501 5.1.8 Malformed sender domain (IDNA conversion failed) c -> MAIL FROM:<aaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaX@bbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbX> -c <- 501 address too long +c <- 501 5.1.7 Sender address too long diff --git a/test/t-12-minor_dialogs/bad_rcpt_to.cmy b/test/t-12-minor_dialogs/bad_rcpt_to.cmy index 0e9bd50..e31ee89 100644 --- a/test/t-12-minor_dialogs/bad_rcpt_to.cmy +++ b/test/t-12-minor_dialogs/bad_rcpt_to.cmy @@ -8,7 +8,7 @@ c -> MAIL FROM:<test@testy.com> c <~ 250 c -> RCPT LALA: <> -c <- 500 unknown command +c <- 500 5.5.2 Unknown command c -> RCPT TO: c <~ 500 @@ -17,10 +17,10 @@ c -> RCPT TO:<pepe> c <~ 501 c -> RCPT TO:<a@xn---> -c <- 501 malformed address (IDNA conversion failed) +c <- 501 5.1.2 Malformed destination domain (IDNA conversion failed) c -> RCPT TO:<henryⅣ@testserver> -c <- 550 recipient invalid, please check the address for typos +c <- 550 5.1.3 Destination address is invalid c -> RCPT TO:<aaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaX@bbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbX> -c <- 501 address too long +c <- 501 5.1.3 Destination address too long diff --git a/test/t-12-minor_dialogs/unknown_command.cmy b/test/t-12-minor_dialogs/unknown_command.cmy index c8673cb..bfd76a1 100644 --- a/test/t-12-minor_dialogs/unknown_command.cmy +++ b/test/t-12-minor_dialogs/unknown_command.cmy @@ -5,5 +5,4 @@ c <~ 220 c -> EHLO localhost c <... 250 HELP c -> WHATISTHIS -c <- 500 unknown command - +c <- 500 5.5.1 Unknown command