feat: phase 5 - mail plugin RBAC integration with delegate access levels (read/write/delete/full)

This commit is contained in:
Agent Zero
2026-07-16 00:34:36 +02:00
parent bb6466b53d
commit a4560344c9
4 changed files with 111 additions and 59 deletions
+8 -1
View File
@@ -329,7 +329,14 @@ class MailSeenBy(Base, TenantMixin):
class MailAccountDelegate(Base, TenantMixin):
"""Delegate access to a mail account (read or full)."""
"""Delegate access to a mail account.
access_level values:
- "read" → Mails lesen, suchen, Threads sehen
- "write" → read + Flags setzen, verschieben, Entwürfe speichern
- "delete" → write + Mails löschen
- "full" → delete + Ordner verwalten + ACLs vergeben
"""
__tablename__ = "mail_account_delegates"
__table_args__ = (
+1 -1
View File
@@ -47,7 +47,7 @@ class MailPlugin(BasePlugin):
],
events=[],
migrations=["0001_initial.sql"],
permissions=[],
permissions=["mail:read", "mail:send", "mail:config", "mail:share", "mail:write", "mail:delete"],
)
async def on_activate(
+98 -57
View File
@@ -18,7 +18,7 @@ from sqlalchemy import and_, asc, desc, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.deps import get_current_user
from app.deps import require_permission
from app.plugins.builtins.mail.models import (
ContactPgpKey,
Mail,
@@ -185,11 +185,15 @@ async def _check_send_permission(
raise HTTPException(403, detail={"detail": "No send permission", "code": "forbidden"})
async def _check_delegate_full_access(
db: AsyncSession, account: MailAccount, user_id: uuid.UUID
_LEVEL_HIERARCHY = {"read": 1, "write": 2, "delete": 3, "full": 4}
async def _check_delegate_access(
db: AsyncSession, account: MailAccount, user_id: uuid.UUID, required_level: str = "read"
) -> None:
"""Check if user has delegate access to account with at least the required level."""
if account.user_id == user_id:
return
return # Owner has full access
delegate = (
await db.execute(
select(MailAccountDelegate).where(
@@ -201,10 +205,13 @@ async def _check_delegate_full_access(
)
).scalar_one_or_none()
if not delegate:
raise HTTPException(403, detail={"detail": "No delegate access", "code": "forbidden"})
if delegate.access_level != "full":
raise HTTPException(403, detail={"detail": "No delegate access to this mailbox", "code": "forbidden"})
user_level = _LEVEL_HIERARCHY.get(delegate.access_level, 0)
required = _LEVEL_HIERARCHY.get(required_level, 1)
if user_level < required:
raise HTTPException(
403, detail={"detail": "Read-only delegate access", "code": "forbidden"}
403,
detail={"detail": f"Insufficient delegate access (need {required_level}, have {delegate.access_level})", "code": "forbidden"}
)
@@ -217,7 +224,7 @@ async def _check_delegate_full_access(
@router.get("/accounts")
async def list_accounts(
db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -242,7 +249,7 @@ async def list_accounts(
async def create_account(
data: MailAccountCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -254,7 +261,7 @@ async def create_account(
@router.get("/accounts/shared")
async def list_shared_accounts(
db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
accounts = (
@@ -276,7 +283,7 @@ async def update_account(
account_id: str,
data: MailAccountUpdate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -290,7 +297,7 @@ async def update_account(
async def delete_account(
account_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -306,7 +313,7 @@ async def assign_shared_users(
account_id: str,
data: SharedMailboxUserAssign,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:share")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -348,7 +355,7 @@ async def create_delegate(
account_id: str,
data: DelegateCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:share")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -389,7 +396,7 @@ async def create_send_permission(
account_id: str,
data: SendPermissionCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:share")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -422,7 +429,7 @@ async def create_send_permission(
async def test_connection(
account_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -458,7 +465,7 @@ async def test_connection(
async def trigger_imap_sync(
account_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -475,7 +482,7 @@ async def trigger_imap_sync(
async def list_folders(
account_id: str = Query(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:read")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -499,7 +506,7 @@ async def list_folders(
async def create_folder(
data: MailFolderCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -532,7 +539,7 @@ async def update_folder(
folder_id: str,
data: MailFolderUpdate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -555,7 +562,7 @@ async def update_folder(
async def delete_folder(
folder_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -568,7 +575,7 @@ async def delete_folder(
if not folder:
raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
account = await _get_account(db, folder.account_id, tenant_id, user_id)
await _check_delegate_full_access(db, account, user_id)
await _check_delegate_access(db, account, user_id, "full")
try:
await imap_delete_folder(db, f_id, tenant_id)
except Exception:
@@ -587,7 +594,7 @@ async def delete_folder(
async def upload_attachment(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
"""Upload a file to be attached to an outgoing email.
@@ -643,7 +650,7 @@ async def upload_attachment(
async def send_mail(
data: MailSendRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:send")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -708,7 +715,7 @@ async def search_mails(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:read")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
pattern = f"%{q}%"
@@ -739,7 +746,7 @@ async def search_mails(
async def list_threads(
account_id: str | None = None,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:read")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
stmt = select(Mail).where(Mail.tenant_id == tenant_id)
@@ -764,7 +771,7 @@ async def list_threads(
async def create_template(
data: MailTemplateCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -782,7 +789,7 @@ async def create_template(
@router.get("/templates")
async def list_templates(
db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
templates = (
@@ -797,7 +804,7 @@ async def list_templates(
async def substitute_template(
data: TemplateSubstituteRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:read")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
tpl_id = _parse_uuid(data.template_id, "template_id")
@@ -823,7 +830,7 @@ async def substitute_template(
async def create_signature(
data: MailSignatureCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -843,7 +850,7 @@ async def create_signature(
@router.get("/signatures")
async def list_signatures(
db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
sigs = (
@@ -861,7 +868,7 @@ async def list_signatures(
async def create_rule(
data: MailRuleCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
acc_id = _parse_uuid(data.account_id, "account_id") if data.account_id else None
@@ -881,7 +888,7 @@ async def create_rule(
@router.get("/rules")
async def list_rules(
db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
rules = (
@@ -898,7 +905,7 @@ async def list_rules(
@router.delete("/rules/{rule_id}", status_code=204)
async def delete_rule(
rule_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
rule_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:config"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
r_id = _parse_uuid(rule_id, "rule_id")
@@ -919,7 +926,7 @@ async def delete_rule(
async def configure_vacation(
data: VacationConfig,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -948,7 +955,7 @@ async def test_vacation_dedup(
account_id: str = Query(...),
sender: str = Query(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -972,7 +979,7 @@ async def test_vacation_dedup(
async def import_pgp_key(
data: PgpKeyImport,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -992,7 +999,7 @@ async def import_pgp_key(
@router.get("/pgp/keys")
async def list_pgp_keys(
db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -1015,7 +1022,7 @@ async def list_pgp_keys(
async def encrypt_message(
data: dict = Body(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:send")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
contact_id = (
@@ -1043,7 +1050,7 @@ async def encrypt_message(
async def create_label(
data: MailLabelCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -1055,7 +1062,7 @@ async def create_label(
@router.get("/labels")
async def list_labels(
db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
labels = (
@@ -1074,7 +1081,7 @@ async def store_contact_pgp_key(
contact_id: str,
data: dict = Body(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:config")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
c_id = _parse_uuid(contact_id, "contact_id")
@@ -1109,7 +1116,7 @@ async def store_contact_pgp_key(
@router.post("/{mail_id}/apply-rules")
async def apply_rules(
mail_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
mail_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:config"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
m_id = _parse_uuid(mail_id, "mail_id")
@@ -1130,7 +1137,7 @@ async def reply_mail(
mail_id: str,
data: MailReplyRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:send")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -1141,6 +1148,7 @@ async def reply_mail(
if not original:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, original.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "read")
await _check_send_permission(db, account, user_id)
signature = None
if data.signature_id:
@@ -1175,7 +1183,7 @@ async def forward_mail_endpoint(
mail_id: str,
data: MailForwardRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:send")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -1186,6 +1194,7 @@ async def forward_mail_endpoint(
if not original:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, original.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "read")
await _check_send_permission(db, account, user_id)
signature = None
if data.signature_id:
@@ -1221,15 +1230,18 @@ async def update_flags(
mail_id: str,
data: MailFlagsUpdate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
mail = (
await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "write")
if data.is_seen is not None:
mail.is_seen = data.is_seen
if data.is_flagged is not None:
@@ -1255,9 +1267,10 @@ async def download_attachment(
mail_id: str,
att_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:read")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
a_id = _parse_uuid(att_id, "att_id")
mail = (
@@ -1265,6 +1278,8 @@ async def download_attachment(
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "read")
attachment = (
await db.execute(
select(MailAttachment).where(
@@ -1300,15 +1315,18 @@ async def link_mail(
mail_id: str,
data: MailLinkRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
mail = (
await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "write")
if data.contact_id:
mail.contact_id = _parse_uuid(data.contact_id, "contact_id")
if data.company_id:
@@ -1326,7 +1344,7 @@ async def create_event_from_mail(
mail_id: str,
data: MailCreateEventRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
@@ -1336,6 +1354,8 @@ async def create_event_from_mail(
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "write")
try:
from app.plugins.builtins.calendar.models import Calendar, CalendarEntry
except ImportError:
@@ -1370,9 +1390,10 @@ async def assign_label(
mail_id: str,
data: MailLabelAssign,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
l_id = _parse_uuid(data.label_id, "label_id")
mail = (
@@ -1380,6 +1401,8 @@ async def assign_label(
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "write")
label = (
await db.execute(
select(MailLabel).where(and_(MailLabel.id == l_id, MailLabel.tenant_id == tenant_id))
@@ -1408,12 +1431,13 @@ async def assign_label(
async def create_draft(
data: MailDraftCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
acc_id = _parse_uuid(data.account_id, "account_id")
await _get_account(db, acc_id, tenant_id, user_id)
account = await _get_account(db, acc_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "write")
mail = await save_draft(db, acc_id, tenant_id, user_id, data.model_dump())
return mail_to_response(mail)
@@ -1423,10 +1447,18 @@ async def update_draft_route(
mail_id: str,
data: MailDraftCreate,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
mail = (
await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "write")
mail = await update_draft(db, m_id, tenant_id, data.model_dump())
return mail_to_response(mail)
@@ -1438,15 +1470,18 @@ async def update_draft_route(
async def delete_mail(
mail_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:delete")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
mail = (
await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "delete")
# Soft-delete: set deleted_at = now()
mail.deleted_at = datetime.now(UTC)
await db.flush()
@@ -1463,9 +1498,10 @@ async def move_mail(
mail_id: str,
data: MailMoveRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:write")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
target_f_id = _parse_uuid(data.target_folder_id, "target_folder_id")
mail = (
@@ -1473,6 +1509,8 @@ async def move_mail(
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "write")
# Verify target folder exists and belongs to same tenant
target_folder = (
await db.execute(
@@ -1509,7 +1547,7 @@ async def list_mails(
sort_by: str = Query("date", pattern="^(date|from|subject)$"),
sort_order: str = Query("desc", pattern="^(asc|desc)$"),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
current_user: dict = Depends(require_permission("mail:read")),
):
tenant_id = uuid.UUID(current_user["tenant_id"])
stmt = select(Mail).where(Mail.tenant_id == tenant_id)
@@ -1542,15 +1580,18 @@ async def list_mails(
@router.get("/{mail_id}")
async def get_mail(
mail_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)
mail_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
m_id = _parse_uuid(mail_id, "mail_id")
mail = (
await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
).scalar_one_or_none()
if not mail:
raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
account = await _get_account(db, mail.account_id, tenant_id, user_id)
await _check_delegate_access(db, account, user_id, "read")
attachments = (
(await db.execute(select(MailAttachment).where(MailAttachment.mail_id == mail.id)))
.scalars()