Compare commits

..
2 Commits
Author SHA1 Message Date
Rsclub2_2 68940e831a feat: Sammelpostfach per SSO (OAuth/XOAUTH2) statt Passwort
Einmal "Connect mailbox" (OIDC mit PKCE, prompt=login, Anmeldung als technisches Konto des
Postfachs), Prüfung der Mail-Adresse im Token, Refresh-Token verschlüsselt in DATA_DIR,
Access-Token im Speicher; ein gunicorn-Worker wegen der Token-Rotation. Ohne OIDC_* weiter mit
EMAIL_PASSWORD.

Claude-Session: https://claude.ai/code/session_01SqzF6qT3TQ8ej3wigEuMSu
2026-10-29 14:29:22 +01:00
Rsclub2_2 b741389754 fix: Debug-Modus raus, gunicorn, UIDs statt Sequenznummern, nur http(s)-Links
- Flask-Debugger (Werkzeug, Codeausführung) war offen auf 0.0.0.0:5000; jetzt gunicorn, Healthcheck
- Verschieben per UID (Sequenznummern verschieben sich nach dem Expunge), nur Ziffern erlaubt
- "Move All" schickte keine IDs mit; Formular enthält jetzt alle UIDs
- Links aus Mails nur noch http(s) (kein javascript:/data:), rel=noopener noreferrer
- Betreff dekodiert, BODY.PEEK (Mails bleiben ungelesen), Python 3.12, feste Versionen, Nicht-Root
- Tests

Claude-Session: https://claude.ai/code/session_01SqzF6qT3TQ8ej3wigEuMSu
2026-10-29 14:08:16 +01:00
4 changed files with 5 additions and 50 deletions

No files matched your search

+3 -11
View File
@@ -150,22 +150,14 @@ def oidc_callback():
if not OAUTH:
abort(404)
if request.args.get("error"):
return render_template("index.html", email_address=EMAIL_USERNAME, oauth=True, connected=False,
error=request.args["error"]), 400
return redirect(url_for("index", error=request.args["error"]))
try:
OAUTH.finish(request.args.get("state", ""), request.args.get("code", ""))
except OAuthError as e:
logger.warning("Mailbox connect failed: %s", e)
return render_template("index.html", email_address=EMAIL_USERNAME, oauth=True, connected=False,
error=str(e)), 400
return redirect(url_for("index", error=str(e)))
logger.info("Collection mailbox %s connected via OAuth", EMAIL_USERNAME)
# the browser session now belongs to the technical account, which forward_auth does not let in:
# confirm here instead of redirecting to the start page
return (
"<!doctype html><meta charset=utf-8><title>Connected</title>"
"<p>Collection mailbox connected. Close this private window and open the Unsubscriber again "
"in your normal browser.</p>"
)
return redirect(url_for("index"))
@app.route("/disconnect", methods=["POST"])
+2 -2
View File
@@ -19,8 +19,8 @@
</form>
{% endif %}
{% else %}
<p>The collection mailbox is not connected yet. Open <code>/connect</code> of this site in a private
window and sign in as <strong>{{ email_address }}</strong> (technical account).</p>
<p>The collection mailbox is not connected yet. Open this page in a private window and sign in
as <strong>{{ email_address }}</strong> (technical account) – not with your own account.</p>
<form action="/connect" method="get">
<button type="submit" class="btn-primary">Connect mailbox</button>
</form>
-3
View File
@@ -21,7 +21,6 @@ from cryptography.fernet import Fernet, InvalidToken
SCOPES = "openid profile email offline_access"
REFRESH_MARGIN_SEC = 60
PENDING_TTL_SEC = 600
MAX_PENDING = 20 # /connect is reachable without login: bound the open requests
class OAuthError(Exception):
@@ -109,8 +108,6 @@ class MailboxOAuth:
now = self.clock()
with self._lock:
self._pending = {k: v for k, v in self._pending.items() if now - v[1] < PENDING_TTL_SEC}
while len(self._pending) >= MAX_PENDING:
del self._pending[min(self._pending, key=lambda k: self._pending[k][1])]
self._pending[state] = (verifier, now)
query = {
"response_type": "code",
-34
View File
@@ -139,37 +139,3 @@ def test_index_shows_connect_when_not_connected(monkeypatch):
monkeypatch.setattr(unsub, "OAUTH", FakeOAuth())
html = unsub.app.test_client().get("/").get_data(as_text=True)
assert 'action="/connect"' in html and 'action="/result"' not in html
def test_callback_confirms_without_redirect(monkeypatch):
import app as unsub
class FakeOAuth:
def finish(self, state, code):
assert (state, code) == ("s", "c")
monkeypatch.setattr(unsub, "OAUTH", FakeOAuth())
r = unsub.app.test_client().get("/oidc/callback?state=s&code=c")
assert r.status_code == 200 and "connected" in r.get_data(as_text=True)
def test_callback_error_is_shown(monkeypatch):
import app as unsub
class FakeOAuth:
def finish(self, state, code):
raise oauth.OAuthError("Signed in as philipp@example.org, expected unsubscribe-bot@example.org")
def connected(self):
return False
monkeypatch.setattr(unsub, "OAUTH", FakeOAuth())
r = unsub.app.test_client().get("/oidc/callback?state=s&code=c")
assert r.status_code == 400 and "expected unsubscribe-bot" in r.get_data(as_text=True)
def test_pending_requests_are_bounded(tmp_path):
mb = _mb(tmp_path, [])
for _ in range(oauth.MAX_PENDING + 15):
mb.start()
assert len(mb._pending) == oauth.MAX_PENDING