feat: ignore client-supplied POP3 credentials #15
+17
-16
@@ -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
|
||||
|
||||
@@ -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"]))
|
||||
|
||||
Reference in New Issue
Block a user