from __future__ import annotations import copy from typing import Any CAMPAIGN_MAIL_SERVER_KEYS = frozenset({"mail_profile_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 one Mail-owned profile, but it must never copy or override that profile's transport configuration. """ 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: created_from = value["created_from"] if not isinstance(created_from, str) or not created_from.strip() or len(created_from) > 128: raise CampaignMailProfileBoundaryError("Campaign editor created_from must be a short string") result["created_from"] = created_from.strip() if "opt_ins" in value: opt_ins = value["opt_ins"] if not isinstance(opt_ins, dict) or any(key not in CAMPAIGN_OPT_IN_KEYS for key in opt_ins): raise CampaignMailProfileBoundaryError("Campaign editor opt_ins contains unsupported fields") if any(not isinstance(item, bool) for item in opt_ins.values()): raise CampaignMailProfileBoundaryError("Campaign editor opt_ins values must be booleans") result["opt_ins"] = copy.deepcopy(opt_ins) if "field_overrides" in value: overrides = value["field_overrides"] if not isinstance(overrides, dict) or len(overrides) > 10_000: raise CampaignMailProfileBoundaryError("Campaign editor field_overrides must be a bounded object") if any( not isinstance(key, str) or not key.strip() or len(key) > 256 or not isinstance(item, bool) for key, item in overrides.items() ): raise CampaignMailProfileBoundaryError( "Campaign editor field_overrides must map short field names to booleans" ) result["field_overrides"] = copy.deepcopy(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 _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") if not isinstance(build_token, str) or not build_token.strip() or len(build_token) > 256: raise CampaignMailProfileBoundaryError("Campaign review build token is invalid") if not isinstance(inspected, bool): raise CampaignMailProfileBoundaryError("Campaign review completion state is invalid") if ( not isinstance(keys, list) or len(keys) > 100_000 or any(not isinstance(key, str) or not key.strip() or len(key) > 512 for key in keys) ): raise CampaignMailProfileBoundaryError("Campaign reviewed message keys are invalid") if not isinstance(updated_at, str) or not updated_at.strip() or len(updated_at) > 128: raise CampaignMailProfileBoundaryError("Campaign review timestamp is invalid") if updated_by is not None and (not isinstance(updated_by, str) or len(updated_by) > 256): raise CampaignMailProfileBoundaryError("Campaign review actor is invalid") return { "build_token": build_token.strip(), "inspection_complete": inspected, "reviewed_message_keys": list(dict.fromkeys(key.strip() for key in keys)), "updated_at": updated_at.strip(), "updated_by_user_id": 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_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] if "mail_profile_id" in server: profile_id = server["mail_profile_id"] if not isinstance(profile_id, str) or not profile_id.strip(): violations.append("/server/mail_profile_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 a Mail-module profile through " f"server.mail_profile_id; remove campaign-local SMTP/IMAP settings ({fields}), " "select an authorized Mail profile, 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.""" profile_id = campaign_mail_profile_id(raw_json) return {"mail_profile_id": profile_id} if profile_id else {}