feat: integrate governed address contacts

This commit is contained in:
2026-08-20 11:10:18 +02:00
parent 2ad122056e
commit 169b81d9db
14 changed files with 745 additions and 7 deletions
+86
View File
@@ -11,6 +11,10 @@ from sqlalchemy.orm import Session
from govoplan_mail.backend.schemas import (
MailAddressLookupCandidate,
MailAddressLookupResponse,
MailAddressWriteTarget,
MailAddressWriteTargetResponse,
MailContactCreateRequest,
MailContactCreateResponse,
MailConnectionTestResponse,
MailBounceObservationListResponse,
MailBounceObservationResponse,
@@ -149,6 +153,7 @@ MAIL_CREDENTIAL_RESOURCE = "mail_credential"
MAILBOX_MESSAGES_CURSOR_SCOPE = "mail.mailbox.messages.v1"
DEFAULT_MAILBOX_MESSAGE_LIMIT = 50
CAPABILITY_ADDRESSES_LOOKUP = "addresses.lookup"
CAPABILITY_ADDRESSES_CONTACT_WRITER = "addresses.contact_writer"
bounce_provider = SqlMailBounceProcessingProvider()
@@ -416,6 +421,15 @@ def _capability_payload(value: object) -> dict[str, Any]:
"source_ref",
"source_revision",
"provenance",
"address_book_label",
"operation",
"allowed",
"reason",
"message",
"scope_type",
"scope_id",
"read_only",
"required_scopes",
):
if hasattr(value, key):
payload[key] = getattr(value, key)
@@ -1361,6 +1375,78 @@ def lookup_mail_addresses(
)
@router.get("/address-write-targets", response_model=MailAddressWriteTargetResponse)
def list_mail_address_write_targets(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> MailAddressWriteTargetResponse:
_require_scope(principal, "mail:profile:use")
capability = _registry_capability(CAPABILITY_ADDRESSES_CONTACT_WRITER)
if capability is None or not hasattr(capability, "list_write_targets"):
return MailAddressWriteTargetResponse(available=False, targets=[])
targets = getattr(capability, "list_write_targets")(session, principal, operation="create_contact")
return MailAddressWriteTargetResponse(
available=True,
targets=[MailAddressWriteTarget.model_validate(_capability_payload(target)) for target in targets],
)
@router.post(
"/address-contacts",
response_model=MailContactCreateResponse,
status_code=status.HTTP_201_CREATED,
)
def create_mail_address_contact(
payload: MailContactCreateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> MailContactCreateResponse:
_require_scope(principal, "mail:profile:use")
capability = _registry_capability(CAPABILITY_ADDRESSES_CONTACT_WRITER)
if capability is None or not all(
hasattr(capability, method)
for method in ("can_write_to_address_book", "create_contact")
):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Address-book contact writing is not available.",
)
decision = getattr(capability, "can_write_to_address_book")(
session,
principal,
address_book_id=payload.address_book_id,
operation="create_contact",
)
decision_payload = _capability_payload(decision)
if decision_payload.get("allowed") is not True:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(decision_payload.get("message") or "The selected address book is not writable."),
)
try:
result = getattr(capability, "create_contact")(
session,
principal,
address_book_id=payload.address_book_id,
payload={
"display_name": payload.display_name or payload.email,
"emails": [{"label": "Mail", "email": payload.email, "is_primary": True}],
},
provenance={
"consumer_module": MAIL_MODULE_ID,
"consumer_workflow": "mailbox_add_contact",
},
)
session.commit()
except ValueError as exc:
session.rollback()
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(exc),
) from exc
return MailContactCreateResponse.model_validate(_capability_payload(result))
@router.get("/settings/delta", response_model=MailSettingsDeltaResponse)
def mail_settings_delta(
scope_type: str = Query(default="tenant"),