from datetime import datetime, timezone from datetime import date from typing import Literal from uuid import UUID from pydantic import BaseModel, ConfigDict, EmailStr, Field, model_validator class AccountRegister(BaseModel): account_type: Literal["individual", "company"] email_name: str = Field( min_length=1, max_length=64, pattern=r"^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?$", ) company_domain: str | None = Field(default=None, max_length=253) first_name: str = Field(min_length=1, max_length=80) last_name: str = Field(min_length=1, max_length=80) date_of_birth: date phone_number: str = Field( min_length=7, max_length=24, pattern=r"^\+?[0-9][0-9 ()-]{5,22}[0-9]$", ) password: str = Field(min_length=8, max_length=128) class AccountRead(BaseModel): id: UUID account_type: str email: EmailStr first_name: str last_name: str date_of_birth: date phone_number: str company_domain: str | None is_domain_owner: bool = False must_change_password: bool = False created_at: datetime model_config = ConfigDict(from_attributes=True) class LoginRequest(BaseModel): email: EmailStr password: str = Field(min_length=1, max_length=128) class AuthResponse(BaseModel): access_token: str token_type: Literal["bearer"] = "bearer" account: AccountRead class OrgMemberCreate(BaseModel): first_name: str = Field(min_length=1, max_length=80) last_name: str = Field(min_length=1, max_length=80) email: EmailStr invitation_email: EmailStr | None = None section_id: UUID | None = None org_role: Literal["user", "admin"] = "user" send_invitation: bool = True force_password_change: bool = False password: str | None = Field(default=None, min_length=8, max_length=128) @model_validator(mode="after") def password_required_without_invite(self): if not self.send_invitation and not self.password: raise ValueError("Password is required when not sending an invitation.") return self class OrgMemberRead(BaseModel): id: UUID email: EmailStr invitation_email: EmailStr | None = None first_name: str last_name: str org_role: str member_status: str must_change_password: bool = False is_domain_owner: bool = False section_id: UUID | None = None section_name: str | None = None invitation_sent: bool = False created_at: datetime model_config = ConfigDict(from_attributes=True) class OrgSectionCreate(BaseModel): name: str = Field(min_length=1, max_length=120) class OrgSectionRead(BaseModel): id: UUID name: str created_at: datetime model_config = ConfigDict(from_attributes=True) class PasswordChangeRequest(BaseModel): current_password: str = Field(min_length=1, max_length=128) new_password: str = Field(min_length=8, max_length=128) class InvitePreview(BaseModel): work_email: EmailStr first_name: str organization: str invited_by: str expires_at: datetime class InviteAcceptRequest(BaseModel): password: str = Field(min_length=8, max_length=128) class OrgMemberListResponse(BaseModel): items: list[OrgMemberRead] counts: dict[str, int] class OrgMemberImportRow(BaseModel): first_name: str = Field(min_length=1, max_length=80) last_name: str = Field(min_length=1, max_length=80) work_email: str = Field(min_length=1, max_length=320) invitation_email: EmailStr | None = None password: str | None = Field(default=None, min_length=8, max_length=128) class OrgMemberImportRequest(BaseModel): org_role: Literal["user", "admin"] = "user" send_invitation: bool = True force_password_change: bool = True rows: list[OrgMemberImportRow] = Field(min_length=1, max_length=300) class OrgMemberImportRowResult(BaseModel): row: int email: str success: bool message: str invitation_sent: bool = False class OrgMemberImportResponse(BaseModel): created: list[OrgMemberRead] errors: list[OrgMemberImportRowResult] created_count: int error_count: int class MailGroupCreate(BaseModel): name: str = Field(min_length=1, max_length=120) email: EmailStr group_type: Literal["distribution", "dynamic"] = "distribution" owner_id: UUID send_permission: Literal[ "org_members", "group_members", "owners_managers", "anyone", ] = "org_members" description: str | None = Field(default=None, max_length=500) member_ids: list[UUID] = Field(default_factory=list) class MailGroupRead(BaseModel): id: UUID name: str email: EmailStr group_type: str owner_id: UUID | None = None send_permission: str = "org_members" member_count: int created_at: datetime model_config = ConfigDict(from_attributes=True) class MailGroupEmailCheck(BaseModel): email: EmailStr available: bool conflict_type: str | None = None class MailGroupListResponse(BaseModel): items: list[MailGroupRead] counts: dict[str, int] class MailAliasCreate(BaseModel): alias_address: EmailStr forwards_to: EmailStr alias_type: Literal["user", "group"] = "user" is_domain_alias: bool = False class MailAliasRead(BaseModel): id: UUID alias_address: EmailStr forwards_to: EmailStr alias_type: str is_domain_alias: bool status: str created_at: datetime model_config = ConfigDict(from_attributes=True) class MailAliasListResponse(BaseModel): items: list[MailAliasRead] counts: dict[str, int] class OrgSecuritySettingsRead(BaseModel): require_strong_passwords: bool require_mfa_admins: bool require_mfa_all_users: bool session_timeout_minutes: int model_config = ConfigDict(from_attributes=True) class HandoffEmailSample(BaseModel): subject: str = "" snippet: str = "" body_text: str = "" is_starred: bool = False class HandoffRuleCreate(BaseModel): owner_email: EmailStr owner_name: str | None = None labels: list[str] = Field(default_factory=list) email_types: list[str] = Field(default_factory=list) instructions: str = "" allow_replies: bool = True reply_as: Literal["agent", "human"] = "agent" active: bool = True class HandoffRuleUpdate(BaseModel): owner_email: EmailStr | None = None owner_name: str | None = None labels: list[str] | None = None email_types: list[str] | None = None instructions: str | None = None allow_replies: bool | None = None reply_as: Literal["agent", "human"] | None = None active: bool | None = None class HandoffRuleRead(BaseModel): id: str owner_email: EmailStr owner_name: str labels: list[str] email_types: list[str] instructions: str allow_replies: bool reply_as: Literal["agent", "human"] active: bool created_at: datetime updated_at: datetime class HandoffTestRequest(BaseModel): rule_id: str | None = None owner_email: EmailStr | None = None labels: list[str] = Field(default_factory=list) email_types: list[str] = Field(default_factory=list) instructions: str = "" email: HandoffEmailSample class HandoffTestResult(BaseModel): owner_email: EmailStr | None = None label_matched: bool matched_labels: list[str] type_matched: bool matched_types: list[str] instruction_matched: bool instruction_reason: str would_handoff: bool result_path: str reason: str class HandoffLinkedExternal(BaseModel): thread_id: str subject: str sender: str class HandoffConversationSummary(BaseModel): id: str external_subject: str external_sender: str human_email: str status: str status_label: str current_owner: str snippet: str message_count: int updated_at: datetime class HandoffMessageRead(BaseModel): id: str role: Literal["agent", "human", "customer"] subject: str snippet: str body_text: str new_reply_content: str | None = None raw_body: str | None = None has_quoted_text: bool = False relay_status: str | None = None event_at: datetime class HandoffConversationRead(BaseModel): id: str external_subject: str external_sender: str human_email: str handoff_reason: str status: str status_label: str current_owner: str linked_external: HandoffLinkedExternal messages: list[HandoffMessageRead] class OrgSecuritySettingsUpdate(BaseModel): require_strong_passwords: bool | None = None require_mfa_admins: bool | None = None require_mfa_all_users: bool | None = None session_timeout_minutes: int | None = Field(default=None, ge=15, le=1440) class OrgMemberRoleUpdate(BaseModel): org_role: Literal["user", "admin"] class EmailCreate(BaseModel): recipients: list[EmailStr] = Field(default_factory=list) cc: list[EmailStr] = Field(default_factory=list) bcc: list[EmailStr] = Field(default_factory=list) subject: str = Field(default="", max_length=998, pattern=r"^[^\r\n]*$") body_text: str = "" body_html: str | None = None reply_to_message_id: str | None = None reply_all: bool = False thread_id: str | None = None @model_validator(mode="after") def recipients_required_without_reply(self): if not self.reply_to_message_id and not self.recipients: raise ValueError("At least one recipient is required.") return self class EmailRead(EmailCreate): id: str sender: EmailStr status: str created_at: datetime class MailAddress(BaseModel): name: str address: str class AttachmentRead(BaseModel): filename: str content_type: str size: int content_id: str | None = None disposition: str = "attachment" class MessageSummary(BaseModel): id: str mailbox: Literal["inbox", "sent", "trash"] direction: Literal["inbound", "outbound"] sender: MailAddress recipients: list[MailAddress] subject: str snippet: str status: str created_at: datetime is_read: bool is_starred: bool = False trashed_at: datetime | None = None attachments: list[AttachmentRead] = Field(default_factory=list) thread_id: str | None = None class MessageRead(MessageSummary): cc: list[MailAddress] = Field(default_factory=list) bcc: list[MailAddress] = Field(default_factory=list) reply_to: list[MailAddress] = Field(default_factory=list) body_text: str = "" body_html: str | None = None message_id: str | None = None in_reply_to: str | None = None references: list[str] = Field(default_factory=list) parent_message_id: str | None = None thread_relation: str | None = None class MessageReadState(BaseModel): is_read: bool class MessageStarState(BaseModel): is_starred: bool class StickyNoteCreate(BaseModel): title: str = Field(min_length=1, max_length=120) body: str = Field(default="", max_length=2000) tags: list[str] = Field(default_factory=list, max_length=5) color: Literal["yellow", "green", "blue", "pink"] = "yellow" class StickyNoteUpdate(BaseModel): title: str | None = Field(default=None, min_length=1, max_length=120) body: str | None = Field(default=None, max_length=2000) tags: list[str] | None = Field(default=None, max_length=5) color: Literal["yellow", "green", "blue", "pink"] | None = None class StickyNoteRead(StickyNoteCreate): id: str created_at: datetime updated_at: datetime class MessagePage(BaseModel): items: list[MessageSummary] next_cursor: str | None = None class ThreadSummary(BaseModel): id: str mailbox: Literal["inbox", "sent", "trash"] subject: str snippet: str participants: list[MailAddress] = Field(default_factory=list) message_count: int unread_count: int = 0 is_read: bool = True is_starred: bool = False has_attachments: bool = False latest_message_id: str latest_at: datetime senders: list[MailAddress] = Field(default_factory=list) class ThreadPage(BaseModel): items: list[ThreadSummary] next_cursor: str | None = None class ThreadRead(BaseModel): thread_id: str subject: str message_count: int messages: list[MessageRead] class DomainCreate(BaseModel): domain_name: str = Field(min_length=3, max_length=253) account_id: UUID | None = None class DomainNameCreate(BaseModel): domain_name: str = Field(min_length=3, max_length=253) class DnsRecordRead(BaseModel): id: UUID record_type: str host: str value: str purpose: str ttl: int model_config = ConfigDict(from_attributes=True) class DnsVerificationCheck(BaseModel): purpose: str verified: bool message: str class DnsVerificationResponse(BaseModel): domain: str status: str verified: bool checks: list[DnsVerificationCheck] class MailDomainRead(BaseModel): id: UUID domain_name: str domain_type: str account_id: UUID | None mail_host: str dkim_selector: str status: str created_at: datetime dns_records: list[DnsRecordRead] = [] model_config = ConfigDict(from_attributes=True) class SmtpCredentialCreate(BaseModel): label: str = Field(default="default", min_length=1, max_length=120) class SmtpCredentialRead(BaseModel): id: UUID username: str label: str is_active: bool created_at: datetime model_config = ConfigDict(from_attributes=True) class SmtpCredentialCreated(SmtpCredentialRead): password: str smtp_host: str smtp_port: int use_tls: bool class DnsSetupRequest(BaseModel): domain: str = Field(min_length=3, max_length=253) mail_server_ip: str = Field(min_length=3, max_length=45) class DnsSetupRecord(BaseModel): record_type: str host: str value: str purpose: str ttl: int priority: int | None = None class DnsSetupResponse(BaseModel): domain: str mail_host: str app_public_host: str mail_server_ip: str is_platform_domain: bool sections: dict[str, list[DnsSetupRecord]] notes: list[str] = Field(default_factory=list) def utc_now() -> datetime: return datetime.now(timezone.utc)