from __future__ import annotations from enum import StrEnum from typing import Literal from pydantic import BaseModel, ConfigDict, Field from app.mailer.campaign.models import BuildStatus, SendStatus class MessageValidationStatus(StrEnum): READY = "ready" WARNING = "warning" NEEDS_REVIEW = "needs_review" BLOCKED = "blocked" EXCLUDED = "excluded" INACTIVE = "inactive" class ImapStatus(StrEnum): NOT_REQUESTED = "not_requested" PENDING = "pending" APPENDED = "appended" FAILED = "failed" SKIPPED = "skipped" class MessageIssue(BaseModel): model_config = ConfigDict(extra="forbid") severity: Literal["info", "warning", "error"] code: str message: str behavior: str | None = None source: str | None = None class MessageAddress(BaseModel): model_config = ConfigDict(extra="forbid") email: str name: str | None = None class MessageAttachmentSummary(BaseModel): model_config = ConfigDict(extra="forbid") attachment_id: str | None = None label: str | None = None status: str behavior: str | None = None required: bool allow_multiple: bool zip_enabled: bool base_path_name: str | None = None base_path: str | None = None file_filter: str directory: str matches: list[str] = Field(default_factory=list) class MessageDraft(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) entry_index: int entry_id: str | None = None active: bool build_status: BuildStatus validation_status: MessageValidationStatus send_status: SendStatus imap_status: ImapStatus subject: str | None = None from_: MessageAddress | None = Field(default=None, alias="from") to: list[MessageAddress] = Field(default_factory=list) cc: list[MessageAddress] = Field(default_factory=list) bcc: list[MessageAddress] = Field(default_factory=list) reply_to: list[MessageAddress] = Field(default_factory=list) bounce_to: list[MessageAddress] = Field(default_factory=list) disposition_notification_to: list[MessageAddress] = Field(default_factory=list) attachment_count: int = 0 attachments: list[MessageAttachmentSummary] = Field(default_factory=list) issues: list[MessageIssue] = Field(default_factory=list) eml_path: str | None = None eml_size_bytes: int | None = None @property def is_queueable(self) -> bool: return self.active and self.build_status == BuildStatus.BUILT and self.validation_status in { MessageValidationStatus.READY, MessageValidationStatus.WARNING, } class CampaignBuildReport(BaseModel): model_config = ConfigDict(extra="forbid") campaign_id: str campaign_name: str campaign_file: str entries_count: int messages: list[MessageDraft] = Field(default_factory=list) @property def built_count(self) -> int: return sum(1 for message in self.messages if message.build_status == BuildStatus.BUILT) @property def build_failed_count(self) -> int: return sum(1 for message in self.messages if message.build_status == BuildStatus.BUILD_FAILED) @property def ready_count(self) -> int: return sum(1 for message in self.messages if message.validation_status == MessageValidationStatus.READY) @property def warning_count(self) -> int: return sum(1 for message in self.messages if message.validation_status == MessageValidationStatus.WARNING) @property def needs_review_count(self) -> int: return sum(1 for message in self.messages if message.validation_status == MessageValidationStatus.NEEDS_REVIEW) @property def blocked_count(self) -> int: return sum(1 for message in self.messages if message.validation_status == MessageValidationStatus.BLOCKED) @property def excluded_count(self) -> int: return sum(1 for message in self.messages if message.validation_status == MessageValidationStatus.EXCLUDED) @property def inactive_count(self) -> int: return sum(1 for message in self.messages if message.validation_status == MessageValidationStatus.INACTIVE) @property def queueable_count(self) -> int: return sum(1 for message in self.messages if message.is_queueable)