agentic.email/backend/app/schemas.py
2026-10-06 01:51:23 +05:30

566 lines
14 KiB
Python

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"]
subject: str
snippet: str
body_text: str
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)