feat: ignore client-supplied POP3 credentials (#15)
Build and publish container / build (push) Successful in 12m15s
Build and publish container / build (push) Successful in 12m15s
Accept any POP3 `USER`/`PASS` from the client and discard them. The proxy always authenticates to the IMAP backend with the configured `BACKEND_IMAP_USER` / `BACKEND_IMAP_PASS`. ## Changes - `handle_user` / `handle_pass`: accept client credentials unconditionally, no validation. - `authenticate`: always use backend credentials; remove the fallback that connected with client-supplied credentials when backend credentials were unset. Raise a clear configuration error when backend credentials are missing. - Tests: client credentials are ignored; missing backend credentials are reported. Closes #14 --------- Co-authored-by: Emma Thorpe <emma.thorpe@citrix.com> Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
+17
-16
@@ -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
|
||||||
|
|||||||
@@ -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"]))
|
||||||
|
|||||||
Reference in New Issue
Block a user