569 lines
14 KiB
Python
569 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
|
|
new_reply_content: str | None = None
|
|
raw_body: str | None = None
|
|
has_quoted_text: bool = False
|
|
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)
|