security: enforce actor-aware Mail profile access

This commit is contained in:
2026-07-21 17:51:25 +02:00
parent 9d1d9bfb58
commit 06e0fbd3c3
7 changed files with 1336 additions and 158 deletions

View File

@@ -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,