security: enforce actor-aware Mail profile access
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -41,24 +40,27 @@ from govoplan_core.core.change_sequence import (
|
||||
sequence_watermark_is_expired,
|
||||
)
|
||||
from govoplan_core.core.pagination import KeysetCursorError, decode_keyset_cursor, encode_keyset_cursor, keyset_query_fingerprint
|
||||
from govoplan_core.db.session import get_database, get_session
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_mail.backend.mailbox_index import (
|
||||
begin_mailbox_refresh,
|
||||
cache_mailbox_folders,
|
||||
cache_mailbox_messages,
|
||||
cached_mailbox_folders,
|
||||
cached_mailbox_message_page,
|
||||
finish_mailbox_refresh,
|
||||
clear_mailbox_index,
|
||||
)
|
||||
from govoplan_mail.backend.mail_profiles import (
|
||||
MailProfileError,
|
||||
campaign_mail_context_visible_to_actor,
|
||||
create_mail_server_profile,
|
||||
delete_mail_profile_credentials,
|
||||
effective_mail_profile_policy_for_scope,
|
||||
get_mail_profile_policy,
|
||||
get_mail_server_profile,
|
||||
get_mail_server_profile_for_actor,
|
||||
imap_config_from_profile,
|
||||
list_mail_server_profiles,
|
||||
mail_profile_visible_to_actor,
|
||||
mail_profile_scope_visible_to_actor,
|
||||
parent_mail_profile_policy,
|
||||
profile_response_payload,
|
||||
set_mail_profile_policy,
|
||||
@@ -71,7 +73,6 @@ from govoplan_mail.backend.sending.imap import ImapAppendError, ImapConfiguratio
|
||||
from govoplan_mail.backend.sending.smtp import test_smtp_login
|
||||
|
||||
router = APIRouter(prefix="/mail", tags=["mail"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAIL_MODULE_ID = "mail"
|
||||
MAIL_PROFILES_COLLECTION = "mail.profiles"
|
||||
@@ -145,6 +146,55 @@ def _require_mailbox_read_scope(principal: ApiPrincipal) -> None:
|
||||
_require_scope(principal, "mail:profile:use")
|
||||
|
||||
|
||||
def _principal_is_tenant_admin(principal: ApiPrincipal) -> bool:
|
||||
return has_scope(principal, "tenant:*")
|
||||
|
||||
|
||||
def _principal_can_manage_tenant_profiles(principal: ApiPrincipal) -> bool:
|
||||
return has_scope(principal, "mail:profile:write")
|
||||
|
||||
|
||||
def _principal_can_manage_system_profiles(principal: ApiPrincipal) -> bool:
|
||||
return has_scope(principal, "system:settings:write")
|
||||
|
||||
|
||||
def _profile_actor_kwargs(
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
administrative_visibility: bool,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"actor_user_id": principal.user.id,
|
||||
"actor_group_ids": principal.group_ids,
|
||||
"actor_can_manage_tenant_profiles": _principal_can_manage_tenant_profiles(principal),
|
||||
"actor_can_manage_system_profiles": _principal_can_manage_system_profiles(principal),
|
||||
"actor_tenant_admin": _principal_is_tenant_admin(principal),
|
||||
"actor_administrative_visibility": administrative_visibility,
|
||||
}
|
||||
|
||||
|
||||
def _get_profile_for_principal(
|
||||
session: Session,
|
||||
*,
|
||||
principal: ApiPrincipal,
|
||||
profile_id: str,
|
||||
require_active: bool = False,
|
||||
administrative_visibility: bool = False,
|
||||
):
|
||||
return get_mail_server_profile_for_actor(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=principal.group_ids,
|
||||
can_manage_tenant_profiles=_principal_can_manage_tenant_profiles(principal),
|
||||
can_manage_system_profiles=_principal_can_manage_system_profiles(principal),
|
||||
tenant_admin=_principal_is_tenant_admin(principal),
|
||||
administrative_visibility=administrative_visibility,
|
||||
require_active=require_active,
|
||||
)
|
||||
|
||||
|
||||
def _require_policy_read_scope(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
if scope_type == "system":
|
||||
_require_scope(principal, "system:settings:read")
|
||||
@@ -163,6 +213,86 @@ def _require_policy_write_scope(principal: ApiPrincipal, scope_type: str) -> Non
|
||||
_require_any_scope(principal, "admin:policies:write", "mail:profile:write")
|
||||
|
||||
|
||||
def _require_policy_scope_visibility(
|
||||
session: Session,
|
||||
*,
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None,
|
||||
) -> None:
|
||||
if scope_type in {"system", "tenant"}:
|
||||
return
|
||||
if has_scope(principal, "admin:policies:read"):
|
||||
return
|
||||
if not mail_profile_scope_visible_to_actor(
|
||||
session,
|
||||
scope_type=scope_type,
|
||||
scope_id=scope_id,
|
||||
profile_tenant_id=principal.tenant_id,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=principal.group_ids,
|
||||
can_manage_tenant_profiles=_principal_can_manage_tenant_profiles(principal),
|
||||
tenant_admin=_principal_is_tenant_admin(principal),
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Mail profile policy not found")
|
||||
|
||||
|
||||
def _require_campaign_context_visibility(
|
||||
session: Session,
|
||||
*,
|
||||
principal: ApiPrincipal,
|
||||
campaign_id: str | None,
|
||||
) -> None:
|
||||
if not campaign_id:
|
||||
return
|
||||
if not campaign_mail_context_visible_to_actor(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=principal.group_ids,
|
||||
tenant_admin=_principal_is_tenant_admin(principal),
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Campaign not found")
|
||||
|
||||
|
||||
def _require_policy_campaign_context(
|
||||
session: Session,
|
||||
*,
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None,
|
||||
campaign_id: str | None,
|
||||
) -> None:
|
||||
if scope_type == "campaign" and campaign_id and campaign_id != scope_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail="campaign_id must match the campaign policy scope_id",
|
||||
)
|
||||
_require_campaign_context_visibility(
|
||||
session,
|
||||
principal=principal,
|
||||
campaign_id=campaign_id or (scope_id if scope_type == "campaign" else None),
|
||||
)
|
||||
|
||||
|
||||
def _require_campaign_profile_mutation_access(
|
||||
session: Session,
|
||||
*,
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None,
|
||||
) -> None:
|
||||
if scope_type != "campaign":
|
||||
return
|
||||
_require_campaign_context_visibility(
|
||||
session,
|
||||
principal=principal,
|
||||
campaign_id=scope_id,
|
||||
)
|
||||
|
||||
|
||||
def _profile_response(profile) -> MailServerProfileResponse:
|
||||
return MailServerProfileResponse.model_validate(profile_response_payload(profile))
|
||||
|
||||
@@ -283,6 +413,45 @@ def _mail_deleted_item(entry) -> DeltaDeletedItem:
|
||||
)
|
||||
|
||||
|
||||
def _profile_change_visible_to_principal(
|
||||
session: Session,
|
||||
*,
|
||||
entry: ChangeSequenceEntry,
|
||||
principal: ApiPrincipal,
|
||||
) -> bool:
|
||||
try:
|
||||
profile = get_mail_server_profile(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=entry.resource_id,
|
||||
)
|
||||
except MailProfileError:
|
||||
payload = entry.payload if isinstance(entry.payload, dict) else {}
|
||||
return mail_profile_scope_visible_to_actor(
|
||||
session,
|
||||
scope_type=str(payload.get("scope_type") or "tenant"),
|
||||
scope_id=str(payload["scope_id"]) if payload.get("scope_id") else None,
|
||||
profile_tenant_id=entry.tenant_id,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=principal.group_ids,
|
||||
can_manage_tenant_profiles=_principal_can_manage_tenant_profiles(principal),
|
||||
tenant_admin=_principal_is_tenant_admin(principal),
|
||||
)
|
||||
return mail_profile_visible_to_actor(
|
||||
session,
|
||||
profile=profile,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
group_ids=principal.group_ids,
|
||||
can_manage_tenant_profiles=_principal_can_manage_tenant_profiles(principal),
|
||||
can_manage_system_profiles=_principal_can_manage_system_profiles(principal),
|
||||
tenant_admin=_principal_is_tenant_admin(principal),
|
||||
administrative_visibility=True,
|
||||
require_active=False,
|
||||
)
|
||||
|
||||
|
||||
def _mailbox_summary_response(message) -> MailMailboxMessageSummaryResponse:
|
||||
return MailMailboxMessageSummaryResponse(
|
||||
uid=message.uid,
|
||||
@@ -451,65 +620,6 @@ def _mailbox_messages_response(
|
||||
)
|
||||
|
||||
|
||||
def _schedule_mailbox_refresh(
|
||||
background_tasks: BackgroundTasks,
|
||||
*,
|
||||
tenant_id: str,
|
||||
profile_id: str,
|
||||
folder: str,
|
||||
limit: int,
|
||||
offset: int = 0,
|
||||
include_folder_status: bool = False,
|
||||
) -> bool:
|
||||
if not begin_mailbox_refresh(tenant_id, profile_id, folder):
|
||||
return False
|
||||
background_tasks.add_task(
|
||||
_refresh_mailbox_index_task,
|
||||
tenant_id=tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=folder,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_folder_status=include_folder_status,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _refresh_mailbox_index_task(
|
||||
*,
|
||||
tenant_id: str,
|
||||
profile_id: str,
|
||||
folder: str,
|
||||
limit: int,
|
||||
offset: int,
|
||||
include_folder_status: bool,
|
||||
) -> None:
|
||||
try:
|
||||
with get_database().session() as session:
|
||||
imap = _imap_config_for_profile(session, tenant_id=tenant_id, profile_id=profile_id)
|
||||
result = load_imap_mailbox_bootstrap(
|
||||
imap_config=imap,
|
||||
folder=folder,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_folder_status=include_folder_status,
|
||||
)
|
||||
cache_mailbox_folders(session, tenant_id=tenant_id, profile_id=profile_id, result=result.folders)
|
||||
cache_mailbox_messages(session, tenant_id=tenant_id, profile_id=profile_id, result=result.messages)
|
||||
session.commit()
|
||||
except Exception:
|
||||
# Background refresh is opportunistic. Foreground requests will surface
|
||||
# concrete IMAP errors when no usable cache is available.
|
||||
logger.debug(
|
||||
"Background mailbox index refresh failed (profile_id=%r, folder=%r)",
|
||||
profile_id,
|
||||
folder,
|
||||
exc_info=True,
|
||||
)
|
||||
finally:
|
||||
finish_mailbox_refresh(tenant_id, profile_id, folder)
|
||||
|
||||
|
||||
def _cached_folder_selection(folders, requested: str, detected_sent_folder: str | None = None) -> str:
|
||||
names = {folder.name for folder in folders}
|
||||
if requested in names:
|
||||
@@ -521,8 +631,13 @@ def _cached_folder_selection(folders, requested: str, detected_sent_folder: str
|
||||
return folders[0].name if folders else requested
|
||||
|
||||
|
||||
def _imap_config_for_profile(session: Session, *, tenant_id: str, profile_id: str):
|
||||
profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=profile_id, require_active=True)
|
||||
def _imap_config_for_principal(session: Session, *, principal: ApiPrincipal, profile_id: str):
|
||||
profile = _get_profile_for_principal(
|
||||
session,
|
||||
principal=principal,
|
||||
profile_id=profile_id,
|
||||
require_active=True,
|
||||
)
|
||||
imap = imap_config_from_profile(profile)
|
||||
if imap is None:
|
||||
raise MailProfileError("Mail-server profile has no IMAP configuration")
|
||||
@@ -538,7 +653,7 @@ def _parent_mail_profile_policy_payload(session: Session, *, tenant_id: str, sco
|
||||
def _full_mail_settings_delta_response(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
principal: ApiPrincipal,
|
||||
scope_type: str,
|
||||
scope_id: str | None,
|
||||
include_inactive: bool,
|
||||
@@ -546,16 +661,17 @@ def _full_mail_settings_delta_response(
|
||||
) -> MailSettingsDeltaResponse:
|
||||
profiles = list_mail_server_profiles(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
tenant_id=principal.tenant_id,
|
||||
include_inactive=include_inactive,
|
||||
campaign_id=campaign_id,
|
||||
**_profile_actor_kwargs(principal, administrative_visibility=True),
|
||||
)
|
||||
return MailSettingsDeltaResponse(
|
||||
profiles=[_profile_response(profile) for profile in profiles],
|
||||
policy=_policy_response(session, tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id, campaign_id=campaign_id),
|
||||
policy=_policy_response(session, tenant_id=principal.tenant_id, scope_type=scope_type, scope_id=scope_id, campaign_id=campaign_id),
|
||||
changed_sections=["profiles", "policy"],
|
||||
deleted=[],
|
||||
watermark=_mail_settings_watermark(session, tenant_id=tenant_id),
|
||||
watermark=_mail_settings_watermark(session, tenant_id=principal.tenant_id),
|
||||
has_more=False,
|
||||
full=True,
|
||||
)
|
||||
@@ -593,10 +709,23 @@ def mail_settings_delta(
|
||||
clean_scope_type = scope_type.strip().casefold()
|
||||
_require_any_scope(principal, "mail:profile:read", "system:settings:read")
|
||||
_require_policy_read_scope(principal, clean_scope_type)
|
||||
_require_policy_scope_visibility(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=clean_scope_type,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
_require_policy_campaign_context(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=clean_scope_type,
|
||||
scope_id=scope_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
if since is None:
|
||||
return _full_mail_settings_delta_response(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
principal=principal,
|
||||
scope_type=clean_scope_type,
|
||||
scope_id=scope_id,
|
||||
include_inactive=include_inactive,
|
||||
@@ -606,16 +735,22 @@ def mail_settings_delta(
|
||||
if entries is None:
|
||||
return _full_mail_settings_delta_response(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
principal=principal,
|
||||
scope_type=clean_scope_type,
|
||||
scope_id=scope_id,
|
||||
include_inactive=include_inactive,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
visible_profile_entries = [
|
||||
entry
|
||||
for entry in entries
|
||||
if entry.collection == MAIL_PROFILES_COLLECTION
|
||||
and entry.resource_type == MAIL_PROFILE_RESOURCE
|
||||
and _profile_change_visible_to_principal(session, entry=entry, principal=principal)
|
||||
]
|
||||
changed_profile_ids = {
|
||||
entry.resource_id
|
||||
for entry in entries
|
||||
if entry.collection == MAIL_PROFILES_COLLECTION and entry.resource_type == MAIL_PROFILE_RESOURCE
|
||||
for entry in visible_profile_entries
|
||||
}
|
||||
visible_profiles = {
|
||||
profile.id: profile
|
||||
@@ -624,6 +759,7 @@ def mail_settings_delta(
|
||||
tenant_id=principal.tenant_id,
|
||||
include_inactive=include_inactive,
|
||||
campaign_id=campaign_id,
|
||||
**_profile_actor_kwargs(principal, administrative_visibility=True),
|
||||
)
|
||||
if profile.id in changed_profile_ids
|
||||
}
|
||||
@@ -635,10 +771,8 @@ def mail_settings_delta(
|
||||
changed_sections.append("policy")
|
||||
deleted = [
|
||||
_mail_deleted_item(entry)
|
||||
for entry in entries
|
||||
if entry.collection == MAIL_PROFILES_COLLECTION
|
||||
and entry.resource_type == MAIL_PROFILE_RESOURCE
|
||||
and entry.resource_id not in visible_profiles
|
||||
for entry in visible_profile_entries
|
||||
if entry.resource_id not in visible_profiles
|
||||
]
|
||||
return MailSettingsDeltaResponse(
|
||||
profiles=[_profile_response(profile) for profile in visible_profiles.values()],
|
||||
@@ -665,6 +799,7 @@ def list_profiles(
|
||||
tenant_id=principal.tenant_id,
|
||||
include_inactive=include_inactive,
|
||||
campaign_id=campaign_id,
|
||||
**_profile_actor_kwargs(principal, administrative_visibility=True),
|
||||
)
|
||||
return MailServerProfileListResponse(profiles=[_profile_response(profile) for profile in profiles])
|
||||
except MailProfileError as exc:
|
||||
@@ -678,6 +813,12 @@ def create_profile(
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_profile_write_scope(principal, payload.scope_type)
|
||||
_require_campaign_profile_mutation_access(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
)
|
||||
if payload.credentials_supplied():
|
||||
_require_profile_credentials_scope(principal, payload.scope_type)
|
||||
try:
|
||||
@@ -720,7 +861,14 @@ def get_profile(
|
||||
):
|
||||
_require_any_scope(principal, "mail:profile:read", "system:settings:read")
|
||||
try:
|
||||
return _profile_response(get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id))
|
||||
return _profile_response(
|
||||
_get_profile_for_principal(
|
||||
session,
|
||||
principal=principal,
|
||||
profile_id=profile_id,
|
||||
administrative_visibility=True,
|
||||
)
|
||||
)
|
||||
except MailProfileError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
|
||||
@@ -735,6 +883,12 @@ def update_profile(
|
||||
try:
|
||||
profile = get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
_require_profile_write_scope(principal, profile.scope_type or "tenant")
|
||||
_require_campaign_profile_mutation_access(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=profile.scope_type or "tenant",
|
||||
scope_id=profile.scope_id,
|
||||
)
|
||||
if payload.credentials_supplied() or payload.clear_imap:
|
||||
_require_profile_credentials_scope(principal, profile.scope_type or "tenant")
|
||||
smtp_config = payload.smtp_config()
|
||||
@@ -791,6 +945,12 @@ def deactivate_profile(
|
||||
try:
|
||||
profile = get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
_require_profile_write_scope(principal, profile.scope_type or "tenant")
|
||||
_require_campaign_profile_mutation_access(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=profile.scope_type or "tenant",
|
||||
scope_id=profile.scope_id,
|
||||
)
|
||||
_require_profile_credentials_scope(principal, profile.scope_type or "tenant")
|
||||
was_active = bool(profile.is_active)
|
||||
profile.is_active = False
|
||||
@@ -801,6 +961,10 @@ def deactivate_profile(
|
||||
user_id=principal.user.id,
|
||||
api_key_id=principal.api_key_id,
|
||||
)
|
||||
# Deactivation invalidates unauthenticated IMAP profiles too. Keep the
|
||||
# cache deletion in this transaction so a failed audit/change record
|
||||
# cannot leave profile state and cached mailbox data out of sync.
|
||||
clear_mailbox_index(session, profile_id=profile.id)
|
||||
session.add(profile)
|
||||
if was_active or deleted_protocols:
|
||||
_record_mail_change(
|
||||
@@ -839,6 +1003,19 @@ def read_mail_profile_policy(
|
||||
):
|
||||
scope_type = scope_type.strip().casefold()
|
||||
_require_policy_read_scope(principal, scope_type)
|
||||
_require_policy_scope_visibility(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=scope_type,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
_require_policy_campaign_context(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=scope_type,
|
||||
scope_id=scope_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
try:
|
||||
return _policy_response(session, tenant_id=principal.tenant_id, scope_type=scope_type, scope_id=scope_id, campaign_id=campaign_id)
|
||||
except MailProfileError as exc:
|
||||
@@ -855,6 +1032,19 @@ def write_mail_profile_policy(
|
||||
):
|
||||
scope_type = scope_type.strip().casefold()
|
||||
_require_policy_write_scope(principal, scope_type)
|
||||
_require_policy_scope_visibility(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=scope_type,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
_require_policy_campaign_context(
|
||||
session,
|
||||
principal=principal,
|
||||
scope_type=scope_type,
|
||||
scope_id=scope_id,
|
||||
campaign_id=None,
|
||||
)
|
||||
try:
|
||||
set_mail_profile_policy(
|
||||
session,
|
||||
@@ -889,7 +1079,12 @@ def test_profile_smtp(
|
||||
_require_scope(principal, "mail:profile:test")
|
||||
_require_scope(principal, "mail:profile:use")
|
||||
try:
|
||||
profile = get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id, require_active=True)
|
||||
profile = _get_profile_for_principal(
|
||||
session,
|
||||
principal=principal,
|
||||
profile_id=profile_id,
|
||||
require_active=True,
|
||||
)
|
||||
smtp = smtp_config_from_profile(profile)
|
||||
result = test_smtp_login(smtp_config=smtp)
|
||||
return MailConnectionTestResponse(ok=True, protocol="smtp", host=result.host, port=result.port, security=result.security, message="SMTP connection successful.", details={"authenticated": result.authenticated})
|
||||
@@ -908,7 +1103,12 @@ def test_profile_imap(
|
||||
_require_scope(principal, "mail:profile:test")
|
||||
_require_scope(principal, "mail:profile:use")
|
||||
try:
|
||||
profile = get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id, require_active=True)
|
||||
profile = _get_profile_for_principal(
|
||||
session,
|
||||
principal=principal,
|
||||
profile_id=profile_id,
|
||||
require_active=True,
|
||||
)
|
||||
imap = imap_config_from_profile(profile)
|
||||
if imap is None:
|
||||
raise MailProfileError("Mail-server profile has no IMAP configuration")
|
||||
@@ -929,7 +1129,12 @@ def list_profile_imap_folders(
|
||||
_require_scope(principal, "mail:profile:test")
|
||||
_require_scope(principal, "mail:profile:use")
|
||||
try:
|
||||
profile = get_mail_server_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id, require_active=True)
|
||||
profile = _get_profile_for_principal(
|
||||
session,
|
||||
principal=principal,
|
||||
profile_id=profile_id,
|
||||
require_active=True,
|
||||
)
|
||||
imap = imap_config_from_profile(profile)
|
||||
if imap is None:
|
||||
raise MailProfileError("Mail-server profile has no IMAP configuration")
|
||||
@@ -944,7 +1149,6 @@ def list_profile_imap_folders(
|
||||
@router.get("/profiles/{profile_id}/mailbox/folders", response_model=MailImapFolderListResponse)
|
||||
def list_profile_mailbox_folders(
|
||||
profile_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
include_status: bool = Query(default=False),
|
||||
refresh: bool = False,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
@@ -952,18 +1156,10 @@ def list_profile_mailbox_folders(
|
||||
):
|
||||
_require_mailbox_read_scope(principal)
|
||||
try:
|
||||
imap = _imap_config_for_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
imap = _imap_config_for_principal(session, principal=principal, profile_id=profile_id)
|
||||
if not include_status and not refresh:
|
||||
cached = cached_mailbox_folders(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
if cached is not None:
|
||||
refreshing = cached.stale and _schedule_mailbox_refresh(
|
||||
background_tasks,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder="INBOX",
|
||||
limit=DEFAULT_MAILBOX_MESSAGE_LIMIT,
|
||||
include_folder_status=include_status,
|
||||
)
|
||||
if cached is not None and not cached.stale:
|
||||
result = SimpleNamespace(
|
||||
host=imap.host,
|
||||
port=imap.port,
|
||||
@@ -971,7 +1167,7 @@ def list_profile_mailbox_folders(
|
||||
folders=cached.folders,
|
||||
detected_sent_folder=None,
|
||||
)
|
||||
return _mailbox_folder_response(result, from_cache=True, refreshing=refreshing, indexed_at=cached.indexed_at)
|
||||
return _mailbox_folder_response(result, from_cache=True, refreshing=False, indexed_at=cached.indexed_at)
|
||||
result = list_imap_folders(imap_config=imap, include_status=include_status)
|
||||
cache_mailbox_folders(session, tenant_id=principal.tenant_id, profile_id=profile_id, result=result)
|
||||
session.commit()
|
||||
@@ -987,7 +1183,6 @@ def list_profile_mailbox_folders(
|
||||
@router.get("/profiles/{profile_id}/mailbox/bootstrap", response_model=MailMailboxBootstrapResponse)
|
||||
def bootstrap_profile_mailbox(
|
||||
profile_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
folder: str = Query(default="INBOX", min_length=1, max_length=255),
|
||||
limit: int = Query(default=DEFAULT_MAILBOX_MESSAGE_LIMIT, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0, le=100000),
|
||||
@@ -998,10 +1193,10 @@ def bootstrap_profile_mailbox(
|
||||
) -> MailMailboxBootstrapResponse:
|
||||
_require_mailbox_read_scope(principal)
|
||||
try:
|
||||
imap = _imap_config_for_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
imap = _imap_config_for_principal(session, principal=principal, profile_id=profile_id)
|
||||
if not refresh:
|
||||
cached_folders = cached_mailbox_folders(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
if cached_folders is not None:
|
||||
if cached_folders is not None and not cached_folders.stale:
|
||||
selected_folder = _cached_folder_selection(cached_folders.folders, folder)
|
||||
cached_messages = cached_mailbox_message_page(
|
||||
session,
|
||||
@@ -1011,16 +1206,7 @@ def bootstrap_profile_mailbox(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
if cached_messages is not None:
|
||||
refreshing = (cached_folders.stale or cached_messages.stale) and _schedule_mailbox_refresh(
|
||||
background_tasks,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=selected_folder,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_folder_status=include_status,
|
||||
)
|
||||
if cached_messages is not None and not cached_messages.stale:
|
||||
folder_result = SimpleNamespace(
|
||||
host=imap.host,
|
||||
port=imap.port,
|
||||
@@ -1044,7 +1230,7 @@ def bootstrap_profile_mailbox(
|
||||
folders=_mailbox_folder_response(
|
||||
folder_result,
|
||||
from_cache=True,
|
||||
refreshing=refreshing,
|
||||
refreshing=False,
|
||||
indexed_at=cached_folders.indexed_at,
|
||||
),
|
||||
messages=_mailbox_messages_response(
|
||||
@@ -1062,7 +1248,7 @@ def bootstrap_profile_mailbox(
|
||||
full=not cursor_stable,
|
||||
messages=cached_messages.messages,
|
||||
from_cache=True,
|
||||
refreshing=refreshing,
|
||||
refreshing=False,
|
||||
indexed_at=cached_messages.indexed_at,
|
||||
),
|
||||
)
|
||||
@@ -1118,7 +1304,6 @@ def bootstrap_profile_mailbox(
|
||||
@router.get("/profiles/{profile_id}/mailbox/messages", response_model=MailMailboxMessageListResponse)
|
||||
def list_profile_mailbox_messages(
|
||||
profile_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
folder: str = Query(default="INBOX", min_length=1, max_length=255),
|
||||
limit: int | None = Query(default=None, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0, le=100000),
|
||||
@@ -1129,7 +1314,7 @@ def list_profile_mailbox_messages(
|
||||
):
|
||||
_require_mailbox_read_scope(principal)
|
||||
try:
|
||||
imap = _imap_config_for_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
imap = _imap_config_for_principal(session, principal=principal, profile_id=profile_id)
|
||||
effective_limit = _mailbox_cursor_limit(cursor, limit)
|
||||
fingerprint = _mailbox_cursor_fingerprint(tenant_id=principal.tenant_id, profile_id=profile_id, folder=folder, limit=effective_limit)
|
||||
effective_offset, after_uid, expected_uidvalidity, cursor_values = _mailbox_cursor_position(cursor, fingerprint=fingerprint, offset=offset)
|
||||
@@ -1145,15 +1330,7 @@ def list_profile_mailbox_messages(
|
||||
cache_matches_cursor = cached is not None and (
|
||||
cursor is None or (expected_uidvalidity is not None and cached.uidvalidity == expected_uidvalidity)
|
||||
)
|
||||
if cached is not None and cache_matches_cursor:
|
||||
refreshing = cached.stale and _schedule_mailbox_refresh(
|
||||
background_tasks,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=folder,
|
||||
limit=effective_limit,
|
||||
offset=effective_offset,
|
||||
)
|
||||
if cached is not None and cache_matches_cursor and not cached.stale:
|
||||
next_cursor, cursor_stable = _next_mailbox_cursor(
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
@@ -1179,7 +1356,7 @@ def list_profile_mailbox_messages(
|
||||
full=not cursor_stable,
|
||||
messages=cached.messages,
|
||||
from_cache=True,
|
||||
refreshing=refreshing,
|
||||
refreshing=False,
|
||||
indexed_at=cached.indexed_at,
|
||||
)
|
||||
result = list_imap_messages(
|
||||
@@ -1236,7 +1413,7 @@ def get_profile_mailbox_message(
|
||||
):
|
||||
_require_mailbox_read_scope(principal)
|
||||
try:
|
||||
imap = _imap_config_for_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
imap = _imap_config_for_principal(session, principal=principal, profile_id=profile_id)
|
||||
result = get_imap_message(imap_config=imap, folder=folder, uid=message_uid)
|
||||
return MailMailboxMessageResponse(
|
||||
profile_id=profile_id,
|
||||
|
||||
Reference in New Issue
Block a user