"""SQLAlchemy models for the Mail plugin.""" from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, ) from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin # --- Mail Accounts (F-MAIL-14, F-MAIL-18) --- class MailAccount(Base, TenantMixin): """Mail account configuration with encrypted IMAP/SMTP credentials.""" __tablename__ = "mail_accounts" __table_args__ = ( Index("ix_mail_accounts_user", "user_id"), Index("ix_mail_accounts_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) email_address: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str] = mapped_column(String(255), nullable=False, default="") imap_host: Mapped[str] = mapped_column(String(255), nullable=False) imap_port: Mapped[int] = mapped_column(Integer, nullable=False, default=993) imap_ssl: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) smtp_host: Mapped[str] = mapped_column(String(255), nullable=False) smtp_port: Mapped[int] = mapped_column(Integer, nullable=False, default=587) smtp_tls: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) username: Mapped[str] = mapped_column(String(255), nullable=False) encrypted_password: Mapped[str] = mapped_column(Text, nullable=False) is_shared: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) sent_folder_imap_name: Mapped[str | None] = mapped_column(String(255), nullable=True) drafts_folder_imap_name: Mapped[str | None] = mapped_column(String(255), nullable=True) spam_folder_imap_name: Mapped[str | None] = mapped_column(String(255), nullable=True) trash_folder_imap_name: Mapped[str | None] = mapped_column(String(255), nullable=True) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # --- Mail Folders (F-MAIL-01, F-MAIL-19) --- class MailFolder(Base, TenantMixin): """IMAP folder mapped to a mail account.""" __tablename__ = "mail_folders" __table_args__ = ( Index("ix_mail_folders_account", "account_id"), Index("ix_mail_folders_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) account_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_accounts.id", ondelete="CASCADE"), nullable=False, ) name: Mapped[str] = mapped_column(String(255), nullable=False) imap_name: Mapped[str] = mapped_column(String(255), nullable=False) parent_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_folders.id", ondelete="CASCADE"), nullable=True, ) is_standard: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) unread_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) total_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # --- Mails (F-MAIL-01, F-MAIL-03, F-MAIL-05) --- class Mail(Base, TenantMixin): """Individual email message stored locally after IMAP sync.""" __tablename__ = "mails" __table_args__ = ( Index("ix_mails_folder", "folder_id"), Index("ix_mails_account", "account_id"), Index("ix_mails_tenant", "tenant_id"), Index("ix_mails_thread", "thread_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) account_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_accounts.id", ondelete="CASCADE"), nullable=False, ) folder_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_folders.id", ondelete="CASCADE"), nullable=False, ) imap_uid: Mapped[str | None] = mapped_column(String(20), nullable=True) message_id: Mapped[str] = mapped_column(String(512), nullable=False) thread_id: Mapped[str] = mapped_column(String(512), nullable=False, default="") in_reply_to: Mapped[str | None] = mapped_column(String(512), nullable=True) references_header: Mapped[str | None] = mapped_column(Text, nullable=True) subject: Mapped[str] = mapped_column(String(512), nullable=False, default="") from_address: Mapped[str] = mapped_column(String(255), nullable=False) to_addresses: Mapped[str] = mapped_column(Text, nullable=False, default="") cc_addresses: Mapped[str] = mapped_column(Text, nullable=False, default="") bcc_addresses: Mapped[str] = mapped_column(Text, nullable=False, default="") body_text: Mapped[str] = mapped_column(Text, nullable=False, default="") body_html: Mapped[str] = mapped_column(Text, nullable=False, default="") body_html_sanitized: Mapped[str] = mapped_column(Text, nullable=False, default="") is_seen: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_flagged: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) flag_type: Mapped[str | None] = mapped_column(String(32), nullable=True, default=None) is_draft: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_answered: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_forwarded: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) has_attachments: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0) received_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) contact_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) company_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # --- Mail Attachments (F-MAIL-04) --- class MailAttachment(Base, TenantMixin): """Attachment on a mail, optionally linked to a DMS file.""" __tablename__ = "mail_attachments" __table_args__ = ( Index("ix_mail_attachments_mail", "mail_id"), Index("ix_mail_attachments_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) mail_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mails.id", ondelete="CASCADE"), nullable=False, ) filename: Mapped[str] = mapped_column(String(255), nullable=False) mime_type: Mapped[str] = mapped_column( String(255), nullable=False, default="application/octet-stream" ) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0) storage_path: Mapped[str] = mapped_column(String(1024), nullable=False) dms_file_id: Mapped[uuid.UUID | None] = mapped_column(PGUUID(as_uuid=True), nullable=True) content_id: Mapped[str | None] = mapped_column(String(255), nullable=True) # --- Mail Labels (F-MAIL-09) --- class MailLabel(Base, TenantMixin): """Custom label/tag for mails (colored).""" __tablename__ = "mail_labels" __table_args__ = (Index("ix_mail_labels_tenant", "tenant_id"),) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) name: Mapped[str] = mapped_column(String(100), nullable=False) color: Mapped[str] = mapped_column(String(20), nullable=False, default="#808080") user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) class MailLabelAssignment(Base, TenantMixin): """Many-to-many: mail <-> label.""" __tablename__ = "mail_label_assignments" __table_args__ = ( UniqueConstraint("mail_id", "label_id", name="uq_mail_label_assignment"), Index("ix_mail_label_assign_mail", "mail_id"), Index("ix_mail_label_assign_label", "label_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) mail_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mails.id", ondelete="CASCADE"), nullable=False, ) label_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_labels.id", ondelete="CASCADE"), nullable=False, ) # --- Mail Rules (F-MAIL-07) --- class MailRule(Base, TenantMixin): """Filter rule: conditions to actions for incoming mails.""" __tablename__ = "mail_rules" __table_args__ = ( Index("ix_mail_rules_account", "account_id"), Index("ix_mail_rules_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) account_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_accounts.id", ondelete="CASCADE"), nullable=True, ) name: Mapped[str] = mapped_column(String(255), nullable=False) priority: Mapped[int] = mapped_column(Integer, nullable=False, default=0) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) conditions: Mapped[str] = mapped_column(Text, nullable=False, default="{}") actions: Mapped[str] = mapped_column(Text, nullable=False, default="{}") # --- Mail Templates (F-MAIL-06) --- class MailTemplate(Base, TenantMixin): """Email template with placeholder substitution.""" __tablename__ = "mail_templates" __table_args__ = (Index("ix_mail_templates_tenant", "tenant_id"),) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) name: Mapped[str] = mapped_column(String(255), nullable=False) subject: Mapped[str] = mapped_column(String(512), nullable=False, default="") body_html: Mapped[str] = mapped_column(Text, nullable=False, default="") user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) # --- Mail Signatures (F-MAIL-13) --- class MailSignature(Base, TenantMixin): """Email signature (HTML) per user, optionally per account.""" __tablename__ = "mail_signatures" __table_args__ = (Index("ix_mail_signatures_tenant", "tenant_id"),) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) account_id: Mapped[uuid.UUID | None] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_accounts.id", ondelete="CASCADE"), nullable=True, ) name: Mapped[str] = mapped_column(String(255), nullable=False) body_html: Mapped[str] = mapped_column(Text, nullable=False, default="") is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) # --- Vacation Auto-Reply (F-MAIL-08) --- class VacationSentLog(Base, TenantMixin): """Dedup log for vacation auto-replies (one per sender per 24 hours).""" __tablename__ = "vacation_sent_log" __table_args__ = ( Index("ix_vacation_sent_log_account", "account_id"), Index("ix_vacation_sent_log_tenant", "tenant_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) account_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_accounts.id", ondelete="CASCADE"), nullable=False, ) sender_address: Mapped[str] = mapped_column(String(255), nullable=False) sent_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) # --- Seen-By Tracking (F-MAIL-15) --- class MailSeenBy(Base, TenantMixin): """Tracks which users have seen a mail in a shared mailbox.""" __tablename__ = "mail_seen_by" __table_args__ = ( UniqueConstraint("mail_id", "user_id", name="uq_mail_seen_by"), Index("ix_mail_seen_by_mail", "mail_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) mail_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mails.id", ondelete="CASCADE"), nullable=False, ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) # --- Delegates (F-MAIL-16) --- class MailAccountDelegate(Base, TenantMixin): """Delegate access to a mail account. access_level values: - "read" → Mails lesen, suchen, Threads sehen - "write" → read + Flags setzen, verschieben, Entwürfe speichern - "delete" → write + Mails löschen - "full" → delete + Ordner verwalten + ACLs vergeben """ __tablename__ = "mail_account_delegates" __table_args__ = ( UniqueConstraint("account_id", "delegate_user_id", name="uq_mail_delegate"), Index("ix_mail_delegates_account", "account_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) account_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_accounts.id", ondelete="CASCADE"), nullable=False, ) delegate_user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) access_level: Mapped[str] = mapped_column(String(20), nullable=False, default="read") # --- Send Permissions (F-MAIL-17) --- class MailAccountSendPermission(Base, TenantMixin): """Permission for a user to send as a shared/group mailbox.""" __tablename__ = "mail_account_send_permissions" __table_args__ = ( UniqueConstraint("account_id", "user_id", name="uq_mail_send_perm"), Index("ix_mail_send_perms_account", "account_id"), ) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) account_id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), ForeignKey("mail_accounts.id", ondelete="CASCADE"), nullable=False, ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) # --- PGP Keys (F-MAIL-12) --- class PgpKey(Base, TenantMixin): """PGP private key for a user (encrypted at rest).""" __tablename__ = "pgp_keys" __table_args__ = (Index("ix_pgp_keys_user", "user_id"),) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) key_id: Mapped[str] = mapped_column(String(255), nullable=False) encrypted_private_key: Mapped[str] = mapped_column(Text, nullable=False) public_key_armored: Mapped[str] = mapped_column(Text, nullable=False) class ContactPgpKey(Base, TenantMixin): """Public PGP key for a contact.""" __tablename__ = "contact_pgp_keys" __table_args__ = (Index("ix_contact_pgp_keys_contact", "contact_id"),) id: Mapped[uuid.UUID] = mapped_column( PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) contact_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False) public_key_armored: Mapped[str] = mapped_column(Text, nullable=False) key_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")