fix: SMTP server handles bare LF line endings and AUTH PLAIN continuation

Two bugs fixed:
- data_received only split on CRLF, so clients sending bare LF (telnet, nc,
  some libraries) got no responses at all. Now splits on LF and strips
  trailing CR, matching real Postfix behavior.
- AUTH PLAIN without inline credentials set state to "await_plain" but no
  handler existed for that state, causing the next line to be dispatched as
  a normal command. Added the missing state handler.
This commit is contained in:
2026-04-13 23:46:50 -04:00
parent fd62413935
commit b71db65149
2 changed files with 85 additions and 4 deletions

View File

@@ -87,9 +87,10 @@ class SMTPProtocol(asyncio.Protocol):
def data_received(self, data):
self._buf += data
while b"\r\n" in self._buf:
line, self._buf = self._buf.split(b"\r\n", 1)
self._handle_line(line.decode(errors="replace"))
while b"\n" in self._buf:
line, self._buf = self._buf.split(b"\n", 1)
# Strip trailing \r so both CRLF and bare LF work
self._handle_line(line.rstrip(b"\r").decode(errors="replace"))
def connection_lost(self, exc):
_log("disconnect", src=self._peer[0] if self._peer else "?")
@@ -118,7 +119,12 @@ class SMTPProtocol(asyncio.Protocol):
self._data_buf.append(line[1:] if line.startswith(".") else line)
return
# ── AUTH multi-step (LOGIN mechanism) ─────────────────────────────────
# ── AUTH multi-step (LOGIN / PLAIN continuation) ─────────────────────
if self._auth_state == "await_plain":
user, password = _decode_auth_plain(line)
self._finish_auth(user, password)
self._auth_state = ""
return
if self._auth_state == "await_user":
self._auth_user = base64.b64decode(line + "==").decode(errors="replace")
self._auth_state = "await_pass"