Fix: Ensure SMTP HELO/EHLO command is issued before MAIL FROM as per RFC 5321 (#621)

This commit is contained in:
Ralph Slooten
2026-01-23 17:27:13 +13:00
parent a3616e52d9
commit 9383c5876b
2 changed files with 29 additions and 12 deletions

View File

@@ -362,8 +362,9 @@ func (s *session) serve() {
// otherwise results in a 5s timeout for each connection
defer func(c net.Conn) { _ = c.Close() }(s.conn)
var gotEHLO bool
var from string
var gotFrom bool
var gotFROM bool
var to []string
var hasRejectedRecipients bool
var buffer bytes.Buffer
@@ -397,8 +398,9 @@ loop:
s.writef("250 %s greets %s", s.srv.Hostname, s.remoteName)
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET, so reset for HELO too.
gotEHLO = true
from = ""
gotFrom = false
gotFROM = false
to = nil
hasRejectedRecipients = false
buffer.Reset()
@@ -407,8 +409,9 @@ loop:
s.writef("%s", s.makeEHLOResponse())
// RFC 2821 section 4.1.4 specifies that EHLO has the same effect as RSET.
gotEHLO = true
from = ""
gotFrom = false
gotFROM = false
to = nil
hasRejectedRecipients = false
buffer.Reset()
@@ -421,6 +424,10 @@ loop:
s.writef("530 5.7.0 Authentication required")
break
}
if !gotEHLO {
s.writef("503 5.5.1 Bad sequence of commands (HELO/EHLO required before MAIL)")
break
}
match, err := extractAndValidateAddress(mailFromRE, args)
if match == nil {
@@ -442,7 +449,7 @@ loop:
if sizeMatch == nil {
// ignore other parameter
from = match[1]
gotFrom = true
gotFROM = true
s.writef("250 2.1.0 Ok")
} else {
// Enforce the maximum message size if one is set.
@@ -454,13 +461,13 @@ loop:
s.writef("%s", err.Error())
} else { // SIZE ok
from = match[1]
gotFrom = true
gotFROM = true
s.writef("250 2.1.0 Ok")
}
}
} else { // No parameters after FROM
from = match[1]
gotFrom = true
gotFROM = true
s.writef("250 2.1.0 Ok")
}
}
@@ -477,7 +484,7 @@ loop:
s.writef("530 5.7.0 Authentication required")
break
}
if !gotFrom {
if !gotFROM {
s.writef("503 5.5.1 Bad sequence of commands (MAIL required before RCPT)")
break
}
@@ -524,7 +531,7 @@ loop:
break
}
hasRecipients := len(to) > 0 || hasRejectedRecipients
if !gotFrom || !hasRecipients {
if !gotFROM || !hasRecipients {
s.writef("503 5.5.1 Bad sequence of commands (MAIL & RCPT required before DATA)")
break
}
@@ -602,7 +609,7 @@ loop:
// Reset for next mail.
from = ""
gotFrom = false
gotFROM = false
to = nil
hasRejectedRecipients = false
buffer.Reset()
@@ -616,7 +623,7 @@ loop:
}
s.writef("250 2.0.0 Ok")
from = ""
gotFrom = false
gotFROM = false
to = nil
hasRejectedRecipients = false
buffer.Reset()
@@ -693,7 +700,7 @@ loop:
// RFC 3207 specifies that the server must discard any prior knowledge obtained from the client.
s.remoteName = ""
from = ""
gotFrom = false
gotFROM = false
to = nil
hasRejectedRecipients = false
buffer.Reset()
@@ -715,7 +722,7 @@ loop:
}
// RFC 4954 specifies that AUTH is not permitted during mail transactions.
if gotFrom || len(to) > 0 {
if gotFROM || len(to) > 0 {
s.writef("503 5.5.1 Bad sequence of commands (AUTH not permitted during mail transaction)")
break
}

View File

@@ -124,6 +124,16 @@ func TestCmdEHLO(t *testing.T) {
_ = conn.Close()
}
func TestCmdMAILBeforeEHLO(t *testing.T) {
conn := newConn(t, &Server{})
// RFC 5321 §4.1.4 — Order of Commands states (emphasis added):
// “The SMTP client MUST issue HELO or EHLO before any other SMTP commands.”
cmdCode(t, conn, "MAIL FROM:<sender@example.com>", "503")
cmdCode(t, conn, "QUIT", "221")
_ = conn.Close()
}
func TestCmdRSET(t *testing.T) {
conn := newConn(t, &Server{})
cmdCode(t, conn, "EHLO host.example.com", "250")