642 lines
20 KiB
Python
642 lines
20 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
|
|
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
|
from govoplan_mail.backend.config import (
|
|
ImapConfig,
|
|
ImapServerConfig,
|
|
SmtpConfig,
|
|
SmtpServerConfig,
|
|
TransportCredentials,
|
|
normalize_split_transport_credentials,
|
|
)
|
|
|
|
|
|
class MailSmtpTestRequest(SmtpConfig):
|
|
"""SMTP settings supplied directly from the WebUI mail settings form."""
|
|
|
|
|
|
class MailImapTestRequest(ImapConfig):
|
|
"""IMAP settings supplied directly from the WebUI mail settings form."""
|
|
|
|
|
|
class MailTransportCredentialsPayload(TransportCredentials):
|
|
"""Credentials supplied separately from saved server settings."""
|
|
|
|
|
|
class MailServerProfileCredentialsPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
smtp: MailTransportCredentialsPayload = Field(default_factory=MailTransportCredentialsPayload)
|
|
imap: MailTransportCredentialsPayload = Field(default_factory=MailTransportCredentialsPayload)
|
|
|
|
|
|
def _normalize_profile_transport_payload(value: object) -> object:
|
|
return normalize_split_transport_credentials(value)
|
|
|
|
|
|
def _merge_transport_credentials(server: SmtpServerConfig | ImapServerConfig, credentials: MailTransportCredentialsPayload) -> dict[str, object]:
|
|
payload = server.model_dump(mode="json")
|
|
if "username" in credentials.model_fields_set:
|
|
payload["username"] = credentials.username
|
|
if "password" in credentials.model_fields_set:
|
|
payload["password"] = credentials.password
|
|
return payload
|
|
|
|
|
|
MailProfileScope = Literal["system", "tenant", "user", "group", "campaign"]
|
|
|
|
|
|
class MailCredentialPolicyPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
inherit: bool | None = None
|
|
|
|
|
|
class MailProfilePolicyPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
allowed_profile_ids: list[str] = Field(default_factory=list)
|
|
allow_user_profiles: bool | None = None
|
|
allow_group_profiles: bool | None = None
|
|
allow_campaign_profiles: bool | None = None
|
|
smtp_credentials: MailCredentialPolicyPayload = Field(default_factory=MailCredentialPolicyPayload)
|
|
imap_credentials: MailCredentialPolicyPayload = Field(default_factory=MailCredentialPolicyPayload)
|
|
whitelist: dict[str, list[str]] = Field(default_factory=dict)
|
|
blacklist: dict[str, list[str]] = Field(default_factory=dict)
|
|
allow_lower_level_limits: dict[str, bool] = Field(default_factory=dict)
|
|
|
|
|
|
class MailProfilePolicyUpdateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
policy: MailProfilePolicyPayload = Field(default_factory=MailProfilePolicyPayload)
|
|
|
|
|
|
class PolicySourceStepResponse(BaseModel):
|
|
scope_type: str
|
|
scope_id: str | None = None
|
|
path: str
|
|
label: str
|
|
applied_fields: list[str] = Field(default_factory=list)
|
|
policy: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class MailProfilePolicyResponse(BaseModel):
|
|
scope_type: MailProfileScope
|
|
scope_id: str | None = None
|
|
policy: dict[str, Any]
|
|
effective_policy: dict[str, Any] | None = None
|
|
parent_policy: dict[str, Any] | None = None
|
|
effective_policy_sources: list[PolicySourceStepResponse] = Field(default_factory=list)
|
|
parent_policy_sources: list[PolicySourceStepResponse] = Field(default_factory=list)
|
|
|
|
|
|
class MailServerProfileCreateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str = Field(min_length=1, max_length=255)
|
|
slug: str | None = Field(default=None, max_length=100)
|
|
description: str | None = None
|
|
is_active: bool = True
|
|
inherit_to_lower_scopes: bool = True
|
|
scope_type: MailProfileScope = "tenant"
|
|
scope_id: str | None = None
|
|
smtp: SmtpServerConfig
|
|
imap: ImapServerConfig | None = None
|
|
credentials: MailServerProfileCredentialsPayload = Field(default_factory=MailServerProfileCredentialsPayload)
|
|
|
|
@model_validator(mode="before")
|
|
@classmethod
|
|
def normalize_legacy_transport_payload(cls, value: object) -> object:
|
|
return _normalize_profile_transport_payload(value)
|
|
|
|
def smtp_config(self) -> SmtpConfig:
|
|
return SmtpConfig.model_validate(_merge_transport_credentials(self.smtp, self.credentials.smtp))
|
|
|
|
def imap_config(self) -> ImapConfig | None:
|
|
if self.imap is None:
|
|
return None
|
|
return ImapConfig.model_validate(_merge_transport_credentials(self.imap, self.credentials.imap))
|
|
|
|
def credentials_supplied(self) -> bool:
|
|
return any(
|
|
field in credentials.model_fields_set
|
|
for credentials in (self.credentials.smtp, self.credentials.imap)
|
|
for field in ("username", "password")
|
|
)
|
|
|
|
|
|
class MailServerProfileUpdateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str | None = Field(default=None, max_length=255)
|
|
slug: str | None = Field(default=None, max_length=100)
|
|
description: str | None = None
|
|
is_active: bool | None = None
|
|
inherit_to_lower_scopes: bool | None = None
|
|
smtp: SmtpServerConfig | None = None
|
|
imap: ImapServerConfig | None = None
|
|
credentials: MailServerProfileCredentialsPayload = Field(default_factory=MailServerProfileCredentialsPayload)
|
|
clear_imap: bool = False
|
|
|
|
@model_validator(mode="before")
|
|
@classmethod
|
|
def normalize_legacy_transport_payload(cls, value: object) -> object:
|
|
return _normalize_profile_transport_payload(value)
|
|
|
|
def smtp_config(self) -> SmtpConfig | None:
|
|
if self.smtp is None and "smtp" not in self.credentials.model_fields_set:
|
|
return None
|
|
server = self.smtp or SmtpServerConfig()
|
|
return SmtpConfig.model_validate(_merge_transport_credentials(server, self.credentials.smtp))
|
|
|
|
def imap_config(self) -> ImapConfig | None:
|
|
if self.imap is None and "imap" not in self.credentials.model_fields_set:
|
|
return None
|
|
server = self.imap or ImapServerConfig()
|
|
return ImapConfig.model_validate(_merge_transport_credentials(server, self.credentials.imap))
|
|
|
|
def credentials_supplied(self) -> bool:
|
|
return any(
|
|
field in credentials.model_fields_set
|
|
for credentials in (self.credentials.smtp, self.credentials.imap)
|
|
for field in ("username", "password")
|
|
)
|
|
|
|
|
|
class MailCredentialEnvelopeResponse(BaseModel):
|
|
id: str
|
|
binding_id: str | None = None
|
|
server_id: str | None = None
|
|
tenant_id: str | None = None
|
|
scope_type: MailProfileScope
|
|
scope_id: str | None = None
|
|
name: str
|
|
description: str | None = None
|
|
credential_kind: str
|
|
public_data: dict[str, Any] = Field(default_factory=dict)
|
|
secret_keys: list[str] = Field(default_factory=list)
|
|
secret_configured: bool = False
|
|
allowed_modules: list[str] = Field(default_factory=list)
|
|
allowed_server_refs: list[str] = Field(default_factory=list)
|
|
inherit_to_lower_scopes: bool = False
|
|
is_default: bool = False
|
|
is_active: bool = True
|
|
revision: str
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
deleted_at: datetime | None = None
|
|
|
|
|
|
class MailServerEndpointResponse(BaseModel):
|
|
id: str
|
|
profile_id: str
|
|
tenant_id: str | None = None
|
|
protocol: Literal["smtp", "imap", "jmap", "pop3"]
|
|
name: str
|
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
scope_type: MailProfileScope
|
|
scope_id: str | None = None
|
|
inherit_to_lower_scopes: bool = True
|
|
is_default: bool = False
|
|
is_active: bool = True
|
|
transport_revision: str
|
|
credentials: list[MailCredentialEnvelopeResponse] = Field(default_factory=list)
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class MailServerEndpointCreateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
protocol: Literal["smtp", "imap", "jmap", "pop3"]
|
|
name: str = Field(min_length=1, max_length=255)
|
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
inherit_to_lower_scopes: bool | None = None
|
|
is_default: bool = False
|
|
is_active: bool = True
|
|
|
|
|
|
class MailServerEndpointUpdateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str | None = Field(default=None, max_length=255)
|
|
config: dict[str, Any] | None = None
|
|
inherit_to_lower_scopes: bool | None = None
|
|
is_default: bool | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
class MailCredentialCreateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str = Field(min_length=1, max_length=255)
|
|
description: str | None = None
|
|
credential_kind: str = "username_password"
|
|
username: str | None = None
|
|
password: str | None = None
|
|
public_data: dict[str, Any] = Field(default_factory=dict)
|
|
secret_data: dict[str, Any] = Field(default_factory=dict)
|
|
inherit_to_lower_scopes: bool | None = None
|
|
allowed_modules: list[str] = Field(default_factory=lambda: ["mail"])
|
|
allowed_server_refs: list[str] = Field(default_factory=list)
|
|
is_default: bool = False
|
|
|
|
|
|
class MailCampaignCredentialCreateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str = Field(min_length=1, max_length=255)
|
|
username: str = Field(min_length=1, max_length=320)
|
|
password: SecretStr
|
|
server_ids: list[str] = Field(min_length=1)
|
|
|
|
|
|
class MailCredentialBindRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
credential_id: str = Field(min_length=1)
|
|
is_default: bool = False
|
|
|
|
|
|
class MailCredentialUpdateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str | None = Field(default=None, max_length=255)
|
|
description: str | None = None
|
|
username: str | None = None
|
|
password: str | None = None
|
|
public_data: dict[str, Any] | None = None
|
|
secret_data: dict[str, Any] | None = None
|
|
inherit_to_lower_scopes: bool | None = None
|
|
allowed_modules: list[str] | None = None
|
|
allowed_server_refs: list[str] | None = None
|
|
is_default: bool | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
class MailCredentialUnlinkRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
retire_if_unused: bool = False
|
|
|
|
|
|
class MailCredentialListResponse(BaseModel):
|
|
credentials: list[MailCredentialEnvelopeResponse] = Field(default_factory=list)
|
|
|
|
|
|
class MailServerProfileResponse(BaseModel):
|
|
id: str
|
|
tenant_id: str | None = None
|
|
scope_type: MailProfileScope = "tenant"
|
|
scope_id: str | None = None
|
|
name: str
|
|
slug: str
|
|
description: str | None = None
|
|
is_active: bool
|
|
inherit_to_lower_scopes: bool = True
|
|
smtp: dict[str, Any]
|
|
imap: dict[str, Any] | None = None
|
|
credentials: dict[str, Any] = Field(default_factory=dict)
|
|
smtp_password_configured: bool = False
|
|
imap_password_configured: bool = False
|
|
servers: list[MailServerEndpointResponse] = Field(default_factory=list)
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
|
|
class MailServerProfileListResponse(BaseModel):
|
|
profiles: list[MailServerProfileResponse] = Field(default_factory=list)
|
|
|
|
|
|
class MailSettingsDeltaResponse(BaseModel):
|
|
profiles: list[MailServerProfileResponse] = Field(default_factory=list)
|
|
policy: MailProfilePolicyResponse | None = None
|
|
changed_sections: list[str] = Field(default_factory=list)
|
|
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
|
|
watermark: str | None = None
|
|
has_more: bool = False
|
|
full: bool = False
|
|
|
|
|
|
class MailAddressLookupCandidate(BaseModel):
|
|
contact_id: str
|
|
address_book_id: str
|
|
display_name: str
|
|
email: str | None = None
|
|
email_label: str | None = None
|
|
organization: str | None = None
|
|
role_title: str | None = None
|
|
tags: list[str] = Field(default_factory=list)
|
|
source_kind: str = "local"
|
|
source_ref: str | None = None
|
|
source_revision: str | None = None
|
|
provenance: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class MailAddressLookupResponse(BaseModel):
|
|
available: bool = False
|
|
candidates: list[MailAddressLookupCandidate] = Field(default_factory=list)
|
|
|
|
|
|
class MailAddressWriteTarget(BaseModel):
|
|
address_book_id: str
|
|
address_book_label: str | None = None
|
|
operation: str = "create_contact"
|
|
allowed: bool = False
|
|
reason: str
|
|
message: str
|
|
scope_type: str | None = None
|
|
scope_id: str | None = None
|
|
source_kind: str | None = None
|
|
read_only: bool = False
|
|
required_scopes: list[str] = Field(default_factory=list)
|
|
provenance: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class MailAddressWriteTargetResponse(BaseModel):
|
|
available: bool = False
|
|
targets: list[MailAddressWriteTarget] = Field(default_factory=list)
|
|
|
|
|
|
class MailContactCreateRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
address_book_id: str = Field(min_length=1, max_length=36)
|
|
display_name: str | None = Field(default=None, max_length=255)
|
|
email: str = Field(min_length=3, max_length=320, pattern=r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
|
|
|
|
|
|
class MailContactCreateResponse(BaseModel):
|
|
contact_id: str
|
|
address_book_id: str
|
|
display_name: str
|
|
email: str | None = None
|
|
source_kind: str = "local"
|
|
provenance: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class MailConnectionTestResponse(BaseModel):
|
|
ok: bool
|
|
protocol: Literal["smtp", "imap", "jmap", "pop3"]
|
|
host: str | None = None
|
|
port: int | None = None
|
|
security: str | None = None
|
|
message: str
|
|
details: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class MailPop3PreviewRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
server_id: str = Field(min_length=1, max_length=36)
|
|
credential_id: str | None = Field(default=None, max_length=36)
|
|
limit: int = Field(default=50, ge=1, le=100)
|
|
|
|
|
|
class MailPop3MessagePreviewResponse(BaseModel):
|
|
message_number: int
|
|
uidl: str
|
|
subject: str | None = None
|
|
from_header: str | None = None
|
|
to_header: str | None = None
|
|
date: str | None = None
|
|
message_id: str | None = None
|
|
size_bytes: int = 0
|
|
body_preview: str | None = None
|
|
already_imported: bool = False
|
|
|
|
|
|
class MailPop3PreviewResponse(BaseModel):
|
|
profile_id: str
|
|
server_id: str
|
|
transport_revision: str
|
|
host: str
|
|
port: int
|
|
security: str
|
|
message_count: int
|
|
mailbox_size_bytes: int
|
|
delete_after_import_allowed: bool = False
|
|
messages: list[MailPop3MessagePreviewResponse] = Field(default_factory=list)
|
|
|
|
|
|
class MailPop3ImportRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
server_id: str = Field(min_length=1, max_length=36)
|
|
credential_id: str | None = Field(default=None, max_length=36)
|
|
expected_transport_revision: str = Field(min_length=1, max_length=120)
|
|
uidls: list[str] = Field(min_length=1, max_length=100)
|
|
delete_after_import: bool = False
|
|
|
|
|
|
class MailPop3ImportRecordResponse(BaseModel):
|
|
id: str
|
|
profile_id: str
|
|
pop3_server_id: str
|
|
transport_revision: str
|
|
provider_uidl: str
|
|
message_id: str | None = None
|
|
subject: str | None = None
|
|
from_header: str | None = None
|
|
to_header: str | None = None
|
|
date: str | None = None
|
|
body_preview: str | None = None
|
|
size_bytes: int
|
|
raw_sha256: str
|
|
status: str
|
|
imported_at: datetime
|
|
deletion_requested: bool = False
|
|
deletion_status: str
|
|
deletion_attempted_at: datetime | None = None
|
|
deletion_error: str | None = None
|
|
|
|
|
|
class MailPop3ImportResponse(BaseModel):
|
|
imports: list[MailPop3ImportRecordResponse] = Field(default_factory=list)
|
|
duplicate_uidls: list[str] = Field(default_factory=list)
|
|
deletion_status: str = "not_requested"
|
|
|
|
|
|
class MailPop3ImportListResponse(BaseModel):
|
|
imports: list[MailPop3ImportRecordResponse] = Field(default_factory=list)
|
|
|
|
|
|
class MailImapFolderResponse(BaseModel):
|
|
name: str
|
|
flags: list[str] = Field(default_factory=list)
|
|
message_count: int | None = None
|
|
unseen_count: int | None = None
|
|
|
|
|
|
class MailImapFolderListResponse(BaseModel):
|
|
ok: bool
|
|
protocol: Literal["imap", "jmap"] = "imap"
|
|
host: str | None = None
|
|
port: int | None = None
|
|
security: str | None = None
|
|
message: str
|
|
folders: list[MailImapFolderResponse] = Field(default_factory=list)
|
|
detected_sent_folder: str | None = None
|
|
detected_folder_mappings: dict[str, str] = Field(default_factory=dict)
|
|
from_cache: bool = False
|
|
refreshing: bool = False
|
|
indexed_at: datetime | None = None
|
|
details: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
class MailMailboxAttachmentResponse(BaseModel):
|
|
filename: str | None = None
|
|
content_type: str
|
|
size_bytes: int
|
|
|
|
|
|
class MailMailboxMessageSummaryResponse(BaseModel):
|
|
uid: str
|
|
folder: str
|
|
subject: str | None = None
|
|
from_header: str | None = None
|
|
to_header: str | None = None
|
|
cc_header: str | None = None
|
|
date: str | None = None
|
|
message_id: str | None = None
|
|
flags: list[str] = Field(default_factory=list)
|
|
size_bytes: int | None = None
|
|
body_preview: str | None = None
|
|
attachment_count: int = 0
|
|
|
|
|
|
class MailMailboxMessageDetailResponse(MailMailboxMessageSummaryResponse):
|
|
body_text: str | None = None
|
|
body_html: str | None = None
|
|
headers: dict[str, str] = Field(default_factory=dict)
|
|
attachments: list[MailMailboxAttachmentResponse] = Field(default_factory=list)
|
|
|
|
|
|
class MailMailboxMessageListResponse(BaseModel):
|
|
profile_id: str
|
|
folder: str
|
|
host: str | None = None
|
|
port: int | None = None
|
|
security: str | None = None
|
|
total_count: int = 0
|
|
offset: int = 0
|
|
limit: int = 50
|
|
cursor: str | None = None
|
|
next_cursor: str | None = None
|
|
cursor_stable: bool = False
|
|
full: bool = False
|
|
from_cache: bool = False
|
|
refreshing: bool = False
|
|
indexed_at: datetime | None = None
|
|
messages: list[MailMailboxMessageSummaryResponse] = Field(default_factory=list)
|
|
|
|
|
|
class MailMailboxChangesResponse(BaseModel):
|
|
profile_id: str
|
|
protocol: Literal["jmap"] = "jmap"
|
|
account_id: str
|
|
old_state: str
|
|
new_state: str
|
|
has_more_changes: bool = False
|
|
created: list[str] = Field(default_factory=list)
|
|
updated: list[str] = Field(default_factory=list)
|
|
destroyed: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class MailMailboxBootstrapResponse(BaseModel):
|
|
profile_id: str
|
|
folder: str
|
|
folders: MailImapFolderListResponse
|
|
messages: MailMailboxMessageListResponse
|
|
|
|
|
|
class MailMailboxMessageResponse(BaseModel):
|
|
profile_id: str
|
|
folder: str
|
|
host: str | None = None
|
|
port: int | None = None
|
|
security: str | None = None
|
|
message: MailMailboxMessageDetailResponse
|
|
|
|
|
|
class MailDeliveryCommandResponse(BaseModel):
|
|
result: dict[str, Any]
|
|
|
|
|
|
class MailDeliveryReconcileRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
decision: Literal["accepted", "not_accepted"]
|
|
evidence_reference: str = Field(min_length=1, max_length=500)
|
|
note: str | None = Field(default=None, max_length=4000)
|
|
|
|
|
|
class MailDeliveryResendRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
idempotency_key: str = Field(min_length=1, max_length=200)
|
|
|
|
|
|
class MailBounceSourceRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
profile_id: str = Field(min_length=1, max_length=36)
|
|
folder: str = Field(default="INBOX", min_length=1, max_length=255)
|
|
imap_server_id: str | None = Field(default=None, max_length=36)
|
|
imap_credential_id: str | None = Field(default=None, max_length=36)
|
|
is_active: bool = True
|
|
|
|
|
|
class MailBounceSourceResponse(BaseModel):
|
|
id: str
|
|
tenant_id: str
|
|
profile_id: str
|
|
folder: str
|
|
imap_server_id: str | None = None
|
|
imap_credential_id: str | None = None
|
|
expected_imap_transport_revision: str
|
|
is_active: bool
|
|
uidvalidity: str | None = None
|
|
highest_processed_uid: int
|
|
last_scanned_at: datetime | None = None
|
|
last_success_at: datetime | None = None
|
|
last_error: str | None = None
|
|
|
|
|
|
class MailBounceSourceListResponse(BaseModel):
|
|
sources: list[MailBounceSourceResponse]
|
|
|
|
|
|
class MailBounceObservationResponse(BaseModel):
|
|
id: str
|
|
tenant_id: str
|
|
profile_id: str
|
|
folder: str
|
|
uid: str
|
|
original_message_id: str | None = None
|
|
command_id: str | None = None
|
|
recipient: str | None = None
|
|
action: str
|
|
status_code: str | None = None
|
|
diagnostic: str | None = None
|
|
permanent: bool
|
|
observed_at: datetime
|
|
matched: bool
|
|
evidence: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class MailBounceObservationListResponse(BaseModel):
|
|
observations: list[MailBounceObservationResponse]
|
|
|
|
|
|
class MailBounceScanResponse(BaseModel):
|
|
sources: int
|
|
processed_messages: int
|
|
observations: int
|
|
failures: list[dict[str, str]] = Field(default_factory=list)
|