diff --git a/internal/smtpd/smtpd.go b/internal/smtpd/smtpd.go index 9b53ce3..9e1cf69 100644 --- a/internal/smtpd/smtpd.go +++ b/internal/smtpd/smtpd.go @@ -422,9 +422,13 @@ loop: break } - match := extractAndValidateAddress(mailFromRE, args) + match, err := extractAndValidateAddress(mailFromRE, args) if match == nil { - s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)") + if err != nil { + s.writef("%s", err.Error()) + } else { + s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid FROM parameter)") + } } else { // Mailpit Chaos if fail, code := chaos.Config.Sender.Trigger(); fail { @@ -478,9 +482,13 @@ loop: break } - match := extractAndValidateAddress(rcptToRE, args) + match, err := extractAndValidateAddress(rcptToRE, args) if match == nil { - s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)") + if err != nil { + s.writef("%s", err.Error()) + } else { + s.writef("501 5.5.4 Syntax error in parameters or arguments (invalid TO parameter)") + } } else { // Mailpit Chaos if fail, code := chaos.Config.Recipient.Trigger(); fail { @@ -1017,31 +1025,33 @@ func (s *session) handleAuthCramMD5() (bool, error) { } // Extract and validate email address from a regex match. -// This ensures that only RFC 5322 email addresses are accepted (if set). -func extractAndValidateAddress(re *regexp.Regexp, args string) []string { +// This ensures that only RFC 5322 compliant email addresses are accepted (if set). +func extractAndValidateAddress(re *regexp.Regexp, args string) ([]string, error) { match := re.FindStringSubmatch(args) - if match == nil || strings.Contains(match[1], " ") { - return nil + if match == nil { + return nil, nil + } + + if strings.Contains(match[1], " ") { + return nil, errors.New("553 5.1.3 The address is not a valid RFC 5321 address") } // first argument will be the email address, validate it if not empty if match[1] != "" { a, err := mail.ParseAddress(match[1]) if err != nil { - return nil - } - - parts := strings.SplitN(a.Address, "@", 2) - - if len(parts) != 2 { - return nil + return nil, errors.New("553 5.1.3 The address is not a valid RFC 5321 address") } // https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1 - if len(parts[0]) > 64 || len(parts[1]) > 255 || len(a.Address) > 256 { - return nil + // RFC states that the local part of an email address SHOULD not exceed 64 characters + // and the domain part SHOULD not exceed 255 characters, however as per https://github.com/axllent/mailpit/issues/620 + // it appears that investigated mail servers do not actually implement this limit, but rather enforce + // a much larger limit (ie: 1024 characters). + if len(a.Address) > 1024 { + return nil, errors.New("500 The address is too long") } } - return match + return match, nil } diff --git a/internal/smtpd/smtpd_test.go b/internal/smtpd/smtpd_test.go index f4bdd4a..8c6007c 100644 --- a/internal/smtpd/smtpd_test.go +++ b/internal/smtpd/smtpd_test.go @@ -106,15 +106,14 @@ func TestCmdEHLO(t *testing.T) { cmdCode(t, conn, "RCPT TO:", "250") // test invalid addresses & header injection - cmdCode(t, conn, "RCPT TO: ", "501") // too long - cmdCode(t, conn, "RCPT TO: ", "501") - cmdCode(t, conn, "RCPT TO: ", "501") - cmdCode(t, conn, "RCPT TO: ", "250") // valid - cmdCode(t, conn, "RCPT TO:", "501") - cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO: ", "500") // too long + cmdCode(t, conn, "RCPT TO:", "553") + cmdCode(t, conn, "RCPT TO: ", "553") + cmdCode(t, conn, "RCPT TO: ", "553") + cmdCode(t, conn, "RCPT TO: ", "553") cmdCode(t, conn, "RCPT TO: ", "501") - cmdCode(t, conn, "RCPT TO:", "501") - cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO:", "553") + cmdCode(t, conn, "RCPT TO: ", "553") cmdCode(t, conn, "RCPT TO: ", "501") cmdCode(t, conn, "RCPT TO: <>", "501") // empty address not allowed here @@ -145,7 +144,7 @@ func TestCmdMAIL(t *testing.T) { // MAIL with no FROM arg should return 501 syntax error cmdCode(t, conn, "MAIL", "501") - // MAIL with empty FROM arg should return 501 syntax error + // // MAIL with empty FROM arg should return 501 syntax error cmdCode(t, conn, "MAIL FROM:", "501") cmdCode(t, conn, "MAIL FROM: ", "501") cmdCode(t, conn, "MAIL FROM: ", "501") @@ -160,19 +159,18 @@ func TestCmdMAIL(t *testing.T) { cmdCode(t, conn, "MAIL FROM: ", "501") // test invalid addresses & header injection - cmdCode(t, conn, "MAIL FROM: ", "501") // too long - cmdCode(t, conn, "MAIL FROM: ", "501") - cmdCode(t, conn, "MAIL FROM: ", "501") - cmdCode(t, conn, "MAIL FROM: ", "250") // valid - cmdCode(t, conn, "MAIL FROM:", "501") - cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM: ", "500") // too long + cmdCode(t, conn, "MAIL FROM:", "553") + cmdCode(t, conn, "MAIL FROM: ", "553") cmdCode(t, conn, "MAIL FROM: ", "501") - cmdCode(t, conn, "MAIL FROM:", "501") - cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM:", "553") + cmdCode(t, conn, "MAIL FROM: ", "553") + cmdCode(t, conn, "MAIL FROM: ", "553") + cmdCode(t, conn, "MAIL FROM: ", "553") cmdCode(t, conn, "MAIL FROM: ", "501") - cmdCode(t, conn, "MAIL FROM: < sender@example.com >", "501") - cmdCode(t, conn, "MAIL FROM: < sender@example.com>", "501") - cmdCode(t, conn, "MAIL FROM: ", "501") + cmdCode(t, conn, "MAIL FROM: < sender@example.com >", "553") + cmdCode(t, conn, "MAIL FROM: < sender@example.com>", "553") + cmdCode(t, conn, "MAIL FROM: ", "553") // MAIL with valid SIZE parameter should return 250 Ok cmdCode(t, conn, "MAIL FROM: SIZE=1000", "250") @@ -241,6 +239,7 @@ func TestCmdRCPT(t *testing.T) { cmdCode(t, conn, "RCPT TO:", "501") cmdCode(t, conn, "RCPT TO: ", "501") cmdCode(t, conn, "RCPT TO: ", "501") + cmdCode(t, conn, "RCPT TO:<@route.example user@example.com>", "553") // RCPT with valid TO arg should return 250 Ok cmdCode(t, conn, "RCPT TO:", "250")