267 lines
8.7 KiB
Python
267 lines
8.7 KiB
Python
from __future__ import annotations
|
|
|
|
import copy
|
|
from pathlib import Path, PureWindowsPath
|
|
from typing import Any
|
|
|
|
from govoplan_campaign.backend.campaign.mail_profile_boundary import public_campaign_mail_server
|
|
|
|
|
|
# These fields locate process-local or storage-backend resources, or authorize
|
|
# a worker claim. They are useful for tightly controlled diagnostics but are
|
|
# not part of the campaign business-data contract.
|
|
CAMPAIGN_INTERNAL_RESPONSE_KEYS = frozenset(
|
|
{
|
|
"campaign_file",
|
|
"claim_token",
|
|
"eml_local_path",
|
|
"eml_path",
|
|
"eml_storage_key",
|
|
"local_path",
|
|
"source_base_path",
|
|
"storage_bucket",
|
|
"storage_key",
|
|
}
|
|
)
|
|
|
|
CAMPAIGN_DIAGNOSTIC_RESPONSE_KEYS = frozenset(
|
|
{
|
|
"background_workers_enabled",
|
|
"build_token",
|
|
"celery_enabled",
|
|
"claimed_at",
|
|
"effective_policy_sha256",
|
|
"execution_input_sha256",
|
|
"imap_claimed_at",
|
|
"imap_transport_revision",
|
|
"job_manifest_sha256",
|
|
"smtp_started_at",
|
|
"smtp_transport_revision",
|
|
}
|
|
)
|
|
|
|
_SEND_NOW_RESULT_KEYS = (
|
|
"campaign_id",
|
|
"version_id",
|
|
"attempted_count",
|
|
"sent_count",
|
|
"failed_count",
|
|
"outcome_unknown_count",
|
|
"skipped_count",
|
|
"preflight_count",
|
|
"delivery_mode",
|
|
"dry_run",
|
|
)
|
|
_SEND_NOW_JOB_RESULT_KEYS = (
|
|
"campaign_id",
|
|
"version_id",
|
|
"job_id",
|
|
"status",
|
|
"attempt_number",
|
|
"dry_run",
|
|
"queued_count",
|
|
"skipped_count",
|
|
"blocked_count",
|
|
"enqueued_count",
|
|
"delivery_mode",
|
|
"worker_queue_available",
|
|
)
|
|
_SYNCHRONOUS_POLICY_KEYS = (
|
|
"max_recipient_jobs",
|
|
"source",
|
|
"deployment_max_recipient_jobs",
|
|
"tenant_max_recipient_jobs",
|
|
)
|
|
_VALIDATION_SUMMARY_KEYS = ("ok", "error_count", "warning_count")
|
|
_BUILD_SUMMARY_KEYS = (
|
|
"built_count",
|
|
"build_failed_count",
|
|
"ready_count",
|
|
"warning_count",
|
|
"needs_review_count",
|
|
"blocked_count",
|
|
"excluded_count",
|
|
"inactive_count",
|
|
"queueable_count",
|
|
)
|
|
|
|
|
|
def public_campaign_payload(value: Any, *, include_diagnostics: bool = False) -> Any:
|
|
"""Return a detached payload without infrastructure-only locators."""
|
|
|
|
if isinstance(value, dict):
|
|
blocked_keys = CAMPAIGN_INTERNAL_RESPONSE_KEYS
|
|
if not include_diagnostics:
|
|
blocked_keys = blocked_keys | CAMPAIGN_DIAGNOSTIC_RESPONSE_KEYS
|
|
return {
|
|
key: public_campaign_payload(item, include_diagnostics=include_diagnostics)
|
|
for key, item in value.items()
|
|
if key not in blocked_keys
|
|
}
|
|
if isinstance(value, list):
|
|
return [public_campaign_payload(item, include_diagnostics=include_diagnostics) for item in value]
|
|
if isinstance(value, tuple):
|
|
return tuple(public_campaign_payload(item, include_diagnostics=include_diagnostics) for item in value)
|
|
return copy.deepcopy(value)
|
|
|
|
|
|
def public_delivery_result_message(
|
|
*,
|
|
last_error: Any,
|
|
send_status: Any,
|
|
imap_status: Any,
|
|
) -> str | None:
|
|
"""Map persisted provider text to a stable business-safe explanation."""
|
|
|
|
if not last_error:
|
|
return None
|
|
clean_send_status = str(send_status or "")
|
|
clean_imap_status = str(imap_status or "")
|
|
if clean_send_status == "outcome_unknown":
|
|
return "SMTP delivery outcome requires operator reconciliation."
|
|
if clean_send_status in {"failed_temporary", "failed_permanent"}:
|
|
return "SMTP delivery failed; an operator can inspect restricted diagnostics."
|
|
if clean_imap_status in {"outcome_unknown", "appending"}:
|
|
return "Sent-folder append outcome requires operator reconciliation."
|
|
if clean_imap_status in {"failed", "skipped"}:
|
|
return "Sent-folder append did not complete; an operator can inspect restricted diagnostics."
|
|
return "Delivery recorded a warning; an operator can inspect restricted diagnostics."
|
|
|
|
|
|
def public_send_campaign_now_result(
|
|
value: dict[str, Any],
|
|
*,
|
|
validation_summary: dict[str, Any],
|
|
build_summary: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
"""Project synchronous delivery into its recipient-authorized public contract.
|
|
|
|
Per-job provider messages are deliberately omitted. They can contain SMTP
|
|
diagnostics or refused envelope addresses and belong only in restricted
|
|
diagnostics backed by persisted job state.
|
|
"""
|
|
|
|
result = _selected_payload(value, _SEND_NOW_RESULT_KEYS)
|
|
policy = value.get("synchronous_send_policy")
|
|
result["synchronous_send_policy"] = _selected_payload(
|
|
policy if isinstance(policy, dict) else {},
|
|
_SYNCHRONOUS_POLICY_KEYS,
|
|
)
|
|
rows = value.get("results")
|
|
if isinstance(rows, list):
|
|
result["results"] = [
|
|
_selected_payload(row, _SEND_NOW_JOB_RESULT_KEYS)
|
|
for row in rows
|
|
if isinstance(row, dict)
|
|
]
|
|
else:
|
|
result["results"] = []
|
|
result["validation"] = _selected_payload(validation_summary, _VALIDATION_SUMMARY_KEYS)
|
|
result["build"] = _selected_payload(build_summary, _BUILD_SUMMARY_KEYS)
|
|
return result
|
|
|
|
|
|
def send_campaign_now_audit_details(value: dict[str, Any]) -> dict[str, Any]:
|
|
"""Return aggregate-only evidence for a synchronous Campaign send audit."""
|
|
|
|
details = _selected_payload(value, _SEND_NOW_RESULT_KEYS)
|
|
policy = value.get("synchronous_send_policy")
|
|
details["synchronous_send_policy"] = _selected_payload(
|
|
policy if isinstance(policy, dict) else {},
|
|
_SYNCHRONOUS_POLICY_KEYS,
|
|
)
|
|
return details
|
|
|
|
|
|
def _selected_payload(value: dict[str, Any], keys: tuple[str, ...]) -> dict[str, Any]:
|
|
return {
|
|
key: copy.deepcopy(value[key])
|
|
for key in keys
|
|
if key in value
|
|
}
|
|
|
|
|
|
def public_campaign_configuration(value: Any) -> Any:
|
|
"""Return campaign JSON without infrastructure locators or mail secrets.
|
|
|
|
Password-named business fields are intentionally retained. Only the
|
|
schema-defined SMTP/IMAP credential paths under ``server`` are secrets.
|
|
"""
|
|
|
|
payload = public_campaign_payload(value)
|
|
if not isinstance(payload, dict):
|
|
return payload
|
|
if "server" in payload:
|
|
payload["server"] = public_campaign_mail_server(payload)
|
|
_sanitize_configuration_paths(payload)
|
|
return payload
|
|
|
|
|
|
def public_source_filename(value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
text = str(value).strip()
|
|
if not text:
|
|
return text
|
|
windows_path = PureWindowsPath(text)
|
|
return windows_path.name if windows_path.is_absolute() or "\\" in text else Path(text).name
|
|
|
|
|
|
def _sanitize_configuration_paths(payload: dict[str, Any]) -> None:
|
|
template = payload.get("template")
|
|
source = template.get("source") if isinstance(template, dict) else None
|
|
if isinstance(source, dict):
|
|
for key in ("subject_path", "text_path", "html_path"):
|
|
if key in source:
|
|
source[key] = _public_configuration_path(source[key])
|
|
|
|
entries = payload.get("entries")
|
|
entry_source = entries.get("source") if isinstance(entries, dict) else None
|
|
if isinstance(entry_source, dict) and "path" in entry_source:
|
|
entry_source["path"] = _public_configuration_path(entry_source["path"])
|
|
|
|
attachments = payload.get("attachments")
|
|
if isinstance(attachments, dict):
|
|
if "base_path" in attachments:
|
|
attachments["base_path"] = _public_configuration_path(attachments["base_path"])
|
|
base_paths = attachments.get("base_paths")
|
|
if isinstance(base_paths, list):
|
|
for item in base_paths:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
for key in ("path", "source"):
|
|
if key in item:
|
|
item[key] = _public_configuration_path(item[key])
|
|
_sanitize_attachment_rules(attachments.get("global"))
|
|
|
|
if isinstance(entries, dict):
|
|
inline_entries = entries.get("inline")
|
|
if isinstance(inline_entries, list):
|
|
for entry in inline_entries:
|
|
if isinstance(entry, dict):
|
|
_sanitize_attachment_rules(entry.get("attachments"))
|
|
defaults = entries.get("defaults")
|
|
if isinstance(defaults, dict):
|
|
_sanitize_attachment_rules(defaults.get("attachments"))
|
|
|
|
|
|
def _sanitize_attachment_rules(value: Any) -> None:
|
|
if not isinstance(value, list):
|
|
return
|
|
for rule in value:
|
|
if isinstance(rule, dict) and "base_dir" in rule:
|
|
rule["base_dir"] = _public_configuration_path(rule["base_dir"])
|
|
|
|
|
|
def _public_configuration_path(value: Any) -> Any:
|
|
if not isinstance(value, str) or not value.strip():
|
|
return value
|
|
text = value.strip()
|
|
windows_path = PureWindowsPath(text)
|
|
path = Path(text)
|
|
if windows_path.is_absolute():
|
|
return windows_path.name
|
|
if path.is_absolute() or text.startswith("~"):
|
|
return path.name
|
|
return value
|