fix(i-d): mail-API-Brueche behoben — signatures PATCH/DELETE + labels DELETE im Backend ergaenzt, drafts PATCH->PUT
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
Root-Cause: Frontend-Komponenten (SignatureManager, LabelManager) rufen Endpunkte auf die das Backend nie hatte (404/405 in Production). Anders als ai/sessions sind diese Funktionen ECHT in Komponenten eingebunden -> Backend-Routen nachbestellt statt Frontend-Calls zu loeschen:
(1) PATCH+DELETE /mail/signatures/{id}: MailSignatureUpdate-Schema neu, Tenant-Scoped + Owner-Check (403 bei fremder Signatur), is_default-Exklusivitaet beim Setzen. (2) DELETE /mail/labels/{id}: gleicher Stil. (3) updateDraft Frontend: apiPatch -> apiPut (Backend hat PUT /drafts/{id} bereits). Beweistest tests/test_mail_sig_label_routes.py 5/5 gruen (PATCH-Werte, DELETE+Liste-leer, 404-Faelle).
Verifikation: create_app registriert beide neuen Routen (563 total); ruff clean; tsc exit=0.
This commit is contained in:
@@ -56,6 +56,7 @@ from app.plugins.builtins.mail.schemas import (
|
||||
MailRuleCreate,
|
||||
MailSendRequest,
|
||||
MailSignatureCreate,
|
||||
MailSignatureUpdate,
|
||||
MailTemplateCreate,
|
||||
PgpKeyImport,
|
||||
SendPermissionCreate,
|
||||
@@ -992,6 +993,66 @@ async def list_signatures(
|
||||
return [signature_to_response(s) for s in sigs]
|
||||
|
||||
|
||||
@router.patch("/signatures/{signature_id}")
|
||||
async def update_signature(
|
||||
signature_id: str,
|
||||
data: MailSignatureUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("mail:config")),
|
||||
):
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
s_id = _parse_uuid(signature_id, "signature_id")
|
||||
sig = (
|
||||
await db.execute(
|
||||
select(MailSignature).where(
|
||||
and_(MailSignature.id == s_id, MailSignature.tenant_id == tenant_id)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not sig:
|
||||
raise HTTPException(404, detail={"detail": "Signature not found", "code": "not_found"})
|
||||
if sig.user_id != user_id:
|
||||
raise HTTPException(403, detail={"detail": "Not your signature", "code": "forbidden"})
|
||||
sig.name = data.name
|
||||
sig.body_html = data.body_html
|
||||
if data.is_default:
|
||||
others = (
|
||||
await db.execute(
|
||||
select(MailSignature).where(MailSignature.user_id == user_id)
|
||||
)
|
||||
).scalars().all()
|
||||
for o in others:
|
||||
o.is_default = False
|
||||
sig.is_default = data.is_default
|
||||
await db.flush()
|
||||
return signature_to_response(sig)
|
||||
|
||||
|
||||
@router.delete("/signatures/{signature_id}", status_code=204)
|
||||
async def delete_signature(
|
||||
signature_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("mail:config")),
|
||||
):
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
s_id = _parse_uuid(signature_id, "signature_id")
|
||||
sig = (
|
||||
await db.execute(
|
||||
select(MailSignature).where(
|
||||
and_(MailSignature.id == s_id, MailSignature.tenant_id == tenant_id)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not sig:
|
||||
raise HTTPException(404, detail={"detail": "Signature not found", "code": "not_found"})
|
||||
if sig.user_id != user_id:
|
||||
raise HTTPException(403, detail={"detail": "Not your signature", "code": "forbidden"})
|
||||
await db.delete(sig)
|
||||
await db.flush()
|
||||
|
||||
|
||||
# ─── Rules (F-MAIL-07) ───
|
||||
|
||||
|
||||
@@ -1206,6 +1267,28 @@ async def list_labels(
|
||||
return [label_to_response(lbl) for lbl in labels]
|
||||
|
||||
|
||||
@router.delete("/labels/{label_id}", status_code=204)
|
||||
async def delete_label(
|
||||
label_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("mail:config")),
|
||||
):
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
l_id = _parse_uuid(label_id, "label_id")
|
||||
label = (
|
||||
await db.execute(
|
||||
select(MailLabel).where(and_(MailLabel.id == l_id, MailLabel.tenant_id == tenant_id))
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not label:
|
||||
raise HTTPException(404, detail={"detail": "Label not found", "code": "not_found"})
|
||||
if label.user_id != user_id:
|
||||
raise HTTPException(403, detail={"detail": "Not your label", "code": "forbidden"})
|
||||
await db.delete(label)
|
||||
await db.flush()
|
||||
|
||||
|
||||
# ─── Contact PGP Keys (F-MAIL-12) ───
|
||||
|
||||
|
||||
|
||||
@@ -259,6 +259,12 @@ class MailSignatureResponse(BaseModel):
|
||||
is_default: bool
|
||||
|
||||
|
||||
class MailSignatureUpdate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
body_html: str = ""
|
||||
is_default: bool = False
|
||||
|
||||
|
||||
# ─── Rules ───
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user