from __future__ import annotations import json import shutil import tempfile from contextlib import contextmanager from pathlib import Path from typing import Any, Iterator from govoplan_campaign.backend.runtime import capability FILES_CAPABILITY = "files.campaign_attachments" MAIL_CAPABILITY = "mail.campaign_delivery" class OptionalModuleUnavailable(RuntimeError): pass class SmtpConfigurationError(RuntimeError): pass class SmtpSendError(RuntimeError): def __init__(self, message: str, *, temporary: bool = False, outcome_unknown: bool = False) -> None: super().__init__(message) self.temporary = temporary self.outcome_unknown = outcome_unknown class ImapConfigurationError(RuntimeError): pass class ImapAppendError(RuntimeError): def __init__(self, message: str, *, temporary: bool | None = None) -> None: super().__init__(message) self.temporary = temporary class MailProfileError(OptionalModuleUnavailable): pass class _PreparedCampaignSnapshot: def __init__(self, directory: Path, path: Path, raw_json: dict[str, Any]) -> None: self._directory = directory self.path = path self.raw_json = raw_json self.managed_files_by_local_path: dict[str, Any] = {} self.shared_assets: list[Any] = [] def cleanup(self) -> None: shutil.rmtree(self._directory, ignore_errors=True) class FilesCampaignIntegration: def __init__(self, delegate: Any | None = None) -> None: self._delegate = delegate @property def available(self) -> bool: return self._delegate is not None @contextmanager def prepared_campaign_snapshot(self, *args: Any, **kwargs: Any) -> Iterator[Any]: if self._delegate is not None: with self._delegate.prepared_campaign_snapshot(*args, **kwargs) as prepared: yield prepared return raw_json = kwargs.get("raw_json") if isinstance(kwargs.get("raw_json"), dict) else {} prefix = str(kwargs.get("prefix") or "govoplan-campaign-") directory = Path(tempfile.mkdtemp(prefix=prefix)) snapshot = _PreparedCampaignSnapshot(directory, directory / "campaign.json", raw_json) snapshot.path.write_text(json.dumps(raw_json, ensure_ascii=False, indent=2), encoding="utf-8") try: yield snapshot finally: snapshot.cleanup() def managed_match_payloads(self, matches: Any, managed_files_by_local_path: dict[str, Any]) -> list[dict[str, Any]]: if self._delegate is None: return [] return self._delegate.managed_match_payloads(matches, managed_files_by_local_path) def public_attachment_summary_payload(self, attachment: Any) -> dict[str, Any]: if self._delegate is not None: return self._delegate.public_attachment_summary_payload(attachment) if hasattr(attachment, "model_dump"): return attachment.model_dump(mode="json") if isinstance(attachment, dict): return dict(attachment) return {"path": str(attachment)} def annotate_built_messages_with_managed_files(self, built_messages: Any, managed_files_by_local_path: dict[str, Any]) -> None: if self._delegate is not None: self._delegate.annotate_built_messages_with_managed_files(built_messages, managed_files_by_local_path) def record_campaign_attachment_uses_for_jobs(self, session: Any, jobs: Any, *, stage: str) -> None: if self._delegate is not None: self._delegate.record_campaign_attachment_uses_for_jobs(session, jobs, stage=stage) def current_version_and_blob(self, session: Any, asset: Any) -> tuple[Any, Any]: if self._delegate is None: raise OptionalModuleUnavailable("Files module is not available") return self._delegate.current_version_and_blob(session, asset) def mark_job_attachment_uses_sent(self, session: Any, job: Any) -> None: if self._delegate is not None: self._delegate.mark_job_attachment_uses_sent(session, job) class MailCampaignIntegration: def __init__(self, delegate: Any | None = None) -> None: self._delegate = delegate if delegate is not None: self.MailProfileError = getattr(delegate, "MailProfileError", MailProfileError) MailProfileError = MailProfileError SmtpConfigurationError = SmtpConfigurationError SmtpSendError = SmtpSendError ImapConfigurationError = ImapConfigurationError ImapAppendError = ImapAppendError @property def available(self) -> bool: return self._delegate is not None def _require(self) -> Any: if self._delegate is None: raise MailProfileError("Mail module is not available") return self._delegate def materialize_campaign_mail_profile_config(self, session: Any, **kwargs: Any) -> dict[str, Any]: if self._delegate is None: raw_json = kwargs.get("raw_json") if self.mail_profile_id_from_campaign_json(raw_json if isinstance(raw_json, dict) else {}): raise MailProfileError("Campaign mail-server profiles require the mail module") return dict(raw_json) if isinstance(raw_json, dict) else {} try: return self._delegate.materialize_campaign_mail_profile_config(session, **kwargs) except getattr(self._delegate, "MailProfileError", MailProfileError) as exc: raise MailProfileError(str(exc)) from exc def assert_campaign_mail_policy_allows_json(self, session: Any, **kwargs: Any) -> None: if self._delegate is None: raw_json = kwargs.get("raw_json") profile_id = self.mail_profile_id_from_campaign_json(raw_json if isinstance(raw_json, dict) else {}) if profile_id: raise MailProfileError("Campaign mail-server profiles require the mail module") return None try: return self._delegate.assert_campaign_mail_policy_allows_json(session, **kwargs) except getattr(self._delegate, "MailProfileError", MailProfileError) as exc: raise MailProfileError(str(exc)) from exc def assert_mail_policy_allows_send(self, session: Any, **kwargs: Any) -> None: delegate = self._require() try: return delegate.assert_mail_policy_allows_send(session, **kwargs) except getattr(delegate, "MailProfileError", MailProfileError) as exc: raise MailProfileError(str(exc)) from exc def mail_profile_id_from_campaign_json(self, raw_json: dict[str, Any]) -> str | None: if self._delegate is not None: return self._delegate.mail_profile_id_from_campaign_json(raw_json) server = raw_json.get("server") if isinstance(raw_json, dict) else None profile_id = server.get("mail_profile_id") if isinstance(server, dict) else None if profile_id is None and isinstance(server, dict): profile_id = server.get("profile_id") return str(profile_id).strip() if profile_id else None def ensure_mail_profile_allowed_for_campaign(self, session: Any, **kwargs: Any) -> Any: delegate = self._require() try: return delegate.ensure_mail_profile_allowed_for_campaign(session, **kwargs) except getattr(delegate, "MailProfileError", MailProfileError) as exc: raise MailProfileError(str(exc)) from exc def smtp_config_from_profile(self, profile: Any) -> Any: return self._require().smtp_config_from_profile(profile) def imap_config_from_profile(self, profile: Any) -> Any: return self._require().imap_config_from_profile(profile) def effective_profile_credentials_inherited(self, session: Any, **kwargs: Any) -> bool: return self._require().effective_profile_credentials_inherited(session, **kwargs) def apply_campaign_credentials(self, profile_payload: dict[str, Any], server: dict[str, Any], protocol: str) -> dict[str, Any]: return self._require().apply_campaign_credentials(profile_payload, server, protocol) def wait_for_rate_limit(self, **kwargs: Any) -> None: if self._delegate is None: return None return self._delegate.wait_for_rate_limit(**kwargs) def send_email_bytes(self, *args: Any, **kwargs: Any) -> Any: delegate = self._require() try: return delegate.send_email_bytes(*args, **kwargs) except getattr(delegate, "SmtpSendError", SmtpSendError) as exc: raise SmtpSendError(str(exc), temporary=bool(getattr(exc, "temporary", False)), outcome_unknown=bool(getattr(exc, "outcome_unknown", False))) from exc except getattr(delegate, "SmtpConfigurationError", SmtpConfigurationError) as exc: raise SmtpConfigurationError(str(exc)) from exc def append_message_to_sent(self, *args: Any, **kwargs: Any) -> Any: delegate = self._require() try: return delegate.append_message_to_sent(*args, **kwargs) except getattr(delegate, "ImapAppendError", ImapAppendError) as exc: raise ImapAppendError(str(exc), temporary=getattr(exc, "temporary", None)) from exc except getattr(delegate, "ImapConfigurationError", ImapConfigurationError) as exc: raise ImapConfigurationError(str(exc)) from exc def send_email_message(self, *args: Any, **kwargs: Any) -> Any: delegate = self._require() try: return delegate.send_email_message(*args, **kwargs) except getattr(delegate, "SmtpSendError", SmtpSendError) as exc: raise SmtpSendError(str(exc), temporary=bool(getattr(exc, "temporary", False)), outcome_unknown=bool(getattr(exc, "outcome_unknown", False))) from exc except getattr(delegate, "SmtpConfigurationError", SmtpConfigurationError) as exc: raise SmtpConfigurationError(str(exc)) from exc def mock_mailbox(self) -> Any | None: if self._delegate is None or not hasattr(self._delegate, "mock_mailbox"): return None return self._delegate.mock_mailbox() def files_integration() -> FilesCampaignIntegration: return FilesCampaignIntegration(capability(FILES_CAPABILITY)) def mail_integration() -> MailCampaignIntegration: return MailCampaignIntegration(capability(MAIL_CAPABILITY))