"""Request/response models for the auth API (SPEC ยง9.1).""" from __future__ import annotations import uuid from typing import Literal from pydantic import BaseModel, Field class RegisterRequest(BaseModel): localpart: str = Field(min_length=3, max_length=30) password: str = Field(min_length=8, max_length=1024) recovery_email: str | None = None class LoginRequest(BaseModel): # Accepts either a bare localpart or the full ``localpart@domain`` handle. handle: str = Field(min_length=1, max_length=320) password: str = Field(min_length=1, max_length=1024) totp: str | None = None # required when the account has 2FA enabled # With a correct code: don't ask this browser for one again (trusted device). remember_device: bool = False class TotpSetupResponse(BaseModel): secret: str # base32, for manual entry provisioning_uri: str # otpauth:// URI to render as a QR code class TotpEnableRequest(BaseModel): code: str = Field(min_length=6, max_length=10) class TotpDisableRequest(BaseModel): password: str = Field(min_length=1, max_length=1024) class SessionOut(BaseModel): sid: str created: str ua: str current: bool class PasswordResetRequestIn(BaseModel): handle: str = Field(min_length=1, max_length=320) class PasswordResetIn(BaseModel): token: str = Field(min_length=1) password: str = Field(min_length=8, max_length=1024) totp: str | None = None # required when the account has 2FA enabled class PasswordResetCheckIn(BaseModel): token: str = Field(min_length=1) class PasswordResetCheckOut(BaseModel): valid: bool # Tells the page to expect the 2FA step; only the link's holder learns it. totp_required: bool = False class AppPasswordIn(BaseModel): name: str = Field(min_length=1, max_length=60) # "Thunderbird on laptop" password: str = Field(min_length=1, max_length=1024) # the account password, to confirm class AppPasswordOut(BaseModel): id: str name: str created_at: str last_used_at: str | None = None class AppPasswordCreatedOut(AppPasswordOut): # The generated password itself: in this answer only, never again. app_password: str class RecoveryEmailIn(BaseModel): password: str = Field(min_length=1, max_length=1024) # None (or blank) removes the recovery address. recovery_email: str | None = Field(default=None, max_length=320) class UserOut(BaseModel): id: uuid.UUID handle: str recovery_email: str | None settings: dict[str, object] totp_enabled: bool = False is_admin: bool = False class RegistrationStatusOut(BaseModel): """Public, unauthenticated: what the signup page should show.""" open: bool # True when the instance has no users yet, so signup creates the administrator. bootstrap: bool class AdminSettingsOut(BaseModel): registration_open: bool default_quota_bytes: int class AdminSettingsIn(BaseModel): """Merge semantics: omitted fields are left as they are.""" registration_open: bool | None = None # Applies to users created after the change; existing mailboxes keep theirs. default_quota_bytes: int | None = Field(default=None, gt=0) class AdminMailOut(BaseModel): smtp_host: str smtp_port: int smtp_security: str smtp_username: str # The password itself never leaves the server. has_password: bool smtp_from: str public_url: str # Host, From address and public address are all set: reset mail can go out. configured: bool class AdminMailIn(BaseModel): """Merge semantics: omitted fields are left as they are.""" smtp_host: str | None = Field(default=None, max_length=255) smtp_port: int | None = Field(default=None, ge=1, le=65535) smtp_security: Literal["starttls", "ssl", "none"] | None = None smtp_username: str | None = Field(default=None, max_length=320) # Omitted keeps the stored password; an empty string removes it. smtp_password: str | None = Field(default=None, max_length=1024) smtp_from: str | None = Field(default=None, max_length=320) public_url: str | None = Field(default=None, max_length=500) class AdminMailTestIn(BaseModel): to: str = Field(min_length=3, max_length=320) class AdminUserOut(BaseModel): id: uuid.UUID handle: str is_admin: bool # ISO timestamp while blocked; null otherwise. blocked_at: str | None quota_bytes: int used_bytes: int created_at: str # The user's connected mail accounts, so the admin can tell who this is. accounts: list[str] class AdminUserPatch(BaseModel): """Merge semantics: omitted fields are left as they are.""" quota_bytes: int | None = Field(default=None, gt=0) blocked: bool | None = None class MessageOut(BaseModel): status: str = "ok"