intermittent commit
This commit is contained in:
@@ -1,13 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
import dataclasses
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_mail.backend.schemas import (
|
||||
MailAddressLookupCandidate,
|
||||
MailAddressLookupResponse,
|
||||
MailConnectionTestResponse,
|
||||
MailImapFolderListResponse,
|
||||
MailImapFolderResponse,
|
||||
MailMailboxBootstrapResponse,
|
||||
MailImapTestRequest,
|
||||
MailMailboxAttachmentResponse,
|
||||
MailMailboxMessageDetailResponse,
|
||||
@@ -33,7 +40,15 @@ 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_session
|
||||
from govoplan_core.db.session import get_database, 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,
|
||||
)
|
||||
from govoplan_mail.backend.mail_profiles import (
|
||||
MailProfileError,
|
||||
create_mail_server_profile,
|
||||
@@ -49,7 +64,8 @@ from govoplan_mail.backend.mail_profiles import (
|
||||
update_mail_server_profile,
|
||||
)
|
||||
from govoplan_mail.backend.config import ImapConfig, SmtpConfig
|
||||
from govoplan_mail.backend.sending.imap import ImapAppendError, ImapConfigurationError, get_imap_message, list_imap_folders, list_imap_messages, test_imap_login
|
||||
from govoplan_mail.backend.runtime import get_registry
|
||||
from govoplan_mail.backend.sending.imap import ImapAppendError, ImapConfigurationError, get_imap_message, list_imap_folders, list_imap_messages, load_imap_mailbox_bootstrap, test_imap_login
|
||||
from govoplan_mail.backend.sending.smtp import test_smtp_login
|
||||
|
||||
router = APIRouter(prefix="/mail", tags=["mail"])
|
||||
@@ -62,6 +78,39 @@ MAIL_PROFILE_RESOURCE = "mail_profile"
|
||||
MAIL_POLICY_RESOURCE = "mail_profile_policy"
|
||||
MAILBOX_MESSAGES_CURSOR_SCOPE = "mail.mailbox.messages.v1"
|
||||
DEFAULT_MAILBOX_MESSAGE_LIMIT = 50
|
||||
CAPABILITY_ADDRESSES_LOOKUP = "addresses.lookup"
|
||||
|
||||
|
||||
def _capability_payload(value: object) -> dict[str, Any]:
|
||||
if dataclasses.is_dataclass(value):
|
||||
return dataclasses.asdict(value)
|
||||
if isinstance(value, dict):
|
||||
return dict(value)
|
||||
payload: dict[str, Any] = {}
|
||||
for key in (
|
||||
"contact_id",
|
||||
"address_book_id",
|
||||
"display_name",
|
||||
"email",
|
||||
"email_label",
|
||||
"organization",
|
||||
"role_title",
|
||||
"tags",
|
||||
"source_kind",
|
||||
"source_ref",
|
||||
"source_revision",
|
||||
"provenance",
|
||||
):
|
||||
if hasattr(value, key):
|
||||
payload[key] = getattr(value, key)
|
||||
return payload
|
||||
|
||||
|
||||
def _registry_capability(name: str) -> object | None:
|
||||
registry = get_registry()
|
||||
if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(name):
|
||||
return None
|
||||
return registry.capability(name)
|
||||
|
||||
|
||||
def _require_scope(principal: ApiPrincipal, scope: str) -> None:
|
||||
@@ -338,6 +387,132 @@ def _next_mailbox_cursor(
|
||||
)
|
||||
|
||||
|
||||
def _mailbox_folder_response(
|
||||
result,
|
||||
*,
|
||||
from_cache: bool = False,
|
||||
refreshing: bool = False,
|
||||
indexed_at=None,
|
||||
) -> MailImapFolderListResponse:
|
||||
folders = [MailImapFolderResponse(name=item.name, flags=item.flags, message_count=item.message_count, unseen_count=item.unseen_count) for item in result.folders]
|
||||
return MailImapFolderListResponse(
|
||||
ok=True,
|
||||
host=result.host,
|
||||
port=result.port,
|
||||
security=result.security,
|
||||
message=f"Found {len(folders)} IMAP folder(s).",
|
||||
folders=folders,
|
||||
detected_sent_folder=result.detected_sent_folder,
|
||||
from_cache=from_cache,
|
||||
refreshing=refreshing,
|
||||
indexed_at=indexed_at,
|
||||
)
|
||||
|
||||
|
||||
def _mailbox_messages_response(
|
||||
*,
|
||||
profile_id: str,
|
||||
folder: str,
|
||||
host: str | None,
|
||||
port: int | None,
|
||||
security: str | None,
|
||||
total_count: int,
|
||||
offset: int,
|
||||
limit: int,
|
||||
cursor: str | None,
|
||||
next_cursor: str | None,
|
||||
cursor_stable: bool,
|
||||
full: bool,
|
||||
messages,
|
||||
from_cache: bool = False,
|
||||
refreshing: bool = False,
|
||||
indexed_at=None,
|
||||
) -> MailMailboxMessageListResponse:
|
||||
return MailMailboxMessageListResponse(
|
||||
profile_id=profile_id,
|
||||
folder=folder,
|
||||
host=host,
|
||||
port=port,
|
||||
security=security,
|
||||
total_count=total_count,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
cursor=cursor,
|
||||
next_cursor=next_cursor,
|
||||
cursor_stable=cursor_stable,
|
||||
full=full,
|
||||
from_cache=from_cache,
|
||||
refreshing=refreshing,
|
||||
indexed_at=indexed_at,
|
||||
messages=[_mailbox_summary_response(message) for message in messages],
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
pass
|
||||
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:
|
||||
return requested
|
||||
if "INBOX" in names:
|
||||
return "INBOX"
|
||||
if detected_sent_folder and detected_sent_folder in names:
|
||||
return detected_sent_folder
|
||||
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)
|
||||
imap = imap_config_from_profile(profile)
|
||||
@@ -378,6 +553,24 @@ def _full_mail_settings_delta_response(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/address-lookup", response_model=MailAddressLookupResponse)
|
||||
def lookup_mail_addresses(
|
||||
query: str = Query(min_length=1),
|
||||
limit: int = Query(default=25, ge=1, le=100),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> MailAddressLookupResponse:
|
||||
_require_scope(principal, "mail:profile:use")
|
||||
capability = _registry_capability(CAPABILITY_ADDRESSES_LOOKUP)
|
||||
if capability is None or not hasattr(capability, "lookup"):
|
||||
return MailAddressLookupResponse(available=False, candidates=[])
|
||||
candidates = getattr(capability, "lookup")(session, principal, query=query, limit=limit)
|
||||
return MailAddressLookupResponse(
|
||||
available=True,
|
||||
candidates=[MailAddressLookupCandidate.model_validate(_capability_payload(candidate)) for candidate in candidates],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/settings/delta", response_model=MailSettingsDeltaResponse)
|
||||
def mail_settings_delta(
|
||||
scope_type: str = Query(default="tenant"),
|
||||
@@ -711,8 +904,7 @@ def list_profile_imap_folders(
|
||||
if imap is None:
|
||||
raise MailProfileError("Mail-server profile has no IMAP configuration")
|
||||
result = list_imap_folders(imap_config=imap)
|
||||
folders = [MailImapFolderResponse(name=item.name, flags=item.flags, message_count=item.message_count, unseen_count=item.unseen_count) for item in result.folders]
|
||||
return MailImapFolderListResponse(ok=True, host=result.host, port=result.port, security=result.security, message=f"Found {len(folders)} IMAP folder(s).", folders=folders, detected_sent_folder=result.detected_sent_folder)
|
||||
return _mailbox_folder_response(result)
|
||||
except MailProfileError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
@@ -722,15 +914,169 @@ 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),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_mailbox_read_scope(principal)
|
||||
try:
|
||||
imap = _imap_config_for_profile(session, tenant_id=principal.tenant_id, profile_id=profile_id)
|
||||
result = list_imap_folders(imap_config=imap)
|
||||
folders = [MailImapFolderResponse(name=item.name, flags=item.flags, message_count=item.message_count, unseen_count=item.unseen_count) for item in result.folders]
|
||||
return MailImapFolderListResponse(ok=True, host=result.host, port=result.port, security=result.security, message=f"Found {len(folders)} IMAP folder(s).", folders=folders, detected_sent_folder=result.detected_sent_folder)
|
||||
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,
|
||||
)
|
||||
result = SimpleNamespace(
|
||||
host=imap.host,
|
||||
port=imap.port,
|
||||
security=imap.security.value,
|
||||
folders=cached.folders,
|
||||
detected_sent_folder=None,
|
||||
)
|
||||
return _mailbox_folder_response(result, from_cache=True, refreshing=refreshing, 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()
|
||||
return _mailbox_folder_response(result)
|
||||
except MailProfileError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ImapConfigurationError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
||||
except ImapAppendError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@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),
|
||||
refresh: bool = False,
|
||||
include_status: bool = Query(default=False),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
) -> MailMailboxBootstrapResponse:
|
||||
_require_mailbox_read_scope(principal)
|
||||
try:
|
||||
imap = _imap_config_for_profile(session, tenant_id=principal.tenant_id, 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:
|
||||
selected_folder = _cached_folder_selection(cached_folders.folders, folder)
|
||||
cached_messages = cached_mailbox_message_page(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=selected_folder,
|
||||
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,
|
||||
)
|
||||
folder_result = SimpleNamespace(
|
||||
host=imap.host,
|
||||
port=imap.port,
|
||||
security=imap.security.value,
|
||||
folders=cached_folders.folders,
|
||||
detected_sent_folder=None,
|
||||
)
|
||||
next_cursor, cursor_stable = _next_mailbox_cursor(
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=cached_messages.folder,
|
||||
limit=cached_messages.limit,
|
||||
offset=cached_messages.offset,
|
||||
total_count=cached_messages.total_count,
|
||||
uidvalidity=cached_messages.uidvalidity,
|
||||
messages=cached_messages.messages,
|
||||
)
|
||||
return MailMailboxBootstrapResponse(
|
||||
profile_id=profile_id,
|
||||
folder=cached_messages.folder,
|
||||
folders=_mailbox_folder_response(
|
||||
folder_result,
|
||||
from_cache=True,
|
||||
refreshing=refreshing,
|
||||
indexed_at=cached_folders.indexed_at,
|
||||
),
|
||||
messages=_mailbox_messages_response(
|
||||
profile_id=profile_id,
|
||||
folder=cached_messages.folder,
|
||||
host=imap.host,
|
||||
port=imap.port,
|
||||
security=imap.security.value,
|
||||
total_count=cached_messages.total_count,
|
||||
offset=cached_messages.offset,
|
||||
limit=cached_messages.limit,
|
||||
cursor=None,
|
||||
next_cursor=next_cursor,
|
||||
cursor_stable=cursor_stable,
|
||||
full=not cursor_stable,
|
||||
messages=cached_messages.messages,
|
||||
from_cache=True,
|
||||
refreshing=refreshing,
|
||||
indexed_at=cached_messages.indexed_at,
|
||||
),
|
||||
)
|
||||
|
||||
result = load_imap_mailbox_bootstrap(
|
||||
imap_config=imap,
|
||||
folder=folder,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_folder_status=include_status,
|
||||
)
|
||||
cache_mailbox_folders(session, tenant_id=principal.tenant_id, profile_id=profile_id, result=result.folders)
|
||||
cache_mailbox_messages(session, tenant_id=principal.tenant_id, profile_id=profile_id, result=result.messages)
|
||||
session.commit()
|
||||
next_cursor, cursor_stable = _next_mailbox_cursor(
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=result.messages.folder,
|
||||
limit=result.messages.limit,
|
||||
offset=result.messages.offset,
|
||||
total_count=result.messages.total_count,
|
||||
uidvalidity=result.messages.uidvalidity,
|
||||
messages=result.messages.messages,
|
||||
)
|
||||
return MailMailboxBootstrapResponse(
|
||||
profile_id=profile_id,
|
||||
folder=result.messages.folder,
|
||||
folders=_mailbox_folder_response(result.folders),
|
||||
messages=_mailbox_messages_response(
|
||||
profile_id=profile_id,
|
||||
folder=result.messages.folder,
|
||||
host=result.messages.host,
|
||||
port=result.messages.port,
|
||||
security=result.messages.security,
|
||||
total_count=result.messages.total_count,
|
||||
offset=result.messages.offset,
|
||||
limit=result.messages.limit,
|
||||
cursor=None,
|
||||
next_cursor=next_cursor,
|
||||
cursor_stable=cursor_stable,
|
||||
full=not cursor_stable,
|
||||
messages=result.messages.messages,
|
||||
),
|
||||
)
|
||||
except MailProfileError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ImapConfigurationError as exc:
|
||||
@@ -742,10 +1088,12 @@ def list_profile_mailbox_folders(
|
||||
@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),
|
||||
cursor: str | None = None,
|
||||
refresh: bool = False,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
@@ -755,6 +1103,55 @@ def list_profile_mailbox_messages(
|
||||
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)
|
||||
if not refresh:
|
||||
cached = cached_mailbox_message_page(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=folder,
|
||||
limit=effective_limit,
|
||||
offset=effective_offset,
|
||||
)
|
||||
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,
|
||||
)
|
||||
next_cursor, cursor_stable = _next_mailbox_cursor(
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=profile_id,
|
||||
folder=cached.folder,
|
||||
limit=cached.limit,
|
||||
offset=cached.offset,
|
||||
total_count=cached.total_count,
|
||||
uidvalidity=cached.uidvalidity,
|
||||
messages=cached.messages,
|
||||
)
|
||||
return _mailbox_messages_response(
|
||||
profile_id=profile_id,
|
||||
folder=cached.folder,
|
||||
host=imap.host,
|
||||
port=imap.port,
|
||||
security=imap.security.value,
|
||||
total_count=cached.total_count,
|
||||
offset=cached.offset,
|
||||
limit=cached.limit,
|
||||
cursor=cursor,
|
||||
next_cursor=next_cursor,
|
||||
cursor_stable=cursor_stable,
|
||||
full=not cursor_stable,
|
||||
messages=cached.messages,
|
||||
from_cache=True,
|
||||
refreshing=refreshing,
|
||||
indexed_at=cached.indexed_at,
|
||||
)
|
||||
result = list_imap_messages(
|
||||
imap_config=imap,
|
||||
folder=folder,
|
||||
@@ -763,6 +1160,8 @@ def list_profile_mailbox_messages(
|
||||
after_uid=after_uid,
|
||||
expected_uidvalidity=expected_uidvalidity,
|
||||
)
|
||||
cache_mailbox_messages(session, tenant_id=principal.tenant_id, profile_id=profile_id, result=result)
|
||||
session.commit()
|
||||
full = bool(cursor_values is not None and result.cursor_reset)
|
||||
next_cursor, cursor_stable = _next_mailbox_cursor(
|
||||
tenant_id=principal.tenant_id,
|
||||
@@ -774,7 +1173,7 @@ def list_profile_mailbox_messages(
|
||||
uidvalidity=result.uidvalidity,
|
||||
messages=result.messages,
|
||||
)
|
||||
return MailMailboxMessageListResponse(
|
||||
return _mailbox_messages_response(
|
||||
profile_id=profile_id,
|
||||
folder=result.folder,
|
||||
host=result.host,
|
||||
@@ -787,7 +1186,7 @@ def list_profile_mailbox_messages(
|
||||
next_cursor=next_cursor,
|
||||
cursor_stable=cursor_stable,
|
||||
full=full or not cursor_stable,
|
||||
messages=[_mailbox_summary_response(message) for message in result.messages],
|
||||
messages=result.messages,
|
||||
)
|
||||
except MailProfileError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
|
||||
Reference in New Issue
Block a user