Files
certain/backend/app/routers/mail.py
T
GuidoandClaude Opus 5 60b8178c57 fix: Mails vor der Einrichtung waren fuer immer ausserhalb des Suchfensters
Der Zeitstempel des letzten Abrufs wird auch dann gesetzt, wenn ein Abruf
nichts fand – und danach sucht Certain nur noch ab gestern. Wer das Postfach
einrichtet, nachdem die Mails schon da waren, findet sie nie: Der erste,
ergebnislose Abruf schiebt das Fenster an ihnen vorbei.

Jetzt gibt es "Aelteres nachholen": Es sucht die letzten 90 Tage und uebergeht
den Zeitstempel. Dass dabei nichts doppelt ankommt, sichert wie bisher die
Message-ID.

Ausserdem sagt die Meldung jetzt, AB WANN gesucht wurde. Bisher stand dort
"Stimmt der Ordner?" – die Frage war fast immer die falsche.

Und ein latenter Fehler, der im August nicht auffaellt: Das Suchdatum wurde mit
strftime("%b") gebaut, also mit dem Monatskuerzel in der Sprache des Servers.
IMAP verlangt die englischen; auf einem deutschen System haette die Suche im
Maerz, Mai, Oktober und Dezember nichts gefunden. Die Kuerzel stehen jetzt fest
im Code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:59:08 +02:00

151 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Postfächer einrichten und Zahlungs-Mails abholen."""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user, get_owned
from ..models import Account, MailSender, MailSource, User
from ..schemas import MailSenderIn, MailSenderOut, MailSourceIn, MailSourceOut
from ..services import geheimnis, mail_import
router = APIRouter(prefix="/mail", tags=["mail"])
def _aus(quelle: MailSource) -> MailSourceOut:
out = MailSourceOut.model_validate(quelle)
out.password_set = bool(quelle.password_enc)
return out
@router.get("/sources", response_model=list[MailSourceOut])
def list_sources(db: Session = Depends(get_db), user: User = Depends(get_current_user)):
quellen = (
db.query(MailSource).filter(MailSource.owner_id == user.id).order_by(MailSource.id).all()
)
return [_aus(q) for q in quellen]
@router.post("/sources", response_model=MailSourceOut, status_code=status.HTTP_201_CREATED)
def create_source(
payload: MailSourceIn, db: Session = Depends(get_db), user: User = Depends(get_current_user)
):
if not payload.password:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Passwort fehlt")
quelle = MailSource(
owner_id=user.id,
**payload.model_dump(exclude={"password"}),
password_enc=geheimnis.verschluesseln(payload.password),
)
db.add(quelle)
db.commit()
db.refresh(quelle)
return _aus(quelle)
@router.put("/sources/{source_id}", response_model=MailSourceOut)
def update_source(
source_id: int,
payload: MailSourceIn,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
quelle = get_owned(db, MailSource, source_id, user)
for schluessel, wert in payload.model_dump(exclude={"password"}).items():
setattr(quelle, schluessel, wert)
# Leeres Passwortfeld heisst „unverändert" – sonst müsste man es bei jeder
# Kleinigkeit erneut eintippen.
if payload.password:
quelle.password_enc = geheimnis.verschluesseln(payload.password)
db.commit()
db.refresh(quelle)
return _aus(quelle)
@router.delete("/sources/{source_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_source(source_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
db.delete(get_owned(db, MailSource, source_id, user))
db.commit()
@router.post("/sources/{source_id}/test")
def test_source(source_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
"""Verbindung prüfen und die Ordner des Postfachs zurückgeben."""
quelle = get_owned(db, MailSource, source_id, user)
try:
return mail_import.teste(quelle)
except mail_import.MailFehler as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
@router.post("/sources/{source_id}/fetch")
def fetch_source(source_id: int, tage: int | None = None,
db: Session = Depends(get_db), user: User = Depends(get_current_user)):
"""Mails abholen. Mit ``tage`` ab heute minus so vielen Tagen statt ab dem
letzten Abruf – für Mails, die schon vor der Einrichtung im Postfach lagen."""
quelle = get_owned(db, MailSource, source_id, user)
if tage is not None and not 1 <= tage <= 365:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="Der Zeitraum muss zwischen 1 und 365 Tagen liegen.")
try:
return mail_import.hole_neue(db, quelle, tage)
except mail_import.MailFehler as e:
quelle.last_error = str(e)[:500]
db.commit()
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
# --- Absender-Zuordnung ------------------------------------------------------
@router.get("/sources/{source_id}/senders", response_model=list[MailSenderOut])
def list_senders(source_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
get_owned(db, MailSource, source_id, user)
return (
db.query(MailSender)
.filter(MailSender.source_id == source_id)
.order_by(MailSender.id)
.all()
)
@router.post("/sources/{source_id}/senders", response_model=MailSenderOut,
status_code=status.HTTP_201_CREATED)
def create_sender(
source_id: int,
payload: MailSenderIn,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
get_owned(db, MailSource, source_id, user)
if payload.account_id is not None:
get_owned(db, Account, payload.account_id, user)
absender = MailSender(source_id=source_id, owner_id=user.id, **payload.model_dump())
db.add(absender)
db.commit()
db.refresh(absender)
return absender
@router.put("/senders/{sender_id}", response_model=MailSenderOut)
def update_sender(
sender_id: int,
payload: MailSenderIn,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
absender = get_owned(db, MailSender, sender_id, user)
if payload.account_id is not None:
get_owned(db, Account, payload.account_id, user)
for schluessel, wert in payload.model_dump().items():
setattr(absender, schluessel, wert)
db.commit()
db.refresh(absender)
return absender
@router.delete("/senders/{sender_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_sender(sender_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)):
db.delete(get_owned(db, MailSender, sender_id, user))
db.commit()