- Dockerfile/compose: Flask-App + selenium/standalone-chrome (noVNC), Healthcheck - app.py: /healthz + eingebettete noVNC-Live-Ansicht (NOVNC_URL, lokal:7900 / Cluster:/novnc/) - Helm-Chart infra/k8s/base/ibo: App + Selenium-Sidecar, 2 Ports, Ingress (/ + /novnc via stripPrefix), SSO-Middlewares aus dem CD-Repo - Gitea-CI: version -> test -> build-push -> helm-validate -> deploy-dev (Write-back) - .gitignore/.dockerignore (Dev-Samples raus) - infra/RUNBOOK-ibo.md: Zitadel/DNS/Deploy/Prod-Promote Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
170 lines
5.5 KiB
Python
170 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
mail.tm - Wegwerf-E-Mail komplett per API, ohne Einrichtung/Konto.
|
|
|
|
Nur Python-Bordmittel (urllib). Ablauf:
|
|
addr, pw, token = create_account() # zufaellige Adresse anlegen
|
|
... Adresse ins Kontaktformular eintragen ...
|
|
link = wait_for_message(token, extractor, log) # auf Mail warten
|
|
|
|
API-Doku: https://docs.mail.tm
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
import random
|
|
import string
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
BASE = "https://api.mail.tm"
|
|
|
|
|
|
def _req(method, path, data=None, token=None):
|
|
url = BASE + path
|
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
if token:
|
|
headers["Authorization"] = "Bearer " + token
|
|
body = json.dumps(data).encode() if data is not None else None
|
|
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
raw = resp.read().decode("utf-8", errors="replace")
|
|
return json.loads(raw) if raw else {}
|
|
except urllib.error.HTTPError as e:
|
|
detail = e.read().decode("utf-8", errors="replace")
|
|
raise RuntimeError(f"mail.tm {method} {path} -> HTTP {e.code}: {detail}")
|
|
|
|
|
|
def _rand(n):
|
|
return "".join(random.choice(string.ascii_lowercase + string.digits)
|
|
for _ in range(n))
|
|
|
|
|
|
def _members(data):
|
|
"""Liste aus der Antwort holen - egal ob reine Liste oder Hydra-Format."""
|
|
if isinstance(data, list):
|
|
return data
|
|
if isinstance(data, dict):
|
|
return data.get("hydra:member", data.get("member", []))
|
|
return []
|
|
|
|
|
|
def get_domain():
|
|
"""Erste aktive mail.tm-Domain holen."""
|
|
data = _req("GET", "/domains?page=1")
|
|
domains = _members(data)
|
|
for d in domains:
|
|
if d.get("isActive", True):
|
|
return d["domain"]
|
|
raise RuntimeError("Keine aktive mail.tm-Domain verfuegbar.")
|
|
|
|
|
|
def create_account():
|
|
"""Legt eine zufaellige Wegwerf-Adresse an. Gibt (address, password, token)."""
|
|
domain = get_domain()
|
|
address = f"{_rand(10)}@{domain}"
|
|
password = _rand(12)
|
|
# Account anlegen (kann bei Rate-Limit 429 liefern -> kurz warten & retry)
|
|
for attempt in range(3):
|
|
try:
|
|
_req("POST", "/accounts", {"address": address, "password": password})
|
|
break
|
|
except RuntimeError as e:
|
|
if "429" in str(e) and attempt < 2:
|
|
time.sleep(2)
|
|
continue
|
|
raise
|
|
token = get_token(address, password)
|
|
return address, password, token
|
|
|
|
|
|
def get_token(address, password):
|
|
data = _req("POST", "/token", {"address": address, "password": password})
|
|
return data["token"]
|
|
|
|
|
|
def list_messages(token):
|
|
data = _req("GET", "/messages?page=1", token=token)
|
|
return _members(data)
|
|
|
|
|
|
def get_message_html(token, message_id):
|
|
"""Volltext einer Nachricht als HTML (oder Text-Fallback)."""
|
|
data = _req("GET", f"/messages/{message_id}", token=token)
|
|
html = data.get("html")
|
|
if isinstance(html, list):
|
|
return "\n".join(html)
|
|
if isinstance(html, str):
|
|
return html
|
|
return data.get("text", "")
|
|
|
|
|
|
def wait_for_message(token, extractor, log,
|
|
sender_hint="", timeout=300, poll=10, should_cancel=None):
|
|
"""
|
|
Pollt das Postfach, bis eine Mail kommt, aus der `extractor(html)` einen
|
|
Link liefert. `extractor` ist eine Funktion html->str|None.
|
|
`should_cancel` (optional): Funktion ()->bool; liefert sie True, wird das
|
|
Warten sofort abgebrochen (z.B. Item geloescht/Batch gestoppt).
|
|
"""
|
|
def _cancelled_sleep(seconds):
|
|
"""Schlaeft in kleinen Schritten und bricht sofort ab, wenn should_cancel
|
|
True liefert. Gibt True zurueck, wenn abgebrochen wurde."""
|
|
slept = 0.0
|
|
while slept < seconds:
|
|
if should_cancel and should_cancel():
|
|
return True
|
|
time.sleep(0.5)
|
|
slept += 0.5
|
|
return False
|
|
|
|
deadline = time.time() + timeout
|
|
seen = set()
|
|
while time.time() < deadline:
|
|
if should_cancel and should_cancel():
|
|
log("Warten abgebrochen.")
|
|
return None
|
|
try:
|
|
msgs = list_messages(token)
|
|
except RuntimeError as e:
|
|
log(f"mail.tm Fehler: {e}")
|
|
if _cancelled_sleep(poll):
|
|
log("Warten abgebrochen.")
|
|
return None
|
|
continue
|
|
|
|
for m in msgs:
|
|
mid = m.get("id")
|
|
if not mid or mid in seen:
|
|
continue
|
|
seen.add(mid)
|
|
frm = ""
|
|
if isinstance(m.get("from"), dict):
|
|
frm = m["from"].get("address", "")
|
|
if sender_hint and sender_hint.lower() not in frm.lower():
|
|
continue
|
|
html = get_message_html(token, mid)
|
|
link = extractor(html)
|
|
if link:
|
|
log(f"Mail von {frm}: Link gefunden.")
|
|
return link
|
|
|
|
remaining = int(deadline - time.time())
|
|
log(f"Noch keine passende Mail. Warte {poll}s (Rest {remaining}s) ...")
|
|
if _cancelled_sleep(poll):
|
|
log("Warten abgebrochen.")
|
|
return None
|
|
|
|
log("!! Timeout - keine Mail mit Link gefunden.")
|
|
return None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Kleiner Selbsttest: Adresse anlegen und Posteingang einmal lesen.
|
|
a, p, t = create_account()
|
|
print("Wegwerf-Adresse:", a)
|
|
print("Passwort:", p)
|
|
print("Nachrichten aktuell:", len(list_messages(t)))
|
|
print("Schick eine Mail an obige Adresse und starte erneut zum Testen.")
|