Add bounce mailbox processing
This commit is contained in:
@@ -12,6 +12,12 @@ from govoplan_mail.backend.schemas import (
|
||||
MailAddressLookupCandidate,
|
||||
MailAddressLookupResponse,
|
||||
MailConnectionTestResponse,
|
||||
MailBounceObservationListResponse,
|
||||
MailBounceObservationResponse,
|
||||
MailBounceScanResponse,
|
||||
MailBounceSourceListResponse,
|
||||
MailBounceSourceRequest,
|
||||
MailBounceSourceResponse,
|
||||
MailCampaignCredentialCreateRequest,
|
||||
MailCredentialBindRequest,
|
||||
MailCredentialCreateRequest,
|
||||
@@ -43,6 +49,7 @@ from govoplan_mail.backend.schemas import (
|
||||
MailSmtpTestRequest,
|
||||
)
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
from govoplan_core.core.change_sequence import (
|
||||
ChangeSequenceEntry,
|
||||
@@ -90,6 +97,14 @@ from govoplan_mail.backend.delivery_outbox import (
|
||||
reconcile_delivery_command,
|
||||
resend_delivery_command,
|
||||
)
|
||||
from govoplan_mail.backend.bounce_processing import (
|
||||
MailBounceError,
|
||||
SqlMailBounceProcessingProvider,
|
||||
configure_bounce_source,
|
||||
delete_bounce_source,
|
||||
list_bounce_observations,
|
||||
list_bounce_sources,
|
||||
)
|
||||
from govoplan_mail.backend.server_hierarchy import (
|
||||
MailHierarchyContext,
|
||||
MailServerHierarchyError,
|
||||
@@ -129,6 +144,7 @@ MAIL_CREDENTIAL_RESOURCE = "mail_credential"
|
||||
MAILBOX_MESSAGES_CURSOR_SCOPE = "mail.mailbox.messages.v1"
|
||||
DEFAULT_MAILBOX_MESSAGE_LIMIT = 50
|
||||
CAPABILITY_ADDRESSES_LOOKUP = "addresses.lookup"
|
||||
bounce_provider = SqlMailBounceProcessingProvider()
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -239,6 +255,143 @@ def resend_mail_delivery_command(
|
||||
return MailDeliveryCommandResponse(result=result)
|
||||
|
||||
|
||||
@router.get("/bounce-sources", response_model=MailBounceSourceListResponse)
|
||||
def get_mail_bounce_sources(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> MailBounceSourceListResponse:
|
||||
_require_scope(principal, "mail:bounce:read")
|
||||
return MailBounceSourceListResponse(
|
||||
sources=[
|
||||
MailBounceSourceResponse.model_validate(item, from_attributes=True)
|
||||
for item in list_bounce_sources(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/bounce-sources", response_model=MailBounceSourceResponse)
|
||||
def save_mail_bounce_source(
|
||||
payload: MailBounceSourceRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> MailBounceSourceResponse:
|
||||
_require_scope(principal, "mail:bounce:manage")
|
||||
try:
|
||||
source = configure_bounce_source(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
profile_id=payload.profile_id,
|
||||
folder=payload.folder,
|
||||
imap_server_id=payload.imap_server_id,
|
||||
imap_credential_id=payload.imap_credential_id,
|
||||
is_active=payload.is_active,
|
||||
created_by_user_id=principal.user.id,
|
||||
)
|
||||
except MailBounceError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
action="mail.bounce_source.saved",
|
||||
object_type="mail_bounce_source",
|
||||
object_id=source.id,
|
||||
details={
|
||||
"profile_id": source.profile_id,
|
||||
"folder": source.folder,
|
||||
"is_active": source.is_active,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return MailBounceSourceResponse.model_validate(source, from_attributes=True)
|
||||
|
||||
|
||||
@router.delete("/bounce-sources/{source_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def remove_mail_bounce_source(
|
||||
source_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> None:
|
||||
_require_scope(principal, "mail:bounce:manage")
|
||||
try:
|
||||
delete_bounce_source(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
source_id=source_id,
|
||||
)
|
||||
except MailBounceError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
action="mail.bounce_source.deleted",
|
||||
object_type="mail_bounce_source",
|
||||
object_id=source_id,
|
||||
details={},
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/bounce-sources/{source_id}/scan",
|
||||
response_model=MailBounceScanResponse,
|
||||
)
|
||||
def scan_mail_bounce_source(
|
||||
source_id: str,
|
||||
limit: int = Query(default=100, ge=1, le=1_000),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> MailBounceScanResponse:
|
||||
_require_scope(principal, "mail:bounce:manage")
|
||||
try:
|
||||
result = bounce_provider.scan_source(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
source_id=source_id,
|
||||
limit=limit,
|
||||
)
|
||||
except MailBounceError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
session.commit()
|
||||
return MailBounceScanResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/bounce-observations",
|
||||
response_model=MailBounceObservationListResponse,
|
||||
)
|
||||
def get_mail_bounce_observations(
|
||||
command_id: str | None = Query(default=None, max_length=36),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> MailBounceObservationListResponse:
|
||||
_require_scope(principal, "mail:bounce:read")
|
||||
return MailBounceObservationListResponse(
|
||||
observations=[
|
||||
MailBounceObservationResponse.model_validate(
|
||||
dataclasses.asdict(item)
|
||||
)
|
||||
for item in list_bounce_observations(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
command_id=command_id,
|
||||
limit=limit,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _capability_payload(value: object) -> dict[str, Any]:
|
||||
if dataclasses.is_dataclass(value):
|
||||
return dataclasses.asdict(value)
|
||||
|
||||
Reference in New Issue
Block a user