From 78a3c21ac7c527253f779b5c3a794fa6f4284ab9 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 17 Jun 2026 18:07:25 +0100 Subject: [PATCH] feat: ignore client-supplied POP3 credentials 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) --- proxy_server.py | 33 +++++++++++++++-------------- tests/test_proxy_server.py | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/proxy_server.py b/proxy_server.py index e37b23a..6b7933f 100644 --- a/proxy_server.py +++ b/proxy_server.py @@ -253,30 +253,31 @@ class POP3Session: return await self.send_line("-ERR Unsupported command") async def handle_user(self, args): - if len(args) != 1: - return await self.send_line("-ERR USER requires username") - self.username = args[0] + # Accept any username. Client credentials are intentionally ignored; + # some legacy clients insist on supplying them, so they are accepted + # 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") async def handle_pass(self, args): - if len(args) != 1: - return await self.send_line("-ERR PASS requires password") - self.password = args[0] + # Accept any password. See handle_user: client credentials are + # accepted but never used or validated. + self.password = args[0] if args else None await asyncio.to_thread(self.authenticate) return await self.send_line("+OK User authenticated") def authenticate(self): - """Authenticate to the IMAP backend using configured 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") + """Authenticate to the IMAP backend using the configured proxy credentials. - 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() self._imap = backend # Snapshot the maildrop once; it stays static for the session lifetime diff --git a/tests/test_proxy_server.py b/tests/test_proxy_server.py index c904ac9..139956a 100644 --- a/tests/test_proxy_server.py +++ b/tests/test_proxy_server.py @@ -227,6 +227,49 @@ def test_top_returns_headers_and_limited_body(): 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(): session = make_session([b"1", b"2", b"3"]) asyncio.run(session.handle_dele(["2"]))