from __future__ import annotations from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field, model_validator PostboxClassification = Literal[ "public", "internal", "confidential", "restricted", ] class PostboxAccessDecisionResponse(BaseModel): allowed: bool action: str postbox_id: str reason_code: str explanation: str organization_unit_id: str | None = None function_id: str | None = None assignment_ids: list[str] = Field(default_factory=list) assignment_sources: list[str] = Field(default_factory=list) selected_assignment_id: str | None = None holder_count: int = 0 vacant: bool = True classification: str = "internal" classification_allowed: bool = True binding_status: str = "active" class PostboxDirectoryItem(BaseModel): id: str tenant_id: str address: str address_key: str name: str status: str classification: str organization_unit_id: str | None = None organization_unit_name: str | None = None function_id: str | None = None function_name: str | None = None context_key: str | None = None template_revision_id: str | None = None holder_count: int = 0 vacant: bool = True access: PostboxAccessDecisionResponse | None = None resource_revision: int = Field(default=1, ge=1) etag: str | None = None class PostboxDirectoryResponse(BaseModel): postboxes: list[PostboxDirectoryItem] class PostboxParticipantPayload(BaseModel): kind: str = Field(min_length=1, max_length=30) reference_type: str = Field(min_length=1, max_length=50) reference_id: str | None = Field(default=None, max_length=255) label: str | None = Field(default=None, max_length=500) address: str | None = Field(default=None, max_length=500) class PostboxAttachmentPayload(BaseModel): reference_type: str = Field(min_length=1, max_length=50) reference_id: str = Field(min_length=1, max_length=255) name: str | None = Field(default=None, max_length=1000) media_type: str | None = Field(default=None, max_length=255) size_bytes: int | None = Field(default=None, ge=0) digest: str | None = Field(default=None, max_length=255) metadata: dict[str, Any] = Field(default_factory=dict) class PostboxMessageItem(BaseModel): id: str tenant_id: str postbox_id: str subject: str body_text: str | None = None status: str availability: Literal["available", "withdrawn", "expired"] classification: str sender_label: str | None = None delivered_at: datetime read_at: datetime | None = None acknowledged_at: datetime | None = None expires_at: datetime | None = None withdrawn_at: datetime | None = None producer_module: str | None = None producer_resource_type: str | None = None producer_resource_id: str | None = None in_reply_to_message_id: str | None = None replaces_message_id: str | None = None encryption_profile: str key_epoch: int ciphertext_ref: str | None = None signed_manifest_ref: str | None = None participants: list[PostboxParticipantPayload] = Field(default_factory=list) attachments: list[PostboxAttachmentPayload] = Field(default_factory=list) metadata: dict[str, Any] = Field(default_factory=dict) class PostboxMessageListResponse(BaseModel): messages: list[PostboxMessageItem] total: int limit: int offset: int class PostboxMessageStateRequest(BaseModel): state: Literal["read", "acknowledged"] class PostboxMessageAuthoringPayload(BaseModel): idempotency_key: str = Field(min_length=1, max_length=255) subject: str = Field(min_length=1, max_length=1000) body_text: str | None = None classification: PostboxClassification = "internal" participants: list[PostboxParticipantPayload] = Field(default_factory=list) attachments: list[PostboxAttachmentPayload] = Field(default_factory=list) metadata: dict[str, Any] = Field(default_factory=dict) class PostboxMessageCreateRequest(PostboxMessageAuthoringPayload): postbox_id: str = Field(min_length=1, max_length=36) class PostboxTargetPayload(BaseModel): postbox_id: str | None = Field(default=None, max_length=36) address_key: str | None = Field(default=None, max_length=500) template_id: str | None = Field(default=None, max_length=36) organization_unit_id: str | None = Field(default=None, max_length=36) function_id: str | None = Field(default=None, max_length=36) context_key: str | None = Field(default=None, max_length=255) @model_validator(mode="after") def validate_target(self) -> "PostboxTargetPayload": direct = bool(self.postbox_id or self.address_key) templated = bool( self.template_id and self.organization_unit_id and self.function_id ) if direct == templated: raise ValueError( "Specify one direct Postbox target or one complete template target." ) return self class PostboxDeliveryCreateRequest(BaseModel): target: PostboxTargetPayload producer_module: str = Field(min_length=1, max_length=100) producer_resource_type: str = Field(min_length=1, max_length=100) producer_resource_id: str | None = Field(default=None, max_length=255) idempotency_key: str = Field(min_length=1, max_length=255) subject: str = Field(min_length=1, max_length=1000) body_text: str | None = None sender_label: str | None = Field(default=None, max_length=500) classification: PostboxClassification = "internal" participants: list[PostboxParticipantPayload] = Field(default_factory=list) attachments: list[PostboxAttachmentPayload] = Field(default_factory=list) expires_at: datetime | None = None metadata: dict[str, Any] = Field(default_factory=dict) class PostboxDeliveryResponse(BaseModel): delivery_id: str postbox_id: str message_id: str address: str status: str vacant: bool holder_count: int duplicate: bool = False evidence: dict[str, Any] = Field(default_factory=dict) class PostboxLinkedCopyPolicyPayload(BaseModel): enabled: bool = False structure_id: str | None = Field(default=None, max_length=36) relation_type_ids: list[str] = Field(default_factory=list, max_length=20) max_depth: int = Field(default=1, ge=1, le=20) stop_unit_id: str | None = Field(default=None, max_length=36) stop_unit_type_id: str | None = Field(default=None, max_length=36) target_function_type_id: str | None = Field(default=None, max_length=36) target_template_id: str | None = Field(default=None, max_length=36) fanout: Literal["nearest", "all"] = "nearest" allowed_classifications: list[PostboxClassification] = Field( default_factory=lambda: ["internal"], max_length=20, ) allowed_producer_modules: list[str] = Field( default_factory=list, max_length=50, ) require_expiry: bool = False max_retention_days: int | None = Field(default=None, ge=1, le=36500) @model_validator(mode="after") def validate_enabled_policy(self) -> "PostboxLinkedCopyPolicyPayload": self.relation_type_ids = list(dict.fromkeys(self.relation_type_ids)) self.allowed_classifications = list( dict.fromkeys( value.strip() for value in self.allowed_classifications ) ) self.allowed_producer_modules = list( dict.fromkeys( value.strip() for value in self.allowed_producer_modules ) ) if any(not value for value in self.relation_type_ids): raise ValueError("Relation type IDs must not be empty.") if any(not value for value in self.allowed_classifications): raise ValueError("Allowed classifications must not be empty.") if any(not value for value in self.allowed_producer_modules): raise ValueError("Allowed producer modules must not be empty.") if self.enabled and not all( ( self.structure_id, self.target_function_type_id, self.target_template_id, self.allowed_classifications, self.allowed_producer_modules, ) ): raise ValueError( "Enabled hierarchy copy requires a structure, target function " "type, target template, classification gate, and producer allowlist." ) return self class PostboxAttentionPolicyPayload(BaseModel): mode: Literal["none", "vacancy_escalation"] = "none" delay_minutes: int | None = Field(default=None, ge=1, le=43200) @model_validator(mode="after") def validate_delay(self) -> "PostboxAttentionPolicyPayload": if self.mode == "vacancy_escalation" and self.delay_minutes is None: raise ValueError("Vacancy escalation requires a delay.") if self.mode == "none": self.delay_minutes = None return self class PostboxSharedVisibilityPolicyPayload(BaseModel): mode: Literal["none"] = "none" class PostboxRoutingPolicyPayload(BaseModel): linked_copy: PostboxLinkedCopyPolicyPayload = Field( default_factory=PostboxLinkedCopyPolicyPayload ) attention: PostboxAttentionPolicyPayload = Field( default_factory=PostboxAttentionPolicyPayload ) shared_visibility: PostboxSharedVisibilityPolicyPayload = Field( default_factory=PostboxSharedVisibilityPolicyPayload ) @model_validator(mode="before") @classmethod def normalize_legacy_policy(cls, value: Any) -> Any: if value in (None, {}, {"mode": "none"}): return {} return value @model_validator(mode="after") def validate_semantics(self) -> "PostboxRoutingPolicyPayload": if ( self.attention.mode == "vacancy_escalation" and ( not self.linked_copy.enabled or self.linked_copy.fanout != "nearest" ) ): raise ValueError( "Vacancy escalation requires nearest linked-copy routing." ) return self class PostboxRoutePreviewTarget(BaseModel): depth: int organization_unit_id: str organization_unit_name: str function_id: str | None = None function_name: str | None = None target_postbox_id: str | None = None target_address: str | None = None status: str vacant: bool = True holder_count: int = 0 path: list[dict[str, Any]] = Field(default_factory=list) diagnostics: list[str] = Field(default_factory=list) class PostboxRouteDryRunRequest(BaseModel): target: PostboxTargetPayload producer_module: str = Field(min_length=1, max_length=100) classification: PostboxClassification = "internal" expires_at: datetime | None = None class PostboxRouteDryRunResponse(BaseModel): status: str source_postbox_id: str | None = None policy: PostboxRoutingPolicyPayload = Field( default_factory=PostboxRoutingPolicyPayload ) routes: list[PostboxRoutePreviewTarget] = Field(default_factory=list) diagnostics: list[str] = Field(default_factory=list) class PostboxExactCreateRequest(BaseModel): name: str = Field(min_length=1, max_length=500) description: str | None = None organization_unit_id: str = Field(min_length=1, max_length=36) function_id: str = Field(min_length=1, max_length=36) address_key: str | None = Field(default=None, max_length=120) classification: PostboxClassification = "internal" class PostboxTemplateRevisionPayload(BaseModel): function_type_id: str | None = Field(default=None, max_length=36) scope_kind: Literal["tenant", "unit", "subtree", "unit_type"] = "tenant" scope_id: str | None = Field(default=None, max_length=255) name_pattern: str = Field( default="{unit_name} / {function_name}", min_length=1, max_length=500, ) address_pattern: str = Field( default="{template_slug}.{unit_slug}.{function_slug}", min_length=1, max_length=500, ) classification: PostboxClassification = "internal" allow_vacant_delivery: bool = True routing_policy: PostboxRoutingPolicyPayload = Field( default_factory=PostboxRoutingPolicyPayload ) class PostboxTemplateCreateRequest(PostboxTemplateRevisionPayload): slug: str = Field(min_length=1, max_length=120) name: str = Field(min_length=1, max_length=250) description: str | None = None class PostboxTemplateReviseRequest(PostboxTemplateRevisionPayload): base_revision: int = Field(ge=1) class PostboxTemplateRevisionItem(PostboxTemplateRevisionPayload): id: str revision: int encryption_profile: str history_policy: dict[str, Any] = Field(default_factory=dict) retention_policy: dict[str, Any] = Field(default_factory=dict) published_at: datetime | None = None created_at: datetime class PostboxTemplateItem(BaseModel): id: str tenant_id: str slug: str name: str description: str | None = None status: str current_revision: int resource_revision: int = Field(ge=1) etag: str published_revision_id: str | None = None revisions: list[PostboxTemplateRevisionItem] = Field(default_factory=list) created_at: datetime updated_at: datetime class PostboxTemplateListResponse(BaseModel): templates: list[PostboxTemplateItem] class PostboxTemplatePublishRequest(BaseModel): revision: int | None = Field(default=None, ge=1) base_revision: int = Field(ge=1) class PostboxMutationRequest(BaseModel): base_revision: int = Field(ge=1) class PostboxMaterializeRequest(BaseModel): organization_unit_id: str = Field(min_length=1, max_length=36) function_id: str = Field(min_length=1, max_length=36) context_key: str | None = Field(default=None, max_length=255) class PostboxOrganizationFunctionItem(BaseModel): id: str slug: str name: str function_type_id: str | None = None delegable: bool = False act_in_place_allowed: bool = False class PostboxOrganizationUnitItem(BaseModel): id: str slug: str name: str unit_type_id: str | None = None parent_id: str | None = None functions: list[PostboxOrganizationFunctionItem] = Field(default_factory=list) class PostboxOrganizationRelationTypeItem(BaseModel): id: str slug: str name: str structure_id: str | None = None is_hierarchical: bool = True status: str = "active" class PostboxOrganizationStructureItem(BaseModel): id: str slug: str name: str structure_kind: str status: str = "active" relation_types: list[PostboxOrganizationRelationTypeItem] = Field( default_factory=list ) class PostboxOrganizationTargetsResponse(BaseModel): units: list[PostboxOrganizationUnitItem] structures: list[PostboxOrganizationStructureItem] = Field( default_factory=list ) class PostboxGroupingPayload(BaseModel): name: str = Field(min_length=1, max_length=250) is_default: bool = False postbox_ids: list[str] = Field(default_factory=list, max_length=250) class PostboxGroupingUpdateRequest(PostboxGroupingPayload): base_revision: int = Field(ge=1) class PostboxGroupingItem(PostboxGroupingPayload): id: str resource_revision: int = Field(ge=1) etag: str created_at: datetime updated_at: datetime class PostboxGroupingListResponse(BaseModel): groupings: list[PostboxGroupingItem]