diff --git a/app/routers/filters.py b/app/routers/filters.py index 362b7ef..b609ab4 100644 --- a/app/routers/filters.py +++ b/app/routers/filters.py @@ -5,7 +5,15 @@ from sqlalchemy.orm import Session import logging from app.database import get_db -from app.models.db_models import Account, FilterAction, FilterCondition, FilterRule, ProcessedMail +from app.models.db_models import ( + Account, + FilterAction, + FilterCondition, + FilterLog, + FilterRule, + LogLevel, + ProcessedMail, +) from app.schemas.schemas import FilterRuleCreate, FilterRuleResponse, FilterRuleUpdate logger = logging.getLogger(__name__) @@ -25,6 +33,36 @@ def _reset_processed_for_rule(db: Session, rule_id: int) -> int: router = APIRouter(prefix="/api/filters", tags=["filters"]) +@router.post("/retry-failed") +def retry_failed_fetches(account_id: int | None = None, db: Session = Depends(get_db)): + """Löscht ProcessedMail-Einträge für UIDs, für die es einen ERROR-Log + mit 'Fehler beim Abrufen' gibt — damit der Scheduler sie beim nächsten + Poll neu versucht (z.B. nach einem Codec-Fix).""" + log_q = db.query(FilterLog.mail_uid, FilterLog.account_id).filter( + FilterLog.level == LogLevel.ERROR, + FilterLog.message.like("Fehler beim Abrufen%"), + FilterLog.mail_uid.isnot(None), + ) + if account_id is not None: + log_q = log_q.filter(FilterLog.account_id == account_id) + + pairs = {(row[0], row[1]) for row in log_q.all()} + if not pairs: + return {"reset": 0, "unique_uids": 0} + + total = 0 + for uid, acc_id in pairs: + q = db.query(ProcessedMail).filter(ProcessedMail.mail_uid == uid) + if acc_id is not None: + q = q.filter(ProcessedMail.account_id == acc_id) + total += q.delete(synchronize_session=False) + db.commit() + logger.info( + "Retry-Failed: %d ProcessedMail-Einträge für %d UIDs entfernt", total, len(pairs) + ) + return {"reset": total, "unique_uids": len(pairs)} + + @router.get("/account/{account_id}", response_model=list[FilterRuleResponse]) def list_filters(account_id: int, db: Session = Depends(get_db)): account = db.get(Account, account_id) diff --git a/app/services/imap_client.py b/app/services/imap_client.py index 1da43cd..1cb3e80 100644 --- a/app/services/imap_client.py +++ b/app/services/imap_client.py @@ -33,6 +33,23 @@ def _quote_mailbox(name: str) -> str: return f'"{escaped}"' +def _safe_decode(data: bytes, charset: str | None) -> str: + """Dekodiert Bytes robust: bekannten Codec normal, unbekannte oder MIME-Pseudo- + Encodings (unknown-8bit, x-unknown, 8bit, binary, ...) fallen auf latin-1 zurück — + latin-1 kann jedes Byte 1:1 abbilden und wirft nie einen Fehler.""" + if not isinstance(data, (bytes, bytearray)): + return str(data) + charset = (charset or "").strip().lower() or "utf-8" + # Bekannte Pseudo-Encodings direkt auf latin-1 mappen + if charset in {"unknown-8bit", "x-unknown", "unknown", "8bit", "7bit", "binary", "us-ascii-8bit"}: + return data.decode("latin-1", errors="replace") + try: + return data.decode(charset, errors="replace") + except (LookupError, TypeError): + # Unbekannter Codec (z.B. proprietäre Schreibweisen) → latin-1 als sicherer Fallback + return data.decode("latin-1", errors="replace") + + def _decode_header_value(value: str | None) -> str: if not value: return "" @@ -40,7 +57,7 @@ def _decode_header_value(value: str | None) -> str: decoded = [] for part, charset in parts: if isinstance(part, bytes): - decoded.append(part.decode(charset or "utf-8", errors="replace")) + decoded.append(_safe_decode(part, charset)) else: decoded.append(part) return " ".join(decoded) @@ -78,13 +95,11 @@ def _extract_body(msg: Message) -> str: if content_type == "text/plain": payload = part.get_payload(decode=True) if payload: - charset = part.get_content_charset() or "utf-8" - return payload.decode(charset, errors="replace") + return _safe_decode(payload, part.get_content_charset()) return "" payload = msg.get_payload(decode=True) if payload: - charset = msg.get_content_charset() or "utf-8" - return payload.decode(charset, errors="replace") + return _safe_decode(payload, msg.get_content_charset()) return "" diff --git a/app/templates/logs.html b/app/templates/logs.html index 3945fe6..da3c794 100644 --- a/app/templates/logs.html +++ b/app/templates/logs.html @@ -29,6 +29,7 @@