Basisimplementierung für Mailversandt.

This commit is contained in:
Frank Agerholm 2026-01-16 20:51:40 +01:00
commit 531b9cb10e
No known key found for this signature in database
14 changed files with 520 additions and 41 deletions

79
app/routers/mail.py Normal file
View file

@ -0,0 +1,79 @@
# app/routers/mail.py
from __future__ import annotations
import base64
from uuid import UUID
from fastapi import APIRouter, UploadFile, File, Form, Depends, status, HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import get_session
from app.schemas import SendTestMailRequest, UploadMailResponse
from app.schema_utils import job_to_schema
from app.task_enqueue import enqueue_job_with_single_task
from app.services import create_job, get_job_with_tasks
from app.job_registry import routing_key
from email import policy
from email.parser import BytesParser
router = APIRouter(prefix="/api/v1/mail", tags=["mail"])
# TODO: vorhandene Auth-Dependency einhängen
async def api_key_auth():
return True
def _guess_domain_from_addresses(addresses: str | None) -> str:
if not addresses:
return "unknown.local"
# nimmt erste Adresse, extrahiert Domain
first = addresses.split(",")[0].strip()
if "@" in first:
return first.split("@")[-1].strip().lower()
return "unknown.local"
@router.post("/send", status_code=status.HTTP_202_ACCEPTED)
async def send_test_mail(
req: SendTestMailRequest,
_=Depends(api_key_auth),
session: AsyncSession = Depends(get_session),
):
if not req.recipients:
raise HTTPException(status_code=400, detail="At least one recipient required.")
domain = req.recipients[0].split("@")[-1].lower()
job = await create_job(
session,
job_type="MailSend",
domain=domain,
payload=req.model_dump(mode="json")
)
job = await get_job_with_tasks(session, job.id)
return job_to_schema(job)
@router.post("/upload", response_model=UploadMailResponse, status_code=status.HTTP_202_ACCEPTED)
async def upload_mail(
mailbox_id: UUID = Form(...),
eml: UploadFile = File(...),
_=Depends(api_key_auth),
session: AsyncSession = Depends(get_session),
):
raw = await eml.read()
# Domain für Job.domain aus To/From ableiten
try:
msg = BytesParser(policy=policy.default).parsebytes(raw)
domain = _guess_domain_from_addresses(msg.get("To")) or _guess_domain_from_addresses(msg.get("From"))
except Exception:
domain = "unknown.local"
payload = {
"mailbox_id": str(mailbox_id),
"raw_eml_b64": base64.b64encode(raw).decode("ascii"),
}
job_id, task_id = await enqueue_job_with_single_task(
session,
job_type="mail.store",
domain=domain,
payload=payload,
task_name="mail.store",
routing_key="mail.store",
)
return UploadMailResponse(id=UUID(task_id), message_id=None)