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 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(min_length=1) 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 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) 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 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 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)