sprint6+7: permission notifications + audit trail + notification entity filter + mail account permissions + migration 0053
Check Cross-Plugin Imports / check (push) Has been cancelled

This commit is contained in:
Agent Zero
2026-07-29 02:18:17 +02:00
parent 71ed592aa2
commit 88c04286af
7 changed files with 240 additions and 64 deletions
+2 -1
View File
@@ -20,11 +20,12 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
# --- Mail Accounts (F-MAIL-14, F-MAIL-18) ---
class MailAccount(Base, TenantMixin):
class MailAccount(Base, TenantMixin, OwnedMixin):
"""Mail account configuration with encrypted IMAP/SMTP credentials."""
__tablename__ = "mail_accounts"
+93 -61
View File
@@ -19,6 +19,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.core.storage import get_storage_backend
from app.core.visibility import apply_visibility_filter, check_single_entity_access
from app.deps import require_permission
from app.plugins.builtins.mail.models import (
ContactPgpKey,
@@ -139,7 +140,8 @@ async def _resolve_attachment_paths(attachment_ids: list[str]) -> list[dict]:
async def _get_account(
db: AsyncSession, account_id: uuid.UUID, tenant_id: uuid.UUID, user_id: uuid.UUID
db: AsyncSession, account_id: uuid.UUID, tenant_id: uuid.UUID, user_id: uuid.UUID,
is_system_admin: bool = False,
) -> MailAccount:
account = (
await db.execute(
@@ -150,21 +152,12 @@ async def _get_account(
).scalar_one_or_none()
if not account:
raise HTTPException(404, detail={"detail": "Mail account not found", "code": "not_found"})
if account.user_id == user_id:
return account
delegate = (
await db.execute(
select(MailAccountDelegate).where(
and_(
MailAccountDelegate.account_id == account_id,
MailAccountDelegate.delegate_user_id == user_id,
)
)
)
).scalar_one_or_none()
if delegate:
return account
raise HTTPException(403, detail={"detail": "No access to this account", "code": "forbidden"})
has_access = await check_single_entity_access(
db, "mail_account", account_id, user_id, tenant_id, "read", is_system_admin
)
if not has_access:
raise HTTPException(403, detail={"detail": "No access to this account", "code": "forbidden"})
return account
async def _check_send_permission(
@@ -229,20 +222,12 @@ async def list_accounts(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
accounts = (
(
await db.execute(
select(MailAccount).where(
and_(
MailAccount.tenant_id == tenant_id,
or_(MailAccount.user_id == user_id, MailAccount.is_shared),
)
)
)
)
.scalars()
.all()
is_system_admin = current_user.get("is_system_admin", False)
query = select(MailAccount).where(MailAccount.tenant_id == tenant_id)
query = await apply_visibility_filter(
db, query, "mail_account", MailAccount, user_id, tenant_id, is_system_admin
)
accounts = (await db.execute(query)).scalars().all()
return [account_to_response(a) for a in accounts]
@@ -254,6 +239,7 @@ async def create_account(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
account = await create_mail_account(
db, tenant_id=tenant_id, user_id=user_id, data=data.model_dump()
)
@@ -288,8 +274,9 @@ async def update_account(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
await update_mail_account(db, account, data.model_dump(exclude_unset=True))
return account_to_response(account)
@@ -302,9 +289,13 @@ async def delete_account(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
if account.user_id != user_id:
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
has_admin = await check_single_entity_access(
db, "mail_account", acc_id, user_id, tenant_id, "admin", is_system_admin
)
if not has_admin:
raise HTTPException(403, detail={"detail": "Only owner can delete", "code": "forbidden"})
await db.delete(account)
@@ -318,9 +309,13 @@ async def assign_shared_users(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
if account.user_id != user_id:
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
has_admin = await check_single_entity_access(
db, "mail_account", acc_id, user_id, tenant_id, "admin", is_system_admin
)
if not has_admin:
raise HTTPException(
403, detail={"detail": "Only owner can assign users", "code": "forbidden"}
)
@@ -360,9 +355,13 @@ async def create_delegate(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
if account.user_id != user_id:
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
has_admin = await check_single_entity_access(
db, "mail_account", acc_id, user_id, tenant_id, "admin", is_system_admin
)
if not has_admin:
raise HTTPException(
403, detail={"detail": "Only owner can create delegates", "code": "forbidden"}
)
@@ -401,9 +400,13 @@ async def create_send_permission(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
if account.user_id != user_id:
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
has_admin = await check_single_entity_access(
db, "mail_account", acc_id, user_id, tenant_id, "admin", is_system_admin
)
if not has_admin:
raise HTTPException(
403, detail={"detail": "Only owner can grant send permissions", "code": "forbidden"}
)
@@ -434,8 +437,9 @@ async def test_connection(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
password = await get_account_password(account)
try:
import aioimaplib
@@ -470,8 +474,9 @@ async def trigger_imap_sync(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
await _get_account(db, acc_id, tenant_id, user_id)
await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
result = await imap_sync_account(db, acc_id, tenant_id)
return result
@@ -487,8 +492,9 @@ async def list_folders(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
await _get_account(db, acc_id, tenant_id, user_id)
await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
folders = (
(
await db.execute(
@@ -511,8 +517,9 @@ async def create_folder(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(data.account_id, "account_id")
await _get_account(db, acc_id, tenant_id, user_id)
await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
parent_id = _parse_uuid(data.parent_id, "parent_id") if data.parent_id else None
folder = MailFolder(
tenant_id=tenant_id,
@@ -544,6 +551,7 @@ async def update_folder(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
f_id = _parse_uuid(folder_id, "folder_id")
folder = (
await db.execute(
@@ -552,7 +560,7 @@ async def update_folder(
).scalar_one_or_none()
if not folder:
raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
await _get_account(db, folder.account_id, tenant_id, user_id)
await _get_account(db, folder.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
if data.name:
folder.name = data.name
await db.flush()
@@ -567,6 +575,7 @@ async def delete_folder(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
f_id = _parse_uuid(folder_id, "folder_id")
folder = (
await db.execute(
@@ -575,7 +584,7 @@ async def delete_folder(
).scalar_one_or_none()
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)
account = await _get_account(db, folder.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "full")
try:
await imap_delete_folder(db, f_id, tenant_id)
@@ -605,6 +614,7 @@ async def empty_folder(
"""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
f_id = _parse_uuid(folder_id, "folder_id")
folder = (
await db.execute(
@@ -613,7 +623,7 @@ async def empty_folder(
).scalar_one_or_none()
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)
account = await _get_account(db, folder.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "delete")
# Check if this folder IS the Trash folder
@@ -705,6 +715,7 @@ async def sync_folder(
"""Sync a single folder from IMAP server immediately."""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
f_id = _parse_uuid(folder_id, "folder_id")
folder = (
await db.execute(
@@ -713,7 +724,7 @@ async def sync_folder(
).scalar_one_or_none()
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)
account = await _get_account(db, folder.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
result = await mail_services.imap_sync_folder(db, f_id, tenant_id)
await db.flush()
return result
@@ -735,6 +746,7 @@ async def upload_attachment(
"""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
# Read file content and check size
content = await file.read()
@@ -777,8 +789,9 @@ async def send_mail(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(data.account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_send_permission(db, account, user_id)
signature = None
if data.signature_id:
@@ -899,6 +912,7 @@ async def create_template(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
template = MailTemplate(
tenant_id=tenant_id,
user_id=user_id,
@@ -958,6 +972,7 @@ async def create_signature(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(data.account_id, "account_id") if data.account_id else None
sig = MailSignature(
tenant_id=tenant_id,
@@ -1054,8 +1069,9 @@ async def configure_vacation(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(data.account_id, "account_id")
await _get_account(db, acc_id, tenant_id, user_id)
await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
vacation_data = {
"is_enabled": data.is_enabled,
"subject": data.subject,
@@ -1083,8 +1099,9 @@ async def test_vacation_dedup(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(account_id, "account_id")
await _get_account(db, acc_id, tenant_id, user_id)
await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
should_send_1 = await should_send_vacation_reply(db, acc_id, sender, tenant_id)
if should_send_1:
await log_vacation_sent(db, acc_id, sender, tenant_id)
@@ -1107,6 +1124,7 @@ async def import_pgp_key(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
key_id, public_key_armored = import_pgp_private_key(data.private_key_armored, data.passphrase)
encrypted_private = encrypt_password(data.private_key_armored)
pgp_key = PgpKey(
@@ -1127,6 +1145,7 @@ async def list_pgp_keys(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
keys = (
(
await db.execute(
@@ -1178,6 +1197,7 @@ async def create_label(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
label = MailLabel(tenant_id=tenant_id, name=data.name, color=data.color, user_id=user_id)
db.add(label)
await db.flush()
@@ -1265,13 +1285,14 @@ async def reply_mail(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
m_id = _parse_uuid(mail_id, "mail_id")
original = (
await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
).scalar_one_or_none()
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)
account = await _get_account(db, original.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "read")
await _check_send_permission(db, account, user_id)
signature = None
@@ -1311,13 +1332,14 @@ async def forward_mail_endpoint(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
m_id = _parse_uuid(mail_id, "mail_id")
original = (
await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
).scalar_one_or_none()
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)
account = await _get_account(db, original.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "read")
await _check_send_permission(db, account, user_id)
signature = None
@@ -1358,13 +1380,14 @@ async def update_flags(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "write")
if data.is_seen is not None:
mail.is_seen = data.is_seen
@@ -1397,6 +1420,7 @@ async def download_attachment(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
m_id = _parse_uuid(mail_id, "mail_id")
a_id = _parse_uuid(att_id, "att_id")
mail = (
@@ -1404,7 +1428,7 @@ 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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "read")
attachment = (
await db.execute(
@@ -1442,13 +1466,14 @@ async def link_mail(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "write")
if data.contact_id:
mail.contact_id = _parse_uuid(data.contact_id, "contact_id")
@@ -1468,13 +1493,14 @@ async def create_event_from_mail(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "write")
try:
from app.plugins.builtins.calendar.contracts import get_contract as get_calendar_contract
@@ -1517,6 +1543,7 @@ async def assign_label(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
m_id = _parse_uuid(mail_id, "mail_id")
l_id = _parse_uuid(data.label_id, "label_id")
mail = (
@@ -1524,7 +1551,7 @@ 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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "write")
label = (
await db.execute(
@@ -1558,8 +1585,9 @@ async def create_draft(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
acc_id = _parse_uuid(data.account_id, "account_id")
account = await _get_account(db, acc_id, tenant_id, user_id)
account = await _get_account(db, acc_id, tenant_id, user_id, is_system_admin=is_system_admin)
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)
@@ -1574,13 +1602,14 @@ async def update_draft_route(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
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)
@@ -1597,13 +1626,14 @@ async def delete_mail(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "delete")
# Find the Trash folder for this account
@@ -1700,6 +1730,7 @@ async def move_mail(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
m_id = _parse_uuid(mail_id, "mail_id")
target_f_id = _parse_uuid(data.target_folder_id, "target_folder_id")
mail = (
@@ -1707,7 +1738,7 @@ 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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "write")
# Verify target folder exists and belongs to same tenant
target_folder = (
@@ -1790,13 +1821,14 @@ async def get_mail(
):
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
is_system_admin = current_user.get("is_system_admin", False)
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)
account = await _get_account(db, mail.account_id, tenant_id, user_id, is_system_admin=is_system_admin)
await _check_delegate_access(db, account, user_id, "read")
attachments = (
(await db.execute(select(MailAttachment).where(MailAttachment.mail_id == mail.id)))