feat(mail): define standard IMAP folder mappings

This commit is contained in:
2026-08-19 21:27:42 +02:00
parent 4cf2bfeb3e
commit 9d1352ba30
10 changed files with 300 additions and 16 deletions
+7 -2
View File
@@ -1,3 +1,8 @@
from govoplan_core.mail.config import ImapConfig, SmtpConfig, TransportSecurity
from govoplan_core.mail.config import (
ImapConfig,
ImapFolderMappings,
SmtpConfig,
TransportSecurity,
)
__all__ = ["ImapConfig", "SmtpConfig", "TransportSecurity"]
__all__ = ["ImapConfig", "ImapFolderMappings", "SmtpConfig", "TransportSecurity"]
+46 -6
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class StrictModel(BaseModel):
@@ -34,21 +34,61 @@ class SmtpServerConfig(StrictModel):
return self
class ImapFolderMappings(StrictModel):
"""Profile-level names for the standard IMAP mailbox roles."""
inbox: str | None = None
sent: str | None = None
drafts: str | None = None
trash: str | None = None
archive: str | None = None
junk: str | None = None
@field_validator("*", mode="before")
@classmethod
def normalize_folder_name(cls, value: Any) -> Any:
if value is None:
return None
normalized = str(value).strip()
return normalized or None
class ImapServerConfig(StrictModel):
host: str | None = None
port: int | None = Field(default=None, ge=1, le=65535)
security: TransportSecurity = TransportSecurity.TLS
sent_folder: str = "auto"
folder_mappings: ImapFolderMappings | None = None
timeout_seconds: int = Field(default=30, ge=1)
@model_validator(mode="before")
@classmethod
def discard_legacy_enabled(cls, value: Any) -> Any:
if isinstance(value, dict) and "enabled" in value:
data = dict(value)
data.pop("enabled", None)
return data
return value
if not isinstance(value, dict):
return value
data = dict(value)
data.pop("enabled", None)
mappings_value = data.get("folder_mappings")
mappings = (
mappings_value.model_dump(exclude_none=True)
if isinstance(mappings_value, ImapFolderMappings)
else dict(mappings_value)
if isinstance(mappings_value, dict)
else {}
)
mapped_sent = str(mappings.get("sent") or "").strip()
legacy_sent = str(data.get("sent_folder") or "").strip()
if mapped_sent:
# The typed mapping is canonical when both new and legacy callers
# provide a Sent value. Keep the legacy field synchronized for
# existing Campaign append consumers.
data["sent_folder"] = mapped_sent
elif legacy_sent and legacy_sent != "auto":
data["sent_folder"] = legacy_sent
mappings["sent"] = legacy_sent
if mappings:
data["folder_mappings"] = mappings
return data
@model_validator(mode="after")
def apply_default_port(self) -> "ImapServerConfig":