addes folder backup attachments logging
This commit is contained in:
+111
-2
@@ -2,15 +2,17 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.db_models import Account
|
||||
from app.models.db_models import Account, ProcessedMail
|
||||
from app.schemas.schemas import (
|
||||
AccountCreate,
|
||||
AccountListResponse,
|
||||
AccountResponse,
|
||||
AccountUpdate,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.services.encryption import decrypt, encrypt
|
||||
from app.services.imap_client import async_test_connection
|
||||
from app.services.imap_client import IMAPClient, async_test_connection
|
||||
|
||||
router = APIRouter(prefix="/api/accounts", tags=["accounts"])
|
||||
|
||||
@@ -87,6 +89,26 @@ async def test_account_connection(account_id: int, db: Session = Depends(get_db)
|
||||
return {"success": success, "message": "Verbindung erfolgreich" if success else "Verbindung fehlgeschlagen"}
|
||||
|
||||
|
||||
class TestConnectionRequest(BaseModel):
|
||||
imap_host: str
|
||||
imap_port: int = 993
|
||||
use_ssl: bool = True
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
@router.post("/test-connection")
|
||||
async def test_connection_direct(data: TestConnectionRequest):
|
||||
success = await async_test_connection(
|
||||
host=data.imap_host,
|
||||
port=data.imap_port,
|
||||
username=data.username,
|
||||
password=data.password,
|
||||
use_ssl=data.use_ssl,
|
||||
)
|
||||
return {"success": success, "message": "Verbindung erfolgreich" if success else "Verbindung fehlgeschlagen"}
|
||||
|
||||
|
||||
@router.post("/{account_id}/poll-now")
|
||||
async def poll_now(account_id: int, db: Session = Depends(get_db)):
|
||||
account = db.get(Account, account_id)
|
||||
@@ -95,3 +117,90 @@ async def poll_now(account_id: int, db: Session = Depends(get_db)):
|
||||
from app.services.scheduler import poll_account
|
||||
await poll_account(account_id)
|
||||
return {"message": f"Polling für '{account.name}' durchgeführt"}
|
||||
|
||||
|
||||
@router.get("/{account_id}/processed")
|
||||
def get_processed_stats(account_id: int, db: Session = Depends(get_db)):
|
||||
account = db.get(Account, account_id)
|
||||
if not account:
|
||||
raise HTTPException(404, "Konto nicht gefunden")
|
||||
total = db.query(ProcessedMail).filter(ProcessedMail.account_id == account_id).count()
|
||||
return {"account_id": account_id, "processed_count": total}
|
||||
|
||||
|
||||
@router.delete("/{account_id}/processed")
|
||||
def reset_processed(account_id: int, folder: str | None = None, db: Session = Depends(get_db)):
|
||||
account = db.get(Account, account_id)
|
||||
if not account:
|
||||
raise HTTPException(404, "Konto nicht gefunden")
|
||||
query = db.query(ProcessedMail).filter(ProcessedMail.account_id == account_id)
|
||||
if folder:
|
||||
query = query.filter(ProcessedMail.folder == folder)
|
||||
count = query.delete()
|
||||
db.commit()
|
||||
scope = f"Ordner '{folder}'" if folder else "alle Ordner"
|
||||
return {"message": f"Verarbeitung zurückgesetzt für {scope} ({count} Einträge)", "deleted": count}
|
||||
|
||||
|
||||
def _get_imap_client(account: Account) -> IMAPClient:
|
||||
return IMAPClient(
|
||||
host=account.imap_host,
|
||||
port=account.imap_port,
|
||||
username=account.username,
|
||||
password=decrypt(account.password),
|
||||
use_ssl=account.use_ssl,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{account_id}/folders")
|
||||
async def list_folders(account_id: int, debug: bool = False, db: Session = Depends(get_db)):
|
||||
account = db.get(Account, account_id)
|
||||
if not account:
|
||||
raise HTTPException(404, "Konto nicht gefunden")
|
||||
|
||||
import asyncio
|
||||
def _list():
|
||||
client = _get_imap_client(account)
|
||||
with client:
|
||||
folders = client.list_folders()
|
||||
raw = None
|
||||
if debug:
|
||||
status, data = client.conn.list()
|
||||
raw = [item.decode("utf-8", errors="replace") if isinstance(item, bytes) else str(item) for item in (data or [])]
|
||||
return folders, raw
|
||||
|
||||
try:
|
||||
folders, raw = await asyncio.to_thread(_list)
|
||||
result = {"folders": folders}
|
||||
if debug and raw is not None:
|
||||
result["raw"] = raw
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Fehler beim Abrufen der Ordner: {e}")
|
||||
|
||||
|
||||
class CreateFolderRequest(BaseModel):
|
||||
folder_name: str
|
||||
|
||||
|
||||
@router.post("/{account_id}/folders")
|
||||
async def create_folder(account_id: int, data: CreateFolderRequest, db: Session = Depends(get_db)):
|
||||
account = db.get(Account, account_id)
|
||||
if not account:
|
||||
raise HTTPException(404, "Konto nicht gefunden")
|
||||
|
||||
import asyncio
|
||||
def _create():
|
||||
client = _get_imap_client(account)
|
||||
with client:
|
||||
return client.create_folder(data.folder_name)
|
||||
|
||||
try:
|
||||
success = await asyncio.to_thread(_create)
|
||||
if success:
|
||||
return {"success": True, "message": f"Ordner '{data.folder_name}' erstellt"}
|
||||
raise HTTPException(400, f"Ordner '{data.folder_name}' konnte nicht erstellt werden")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Fehler: {e}")
|
||||
|
||||
+29
-1
@@ -1,10 +1,26 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import logging
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.db_models import Account, FilterAction, FilterCondition, FilterRule
|
||||
from app.models.db_models import Account, FilterAction, FilterCondition, FilterRule, ProcessedMail
|
||||
from app.schemas.schemas import FilterRuleCreate, FilterRuleResponse, FilterRuleUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _reset_processed_for_folder(db: Session, account_id: int, folder: str) -> int:
|
||||
"""Reset processed mails for a specific account/folder so they get re-evaluated."""
|
||||
count = (
|
||||
db.query(ProcessedMail)
|
||||
.filter(ProcessedMail.account_id == account_id, ProcessedMail.folder == folder)
|
||||
.delete()
|
||||
)
|
||||
if count:
|
||||
logger.info("Filter geändert: %d verarbeitete Mails in '%s' zurückgesetzt (Account %d)", count, folder, account_id)
|
||||
return count
|
||||
|
||||
router = APIRouter(prefix="/api/filters", tags=["filters"])
|
||||
|
||||
|
||||
@@ -54,6 +70,9 @@ def create_filter(data: FilterRuleCreate, db: Session = Depends(get_db)):
|
||||
action = FilterAction(rule_id=rule.id, **action_data.model_dump())
|
||||
db.add(action)
|
||||
|
||||
# Neue Regel → Ordner zurücksetzen damit bestehende Mails geprüft werden
|
||||
_reset_processed_for_folder(db, data.account_id, data.source_folder)
|
||||
|
||||
db.commit()
|
||||
db.refresh(rule)
|
||||
return rule
|
||||
@@ -65,6 +84,7 @@ def update_filter(rule_id: int, data: FilterRuleUpdate, db: Session = Depends(ge
|
||||
if not rule:
|
||||
raise HTTPException(404, "Filterregel nicht gefunden")
|
||||
|
||||
old_folder = rule.source_folder
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# Update conditions if provided
|
||||
@@ -88,6 +108,12 @@ def update_filter(rule_id: int, data: FilterRuleUpdate, db: Session = Depends(ge
|
||||
for key, value in update_data.items():
|
||||
setattr(rule, key, value)
|
||||
|
||||
# Regel geändert → betroffene Ordner zurücksetzen
|
||||
_reset_processed_for_folder(db, rule.account_id, old_folder)
|
||||
new_folder = rule.source_folder
|
||||
if new_folder != old_folder:
|
||||
_reset_processed_for_folder(db, rule.account_id, new_folder)
|
||||
|
||||
db.commit()
|
||||
db.refresh(rule)
|
||||
return rule
|
||||
@@ -98,6 +124,8 @@ def delete_filter(rule_id: int, db: Session = Depends(get_db)):
|
||||
rule = db.get(FilterRule, rule_id)
|
||||
if not rule:
|
||||
raise HTTPException(404, "Filterregel nicht gefunden")
|
||||
# Ordner zurücksetzen — andere Regeln könnten jetzt anders greifen
|
||||
_reset_processed_for_folder(db, rule.account_id, rule.source_folder)
|
||||
db.delete(rule)
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.db_models import FilterLog
|
||||
|
||||
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def get_logs(
|
||||
account_id: int | None = None,
|
||||
level: str | None = None,
|
||||
limit: int = Query(default=100, le=500),
|
||||
offset: int = 0,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = db.query(FilterLog).order_by(FilterLog.created_at.desc())
|
||||
if account_id:
|
||||
query = query.filter(FilterLog.account_id == account_id)
|
||||
if level:
|
||||
query = query.filter(FilterLog.level == level)
|
||||
total = query.count()
|
||||
logs = query.offset(offset).limit(limit).all()
|
||||
return {
|
||||
"total": total,
|
||||
"logs": [
|
||||
{
|
||||
"id": log.id,
|
||||
"account_id": log.account_id,
|
||||
"account_name": log.account_name,
|
||||
"level": log.level.value if log.level else "info",
|
||||
"message": log.message,
|
||||
"rule_name": log.rule_name,
|
||||
"action_type": log.action_type,
|
||||
"mail_uid": log.mail_uid,
|
||||
"mail_subject": log.mail_subject,
|
||||
"mail_from": log.mail_from,
|
||||
"folder": log.folder,
|
||||
"details": log.details,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/")
|
||||
def clear_logs(account_id: int | None = None, db: Session = Depends(get_db)):
|
||||
query = db.query(FilterLog)
|
||||
if account_id:
|
||||
query = query.filter(FilterLog.account_id == account_id)
|
||||
count = query.delete()
|
||||
db.commit()
|
||||
return {"deleted": count}
|
||||
@@ -1,8 +1,9 @@
|
||||
from fastapi import APIRouter, Depends, UploadFile
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from fastapi.responses import PlainTextResponse, Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.services.backup_service import export_backup, import_backup
|
||||
from app.services.yaml_service import export_to_yaml, import_from_yaml
|
||||
|
||||
router = APIRouter(prefix="/api/yaml", tags=["yaml"])
|
||||
@@ -19,3 +20,21 @@ async def yaml_import(file: UploadFile, db: Session = Depends(get_db)):
|
||||
yaml_str = content.decode("utf-8")
|
||||
result = import_from_yaml(yaml_str, db)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/backup")
|
||||
def backup_export(db: Session = Depends(get_db)):
|
||||
content = export_backup(db)
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/json",
|
||||
headers={"Content-Disposition": "attachment; filename=mailfilter-backup.json"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/backup")
|
||||
async def backup_import(file: UploadFile, db: Session = Depends(get_db)):
|
||||
content = await file.read()
|
||||
json_str = content.decode("utf-8")
|
||||
result = import_backup(json_str, db)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user