feat: Postfaecher einrichten und Zahlungs-Mails abholen (Etappe B, Teil 2)
- Verschluesselung fuer Zugangsdaten: Ein IMAP-Passwort laesst sich nicht hashen, Certain muss es dem Mailserver vorlegen. Es liegt deshalb mit Fernet verschluesselt in der Datenbank, der Schluessel wird per HKDF aus session_secret abgeleitet und nirgends gespeichert. Wird das Geheimnis getauscht, meldet Certain das verstaendlich statt mit einem Stacktrace. - IMAP-Abruf ueber imaplib/email (Standardbibliothek). Ordnerliste, Verbindungstest mit klarer Fehlermeldung, Absender-Zuordnung auf Konten, danach als gelesen markieren oder verschieben. - Wiedererkennung ueber die Message-ID statt der UID – die vergibt der Server beim Verschieben neu. - Nichts wird stillschweigend gebucht: Eine Mail wird zum Eingangs-Eintrag. Fehlt das Zielkonto oder ist ein Feld unsicher, gilt sie als unklar. - Hintergrund-Thread beim Start, nach dem Muster des EZB-Kursabrufs: ein langsames Postfach darf den Dienst nie blockieren. Getestet gegen einen IMAP-Doppelgaenger, damit die Suite ohne Netz laeuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ gebaute Frontend (``web/dist``) aus – hinter dem Nginx Proxy Manager des Nutze
|
||||
Beim Start werden die Alembic-Migrationen angewandt und der erste Admin angelegt.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
@@ -19,6 +20,8 @@ from .config import get_settings
|
||||
from .database import SessionLocal
|
||||
from .deps import enforce_account_state
|
||||
from .migrate import upgrade_to_head
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from .routers import (
|
||||
accounts,
|
||||
auth,
|
||||
@@ -30,6 +33,7 @@ from .routers import (
|
||||
imports,
|
||||
policy,
|
||||
inbox,
|
||||
mail,
|
||||
quick,
|
||||
rates,
|
||||
settings as settings_router,
|
||||
@@ -68,7 +72,36 @@ async def lifespan(app: FastAPI):
|
||||
s.close()
|
||||
|
||||
threading.Thread(target=_laden, name="ecb-rates", daemon=True).start()
|
||||
|
||||
# Zahlungs-Mails regelmässig abholen. Wie beim Kursabruf im Hintergrund:
|
||||
# Ein langsames oder nicht erreichbares Postfach darf den Dienst nicht
|
||||
# blockieren. Fehler landen sichtbar an der Quelle, nicht im Log-Nirwana.
|
||||
stopp = threading.Event()
|
||||
|
||||
def _mails() -> None:
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from .models import MailSource
|
||||
from .services import mail_import
|
||||
|
||||
while not stopp.wait(60):
|
||||
s = SessionLocal()
|
||||
try:
|
||||
jetzt = datetime.now(timezone.utc)
|
||||
for quelle in s.query(MailSource).filter(MailSource.is_active.is_(True)):
|
||||
faellig = quelle.last_poll_at is None or quelle.last_poll_at + timedelta(
|
||||
minutes=quelle.poll_minutes
|
||||
) <= jetzt
|
||||
if faellig:
|
||||
mail_import.abruf_mit_fehlerbehandlung(s, quelle)
|
||||
except Exception:
|
||||
log.exception("Mail-Abruf fehlgeschlagen")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
threading.Thread(target=_mails, name="mail-abruf", daemon=True).start()
|
||||
yield
|
||||
stopp.set()
|
||||
|
||||
|
||||
app = FastAPI(title=APP_NAME, version=VERSION, lifespan=lifespan)
|
||||
@@ -120,6 +153,7 @@ api.include_router(backup.router)
|
||||
api.include_router(rates.router)
|
||||
api.include_router(quick.router)
|
||||
api.include_router(inbox.router)
|
||||
api.include_router(mail.router)
|
||||
|
||||
app.include_router(api)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user