from __future__ import annotations import copy from typing import Any CAMPAIGN_MAIL_SERVER_KEYS = frozenset( { "mail_profile_id", "smtp_server_id", "smtp_credential_id", "imap_server_id", "imap_credential_id", } ) CAMPAIGN_CLIENT_EDITOR_STATE_KEYS = frozenset({"created_from", "field_overrides", "opt_ins"}) CAMPAIGN_OPT_IN_KEYS = frozenset( {"campaign_address_suggestions", "remember_used_addresses", "inline_guidance"} ) CAMPAIGN_REVIEW_STATE_KEYS = frozenset( { "build_token", "inspection_complete", "reviewed_message_keys", "updated_at", "updated_by_user_id", } ) class CampaignMailProfileBoundaryError(ValueError): """Raised when campaign JSON owns mail transport configuration. SMTP/IMAP endpoints and credentials are Mail-module data. Campaign JSON may select Mail-owned profile, server, and credential identifiers, but it must never copy or override transport configuration. """ def _validated_opt_ins(value: Any) -> dict[str, bool]: if not isinstance(value, dict) or any( key not in CAMPAIGN_OPT_IN_KEYS for key in value ): raise CampaignMailProfileBoundaryError( "Campaign editor opt_ins contains unsupported fields" ) if any(not isinstance(item, bool) for item in value.values()): raise CampaignMailProfileBoundaryError( "Campaign editor opt_ins values must be booleans" ) return copy.deepcopy(value) def _is_valid_field_override(key: Any, value: Any) -> bool: return ( isinstance(key, str) and bool(key.strip()) and len(key) <= 256 and isinstance(value, bool) ) def _validated_field_overrides(value: Any) -> dict[str, bool]: if not isinstance(value, dict) or len(value) > 10_000: raise CampaignMailProfileBoundaryError( "Campaign editor field_overrides must be a bounded object" ) if any(not _is_valid_field_override(key, item) for key, item in value.items()): raise CampaignMailProfileBoundaryError( "Campaign editor field_overrides must map short field names to booleans" ) return copy.deepcopy(value) def _validated_required_string(value: Any, *, max_length: int, error: str) -> str: if not isinstance(value, str) or not value.strip() or len(value) > max_length: raise CampaignMailProfileBoundaryError(error) return value.strip() def validate_campaign_editor_state( value: dict[str, Any] | None, *, allow_server_review_state: bool = False, ) -> dict[str, Any]: """Validate the bounded Campaign-owned UI metadata contract. Arbitrary editor metadata would be a second, weakly typed persistence and response channel for Mail credentials. Review evidence is server-owned and cannot be supplied through ordinary version create/update requests. """ if value is None: return {} if not isinstance(value, dict): raise CampaignMailProfileBoundaryError("Campaign editor state must be an object") allowed = set(CAMPAIGN_CLIENT_EDITOR_STATE_KEYS) if allow_server_review_state: allowed.add("review_send") if any(key not in allowed for key in value): raise CampaignMailProfileBoundaryError( "Campaign editor state contains unsupported or transport-owned fields" ) result: dict[str, Any] = {} if "created_from" in value: result["created_from"] = _validated_required_string( value["created_from"], max_length=128, error="Campaign editor created_from must be a short string", ) if "opt_ins" in value: result["opt_ins"] = _validated_opt_ins(value["opt_ins"]) if "field_overrides" in value: result["field_overrides"] = _validated_field_overrides( value["field_overrides"] ) if "review_send" in value: result["review_send"] = _validated_server_review_state(value["review_send"]) return result def public_campaign_editor_state( value: Any, *, include_diagnostics: bool = False, ) -> dict[str, Any]: """Return only known non-secret UI metadata from current or legacy rows.""" if not isinstance(value, dict): return {} result: dict[str, Any] = {} for key in CAMPAIGN_CLIENT_EDITOR_STATE_KEYS: if key not in value: continue try: result.update(validate_campaign_editor_state({key: value[key]})) except CampaignMailProfileBoundaryError: continue if "review_send" in value: try: review_state = _validated_server_review_state(value["review_send"]) if not include_diagnostics: review_state.pop("build_token", None) result["review_send"] = review_state except CampaignMailProfileBoundaryError: pass return result def campaign_editor_state_for_edit(value: Any) -> dict[str, Any]: """Copy only client-owned safe metadata into a new editable version.""" state = public_campaign_editor_state(value) state.pop("review_send", None) return state def _is_valid_reviewed_message_key(value: Any) -> bool: return isinstance(value, str) and bool(value.strip()) and len(value) <= 512 def _validated_reviewed_message_keys(value: Any) -> list[str]: if not isinstance(value, list) or len(value) > 100_000: raise CampaignMailProfileBoundaryError( "Campaign reviewed message keys are invalid" ) if any(not _is_valid_reviewed_message_key(key) for key in value): raise CampaignMailProfileBoundaryError( "Campaign reviewed message keys are invalid" ) return list(dict.fromkeys(key.strip() for key in value)) def _validated_review_actor(value: Any) -> str | None: if value is not None and (not isinstance(value, str) or len(value) > 256): raise CampaignMailProfileBoundaryError("Campaign review actor is invalid") return value def _validated_server_review_state(value: Any) -> dict[str, Any]: if not isinstance(value, dict) or any( key not in CAMPAIGN_REVIEW_STATE_KEYS for key in value ): raise CampaignMailProfileBoundaryError( "Campaign review editor state is invalid" ) build_token = value.get("build_token") inspected = value.get("inspection_complete") keys = value.get("reviewed_message_keys", []) updated_at = value.get("updated_at") updated_by = value.get("updated_by_user_id") validated_build_token = _validated_required_string( build_token, max_length=256, error="Campaign review build token is invalid", ) if not isinstance(inspected, bool): raise CampaignMailProfileBoundaryError( "Campaign review completion state is invalid" ) validated_keys = _validated_reviewed_message_keys(keys) validated_updated_at = _validated_required_string( updated_at, max_length=128, error="Campaign review timestamp is invalid", ) validated_updated_by = _validated_review_actor(updated_by) return { "build_token": validated_build_token, "inspection_complete": inspected, "reviewed_message_keys": validated_keys, "updated_at": validated_updated_at, "updated_by_user_id": validated_updated_by, } def campaign_mail_profile_id(raw_json: dict[str, Any] | None) -> str | None: server = raw_json.get("server") if isinstance(raw_json, dict) else None if not isinstance(server, dict): return None value = server.get("mail_profile_id") if not isinstance(value, str): return None normalized = value.strip() return normalized or None def campaign_mail_resource_ids( raw_json: dict[str, Any] | None, ) -> dict[str, str | None]: server = raw_json.get("server") if isinstance(raw_json, dict) else None if not isinstance(server, dict): return { "mail_profile_id": None, "smtp_server_id": None, "smtp_credential_id": None, "imap_server_id": None, "imap_credential_id": None, } return { key: ( value.strip() if isinstance((value := server.get(key)), str) and value.strip() else None ) for key in CAMPAIGN_MAIL_SERVER_KEYS } def campaign_mail_profile_boundary_violations(raw_json: dict[str, Any] | None) -> tuple[str, ...]: server = raw_json.get("server") if isinstance(raw_json, dict) else None if not isinstance(server, dict): return () violations = [f"/server/{key}" for key in sorted(server) if key not in CAMPAIGN_MAIL_SERVER_KEYS] for key in CAMPAIGN_MAIL_SERVER_KEYS: if key not in server: continue value = server[key] if not isinstance(value, str) or not value.strip(): violations.append(f"/server/{key}") references = campaign_mail_resource_ids(raw_json) if references["mail_profile_id"] is None and any( references[key] for key in references if key != "mail_profile_id" ): violations.append("/server/mail_profile_id") for protocol in ("smtp", "imap"): if ( references[f"{protocol}_credential_id"] and not references[f"{protocol}_server_id"] ): violations.append(f"/server/{protocol}_server_id") return tuple(violations) def assert_campaign_uses_mail_profile_reference( raw_json: dict[str, Any] | None, *, require_profile: bool = False, ) -> None: violations = campaign_mail_profile_boundary_violations(raw_json) if violations: fields = ", ".join(violations) raise CampaignMailProfileBoundaryError( "Campaign JSON may only reference Mail-owned profiles, servers, and credentials; " f"remove campaign-local SMTP/IMAP settings or invalid references ({fields}), " "select authorized Mail resources, and save a new campaign version." ) if require_profile and campaign_mail_profile_id(raw_json) is None: raise CampaignMailProfileBoundaryError( "Campaign delivery requires server.mail_profile_id. Select an authorized, active " "profile from the Mail module and validate the campaign again." ) def public_campaign_mail_server(raw_json: dict[str, Any] | None) -> dict[str, str]: """Return the complete public/persisted Campaign-to-Mail contract.""" return { key: value for key, value in campaign_mail_resource_ids(raw_json).items() if value }