campaign sending prototype
This commit is contained in:
@@ -7,6 +7,11 @@ from govoplan_mail.backend.schemas import (
|
||||
MailImapFolderListResponse,
|
||||
MailImapFolderResponse,
|
||||
MailImapTestRequest,
|
||||
MailMailboxAttachmentResponse,
|
||||
MailMailboxMessageDetailResponse,
|
||||
MailMailboxMessageListResponse,
|
||||
MailMailboxMessageResponse,
|
||||
MailMailboxMessageSummaryResponse,
|
||||
MailProfilePolicyResponse,
|
||||
MailProfilePolicyUpdateRequest,
|
||||
MailServerProfileCreateRequest,
|
||||
@@ -30,7 +35,7 @@ from govoplan_mail.backend.mail_profiles import (
|
||||
smtp_config_from_profile,
|
||||
update_mail_server_profile,
|
||||
)
|
||||
from govoplan_mail.backend.sending.imap import list_imap_folders, test_imap_login
|
||||
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.sending.smtp import test_smtp_login
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -61,6 +66,11 @@ def _require_profile_credentials_scope(principal: ApiPrincipal, scope_type: str)
|
||||
_require_scope(principal, "mail:secret:manage")
|
||||
|
||||
|
||||
def _require_mailbox_read_scope(principal: ApiPrincipal) -> None:
|
||||
_require_scope(principal, "mail:mailbox:read")
|
||||
_require_scope(principal, "mail:profile:use")
|
||||
|
||||
|
||||
def _require_policy_read_scope(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
if scope_type == "system":
|
||||
_require_scope(principal, "system:settings:read")
|
||||
@@ -83,6 +93,51 @@ def _profile_response(profile) -> MailServerProfileResponse:
|
||||
return MailServerProfileResponse.model_validate(profile_response_payload(profile))
|
||||
|
||||
|
||||
def _mailbox_summary_response(message) -> MailMailboxMessageSummaryResponse:
|
||||
return MailMailboxMessageSummaryResponse(
|
||||
uid=message.uid,
|
||||
folder=message.folder,
|
||||
subject=message.subject,
|
||||
from_header=message.from_header,
|
||||
to_header=message.to_header,
|
||||
cc_header=message.cc_header,
|
||||
date=message.date,
|
||||
message_id=message.message_id,
|
||||
flags=message.flags,
|
||||
size_bytes=message.size_bytes,
|
||||
body_preview=message.body_preview,
|
||||
attachment_count=message.attachment_count,
|
||||
)
|
||||
|
||||
|
||||
def _mailbox_detail_response(message) -> MailMailboxMessageDetailResponse:
|
||||
return MailMailboxMessageDetailResponse(
|
||||
uid=message.uid,
|
||||
folder=message.folder,
|
||||
subject=message.subject,
|
||||
from_header=message.from_header,
|
||||
to_header=message.to_header,
|
||||
cc_header=message.cc_header,
|
||||
date=message.date,
|
||||
message_id=message.message_id,
|
||||
flags=message.flags,
|
||||
size_bytes=message.size_bytes,
|
||||
body_preview=message.body_preview,
|
||||
body_text=message.body_text,
|
||||
body_html=message.body_html,
|
||||
headers=message.headers,
|
||||
attachments=[MailMailboxAttachmentResponse(filename=item.filename, content_type=item.content_type, size_bytes=item.size_bytes) for item in message.attachments],
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
if imap is None:
|
||||
raise MailProfileError("Mail-server profile has no IMAP configuration")
|
||||
return imap
|
||||
|
||||
|
||||
@router.get("/profiles", response_model=MailServerProfileListResponse)
|
||||
def list_profiles(
|
||||
include_inactive: bool = False,
|
||||
@@ -311,6 +366,83 @@ def list_profile_imap_folders(
|
||||
return MailImapFolderListResponse(ok=False, message=_safe_error_message(exc), folders=[], detected_sent_folder=None, details={"error_type": exc.__class__.__name__})
|
||||
|
||||
|
||||
@router.get("/profiles/{profile_id}/mailbox/folders", response_model=MailImapFolderListResponse)
|
||||
def list_profile_mailbox_folders(
|
||||
profile_id: str,
|
||||
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) 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)
|
||||
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_ENTITY, 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/messages", response_model=MailMailboxMessageListResponse)
|
||||
def list_profile_mailbox_messages(
|
||||
profile_id: str,
|
||||
folder: str = Query(default="INBOX", min_length=1, max_length=255),
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
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_messages(imap_config=imap, folder=folder, limit=limit)
|
||||
return MailMailboxMessageListResponse(
|
||||
profile_id=profile_id,
|
||||
folder=result.folder,
|
||||
host=result.host,
|
||||
port=result.port,
|
||||
security=result.security,
|
||||
messages=[_mailbox_summary_response(message) for message in result.messages],
|
||||
)
|
||||
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_ENTITY, 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/messages/{message_uid}", response_model=MailMailboxMessageResponse)
|
||||
def get_profile_mailbox_message(
|
||||
profile_id: str,
|
||||
message_uid: str,
|
||||
folder: str = Query(default="INBOX", min_length=1, max_length=255),
|
||||
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 = get_imap_message(imap_config=imap, folder=folder, uid=message_uid)
|
||||
return MailMailboxMessageResponse(
|
||||
profile_id=profile_id,
|
||||
folder=result.folder,
|
||||
host=result.host,
|
||||
port=result.port,
|
||||
security=result.security,
|
||||
message=_mailbox_detail_response(result.message),
|
||||
)
|
||||
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_ENTITY, detail=str(exc)) from exc
|
||||
except ImapAppendError as exc:
|
||||
status_code = status.HTTP_404_NOT_FOUND if "not found" in str(exc).casefold() else status.HTTP_502_BAD_GATEWAY
|
||||
raise HTTPException(status_code=status_code, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def _safe_error_message(exc: Exception) -> str:
|
||||
text = str(exc).strip()
|
||||
return text or exc.__class__.__name__
|
||||
|
||||
Reference in New Issue
Block a user