feat: ignore client-supplied POP3 credentials
Build and publish container / build (pull_request) Successful in 9m23s

POP3 clients may be required to send USER/PASS, but the proxy never uses
them. Accept any client credentials blindly and always authenticate to the
IMAP backend with the configured BACKEND_IMAP_USER / BACKEND_IMAP_PASS.

Remove the previous fallback that connected to the backend using
client-supplied credentials when backend credentials were unset; the proxy
now raises a clear configuration error in that case.

Add tests covering that client credentials are ignored and that missing
backend credentials are reported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Emma Thorpe
2026-06-17 18:07:25 +01:00
parent e51740b8db
commit 78a3c21ac7
2 changed files with 60 additions and 16 deletions
+17 -16
View File
@@ -253,30 +253,31 @@ class POP3Session:
return await self.send_line("-ERR Unsupported command") return await self.send_line("-ERR Unsupported command")
async def handle_user(self, args): async def handle_user(self, args):
if len(args) != 1: # Accept any username. Client credentials are intentionally ignored;
return await self.send_line("-ERR USER requires username") # some legacy clients insist on supplying them, so they are accepted
self.username = args[0] # blindly. The backend is always reached with the proxy's own creds.
self.username = args[0] if args else None
return await self.send_line("+OK") return await self.send_line("+OK")
async def handle_pass(self, args): async def handle_pass(self, args):
if len(args) != 1: # Accept any password. See handle_user: client credentials are
return await self.send_line("-ERR PASS requires password") # accepted but never used or validated.
self.password = args[0] self.password = args[0] if args else None
await asyncio.to_thread(self.authenticate) await asyncio.to_thread(self.authenticate)
return await self.send_line("+OK User authenticated") return await self.send_line("+OK User authenticated")
def authenticate(self): def authenticate(self):
"""Authenticate to the IMAP backend using configured credentials.""" """Authenticate to the IMAP backend using the configured proxy credentials.
if Settings.BACKEND_IMAP_USER and Settings.BACKEND_IMAP_PASS:
username = Settings.BACKEND_IMAP_USER
password = Settings.BACKEND_IMAP_PASS
elif self.username and self.password:
username = self.username
password = self.password
else:
raise RuntimeError("No IMAP credentials available")
backend = IMAPBackend(username, password) Client-supplied POP3 credentials are deliberately ignored: the proxy
always connects to the backend with ``BACKEND_IMAP_USER`` /
``BACKEND_IMAP_PASS``. This is by design for legacy clients that require
credentials to be entered even though the proxy does not use them.
"""
if not (Settings.BACKEND_IMAP_USER and Settings.BACKEND_IMAP_PASS):
raise RuntimeError("Backend IMAP credentials are not configured")
backend = IMAPBackend(Settings.BACKEND_IMAP_USER, Settings.BACKEND_IMAP_PASS)
backend.login() backend.login()
self._imap = backend self._imap = backend
# Snapshot the maildrop once; it stays static for the session lifetime # Snapshot the maildrop once; it stays static for the session lifetime
+43
View File
@@ -227,6 +227,49 @@ def test_top_returns_headers_and_limited_body():
assert output.endswith(b".\r\n") assert output.endswith(b".\r\n")
def test_authenticate_ignores_client_credentials(monkeypatch):
captured = {}
def fake_login(self):
captured["username"] = self.username
captured["password"] = self.password
monkeypatch.setattr(IMAPBackend, "login", fake_login)
monkeypatch.setattr(IMAPBackend, "list_uids", lambda self: [])
previous_user = Settings.BACKEND_IMAP_USER
previous_pass = Settings.BACKEND_IMAP_PASS
Settings.BACKEND_IMAP_USER = "backend-user"
Settings.BACKEND_IMAP_PASS = "backend-pass"
session = POP3Session(None, FakeWriter())
session.username = "client-user"
session.password = "client-pass"
session.authenticate()
# The proxy must connect with its own credentials, never the client's.
assert captured["username"] == "backend-user"
assert captured["password"] == "backend-pass"
Settings.BACKEND_IMAP_USER = previous_user
Settings.BACKEND_IMAP_PASS = previous_pass
def test_authenticate_requires_backend_credentials(monkeypatch):
previous_user = Settings.BACKEND_IMAP_USER
previous_pass = Settings.BACKEND_IMAP_PASS
Settings.BACKEND_IMAP_USER = None
Settings.BACKEND_IMAP_PASS = None
session = POP3Session(None, FakeWriter())
session.username = "client-user"
session.password = "client-pass"
with pytest.raises(RuntimeError, match="Backend IMAP credentials are not configured"):
session.authenticate()
Settings.BACKEND_IMAP_USER = previous_user
Settings.BACKEND_IMAP_PASS = previous_pass
def test_dele_survives_stat_list_uidl_until_quit(): def test_dele_survives_stat_list_uidl_until_quit():
session = make_session([b"1", b"2", b"3"]) session = make_session([b"1", b"2", b"3"])
asyncio.run(session.handle_dele(["2"])) asyncio.run(session.handle_dele(["2"]))