from __future__ import annotations import copy import fnmatch import json import re from dataclasses import dataclass, field from typing import Any, Iterable from sqlalchemy import and_, or_, text from sqlalchemy.orm import Session from govoplan_core.admin.settings import get_system_settings from govoplan_core.core.access import CAPABILITY_ACCESS_DIRECTORY, AccessDirectory from govoplan_core.core.campaigns import ( CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT, CampaignMailPolicyContext, CampaignMailPolicyContextProvider, ) from govoplan_core.core.policy import policy_source_step from govoplan_core.core.runtime import get_registry from govoplan_mail.backend.db.models import MailProfilePolicy, MailServerProfile from govoplan_mail.backend.config import ImapConfig, SmtpConfig from govoplan_core.security.secrets import decrypt_secret, encrypt_secret class MailProfileError(RuntimeError): pass MAIL_PROFILE_POLICY_SETTINGS_KEY = "mail_profile_policy" PROFILE_SCOPE_TYPES = {"system", "tenant", "user", "group", "campaign"} PROFILE_PATTERN_KEYS = ("smtp_hosts", "imap_hosts", "envelope_senders", "from_headers", "recipient_domains") PROFILE_SCOPE_ORDER = {"system": 0, "tenant": 1, "user": 2, "group": 2, "campaign": 3} MAIL_POLICY_LIMIT_KEYS = ( "allowed_profile_ids", "allow_user_profiles", "allow_group_profiles", "allow_campaign_profiles", "smtp_credentials.inherit", "imap_credentials.inherit", "whitelist.smtp_hosts", "whitelist.imap_hosts", "whitelist.envelope_senders", "whitelist.from_headers", "whitelist.recipient_domains", "blacklist.smtp_hosts", "blacklist.imap_hosts", "blacklist.envelope_senders", "blacklist.from_headers", "blacklist.recipient_domains", ) def default_mail_policy_lower_level_limits() -> dict[str, bool]: return {key: True for key in MAIL_POLICY_LIMIT_KEYS} @dataclass(slots=True) class EffectiveCredentialPolicy: inherit: bool = True explicit_inherit: bool = False inherit_source: str | None = None def as_dict(self) -> dict[str, Any]: return {"inherit": self.inherit} @dataclass(slots=True) class EffectiveMailProfilePolicy: allowed_profile_id_sets: list[set[str]] = field(default_factory=list) allow_user_profiles: bool = True allow_group_profiles: bool = True allow_campaign_profiles: bool = True smtp_credentials: EffectiveCredentialPolicy = field(default_factory=EffectiveCredentialPolicy) imap_credentials: EffectiveCredentialPolicy = field(default_factory=EffectiveCredentialPolicy) whitelist_groups: dict[str, list[list[str]]] = field(default_factory=lambda: {key: [] for key in PROFILE_PATTERN_KEYS}) blacklist_patterns: dict[str, list[str]] = field(default_factory=lambda: {key: [] for key in PROFILE_PATTERN_KEYS}) allow_lower_level_limits: dict[str, bool] = field(default_factory=default_mail_policy_lower_level_limits) source_policies: list[dict[str, Any]] = field(default_factory=list) def profile_id_allowed(self, profile_id: str) -> bool: return all(profile_id in allowed for allowed in self.allowed_profile_id_sets) def value_allowed(self, key: str, value: str | None) -> tuple[bool, str | None]: if key not in PROFILE_PATTERN_KEYS or not value: return True, None normalized = _normalize_match_value(value) for pattern in self.blacklist_patterns.get(key, []): if _pattern_matches(normalized, pattern): return False, f"{key} value {value!r} is blocked by mail profile blacklist pattern {pattern!r}" for group in self.whitelist_groups.get(key, []): if group and not any(_pattern_matches(normalized, pattern) for pattern in group): return False, f"{key} value {value!r} is not allowed by the effective mail profile whitelist" return True, None def as_dict(self) -> dict[str, Any]: allowed_profile_ids: list[str] | None = None if self.allowed_profile_id_sets: allowed_profile_ids = sorted(set.intersection(*self.allowed_profile_id_sets)) if len(self.allowed_profile_id_sets) > 1 else sorted(next(iter(self.allowed_profile_id_sets))) whitelist: dict[str, list[str]] = {} for key, groups in self.whitelist_groups.items(): flattened = sorted({pattern for group in groups for pattern in group}) if flattened: whitelist[key] = flattened blacklist = {key: patterns for key, patterns in self.blacklist_patterns.items() if patterns} return { "allowed_profile_ids": allowed_profile_ids, "allow_user_profiles": self.allow_user_profiles, "allow_group_profiles": self.allow_group_profiles, "allow_campaign_profiles": self.allow_campaign_profiles, "smtp_credentials": self.smtp_credentials.as_dict(), "imap_credentials": self.imap_credentials.as_dict(), "whitelist": whitelist, "blacklist": blacklist, "allow_lower_level_limits": dict(self.allow_lower_level_limits), } def slugify_profile_name(value: str) -> str: slug = re.sub(r"[^a-z0-9]+", "-", value.strip().casefold()).strip("-") return slug or "mail-profile" def _transport_payload(config: SmtpConfig | ImapConfig) -> tuple[dict[str, Any], str | None, str | None, bool, bool]: payload = config.model_dump(mode="json") username_was_supplied = "username" in config.model_fields_set password_was_supplied = "password" in config.model_fields_set username = payload.pop("username", None) password = payload.pop("password", None) payload.pop("enabled", None) return payload, username, password, username_was_supplied, password_was_supplied def _normalize_scope_type(scope_type: str | None) -> str: clean = (scope_type or "tenant").strip().casefold() if clean not in PROFILE_SCOPE_TYPES: raise MailProfileError("Mail-server profile scope must be system, tenant, user, group or campaign") return clean def _profile_scope_type(profile: MailServerProfile) -> str: return _normalize_scope_type(getattr(profile, "scope_type", None) or "tenant") def _profile_scope_id(profile: MailServerProfile) -> str | None: scope_type = _profile_scope_type(profile) value = getattr(profile, "scope_id", None) if value: return str(value) if scope_type == "tenant" and profile.tenant_id: return str(profile.tenant_id) return None def _clean_string_list(value: Any) -> list[str]: if value is None: return [] if isinstance(value, str): value = [value] if not isinstance(value, list): raise MailProfileError("Mail profile policy lists must be arrays of strings") result: list[str] = [] for item in value: text = str(item).strip() if text: result.append(text) return result def _meaningful_allow_patterns(patterns: Iterable[str]) -> list[str]: return [pattern for pattern in patterns if pattern and pattern != "*"] def _normalize_pattern_rules(value: Any) -> dict[str, list[str]]: if value is None: return {} if not isinstance(value, dict): raise MailProfileError("Mail profile pattern policy must be an object") rules: dict[str, list[str]] = {} for key, raw_patterns in value.items(): if key not in PROFILE_PATTERN_KEYS: raise MailProfileError(f"Unsupported mail profile pattern key: {key}") patterns = _clean_string_list(raw_patterns) if patterns: rules[key] = patterns return rules def _normalize_credential_policy(value: Any) -> dict[str, bool | None]: data = value if isinstance(value, dict) else {} inherit = data.get("inherit") if inherit is None: inherit = data.get("inherit_credentials") return {"inherit": inherit if isinstance(inherit, bool) else None} def _normalize_allow_lower_level_limits(value: Any, *, fill_defaults: bool = False) -> dict[str, bool]: if value is None: return default_mail_policy_lower_level_limits() if fill_defaults else {} if not isinstance(value, dict): raise MailProfileError("Mail profile lower-level limit policy must be an object") normalized = default_mail_policy_lower_level_limits() if fill_defaults else {} for key in MAIL_POLICY_LIMIT_KEYS: raw_value = value.get(key) if isinstance(raw_value, bool): normalized[key] = raw_value return normalized def normalize_mail_profile_policy(payload: dict[str, Any] | None) -> dict[str, Any]: data = dict(payload or {}) allowed_ids = _clean_string_list(data.get("allowed_profile_ids")) whitelist = _normalize_pattern_rules(data.get("whitelist") or data.get("allow_patterns") or {}) blacklist = _normalize_pattern_rules(data.get("blacklist") or data.get("deny_patterns") or {}) normalized: dict[str, Any] = { "allowed_profile_ids": allowed_ids, "smtp_credentials": _normalize_credential_policy(data.get("smtp_credentials")), "imap_credentials": _normalize_credential_policy(data.get("imap_credentials")), "whitelist": whitelist, "blacklist": blacklist, "allow_lower_level_limits": _normalize_allow_lower_level_limits(data.get("allow_lower_level_limits")), } for key in ("allow_user_profiles", "allow_group_profiles", "allow_campaign_profiles"): value = data.get(key) normalized[key] = value if isinstance(value, bool) else None return normalized def _policy_from_settings(settings_payload: dict[str, Any] | None) -> dict[str, Any]: if not isinstance(settings_payload, dict): return normalize_mail_profile_policy({}) raw = settings_payload.get(MAIL_PROFILE_POLICY_SETTINGS_KEY, {}) return normalize_mail_profile_policy(raw if isinstance(raw, dict) else {}) def _policy_from_attr(value: Any) -> dict[str, Any]: return normalize_mail_profile_policy(value if isinstance(value, dict) else {}) def _json_object(value: Any) -> dict[str, Any]: if isinstance(value, dict): return value if isinstance(value, str) and value.strip(): try: parsed = json.loads(value) except json.JSONDecodeError: return {} return parsed if isinstance(parsed, dict) else {} return {} def _access_directory() -> AccessDirectory | None: registry = get_registry() if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_ACCESS_DIRECTORY): return None capability = registry.require_capability(CAPABILITY_ACCESS_DIRECTORY) if not isinstance(capability, AccessDirectory): raise MailProfileError("Access directory capability is invalid") return capability def _campaign_policy_provider() -> CampaignMailPolicyContextProvider | None: registry = get_registry() if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT): return None capability = registry.require_capability(CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT) if not isinstance(capability, CampaignMailPolicyContextProvider): raise MailProfileError("Campaign mail policy context capability is invalid") return capability def _campaign_policy_context(session: Session, *, tenant_id: str, campaign_id: str) -> CampaignMailPolicyContext: provider = _campaign_policy_provider() if provider is None: raise MailProfileError("Campaign module is not available") context = provider.get_campaign_mail_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id) if context is None: raise MailProfileError("Campaign not found for mail-server profile policy") return context def _legacy_tenant_settings(session: Session, tenant_id: str) -> dict[str, Any] | None: row = session.execute(text("select settings from core_scopes where id = :tenant_id"), {"tenant_id": tenant_id}).first() return _json_object(row[0]) if row else None def _legacy_subject_policy(session: Session, *, table_name: str, tenant_id: str, scope_id: str) -> dict[str, Any] | None: if table_name not in {"access_users", "access_groups"}: raise MailProfileError("Unsupported mail policy subject") row = session.execute( text(f"select mail_profile_policy from {table_name} where id = :scope_id and tenant_id = :tenant_id"), {"scope_id": scope_id, "tenant_id": tenant_id}, ).first() return _json_object(row[0]) if row else None def _tenant_exists(session: Session, tenant_id: str) -> bool: return _legacy_tenant_settings(session, tenant_id) is not None def _user_exists(session: Session, *, tenant_id: str, user_id: str) -> bool: directory = _access_directory() if directory is not None: user = directory.get_user(user_id) return bool(user and user.tenant_id == tenant_id and user.status == "active") return _legacy_subject_policy(session, table_name="access_users", tenant_id=tenant_id, scope_id=user_id) is not None def _group_exists(session: Session, *, tenant_id: str, group_id: str) -> bool: directory = _access_directory() if directory is not None: group = directory.get_group(group_id) return bool(group and group.tenant_id == tenant_id and group.status == "active") return _legacy_subject_policy(session, table_name="access_groups", tenant_id=tenant_id, scope_id=group_id) is not None def _policy_scope_key(*, tenant_id: str, scope_type: str, scope_id: str | None) -> tuple[str | None, str, str | None]: clean_scope = _normalize_scope_type(scope_type) if clean_scope == "system": return None, clean_scope, None if clean_scope == "tenant": return tenant_id, clean_scope, scope_id or tenant_id return tenant_id, clean_scope, scope_id def _mail_policy_row( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None, ) -> MailProfilePolicy | None: row_tenant_id, row_scope_type, row_scope_id = _policy_scope_key(tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id) query = session.query(MailProfilePolicy).filter(MailProfilePolicy.scope_type == row_scope_type) query = query.filter(MailProfilePolicy.tenant_id.is_(None) if row_tenant_id is None else MailProfilePolicy.tenant_id == row_tenant_id) query = query.filter(MailProfilePolicy.scope_id.is_(None) if row_scope_id is None else MailProfilePolicy.scope_id == row_scope_id) return query.one_or_none() def _legacy_mail_profile_policy( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None, ) -> dict[str, Any] | None: clean_scope = _normalize_scope_type(scope_type) if clean_scope == "system": return _policy_from_settings(get_system_settings(session).settings or {}) if clean_scope == "tenant": if scope_id and scope_id != tenant_id: return None settings = _legacy_tenant_settings(session, tenant_id) return _policy_from_settings(settings or {}) if settings is not None else None if not scope_id: return None if clean_scope == "user": legacy = _legacy_subject_policy(session, table_name="access_users", tenant_id=tenant_id, scope_id=scope_id) return _policy_from_attr(legacy) if legacy is not None else None if clean_scope == "group": legacy = _legacy_subject_policy(session, table_name="access_groups", tenant_id=tenant_id, scope_id=scope_id) return _policy_from_attr(legacy) if legacy is not None else None try: context = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=scope_id) except MailProfileError: return None return _policy_from_attr(context.mail_profile_policy) def _read_mail_profile_policy( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None = None, ) -> dict[str, Any] | None: clean_scope = _normalize_scope_type(scope_type) if clean_scope != "campaign": row = _mail_policy_row(session, tenant_id=tenant_id, scope_type=clean_scope, scope_id=scope_id) if row is not None: return _policy_from_attr(row.policy) return _legacy_mail_profile_policy(session, tenant_id=tenant_id, scope_type=clean_scope, scope_id=scope_id) def _write_mail_profile_policy( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None, policy: dict[str, Any], ) -> dict[str, Any]: row_tenant_id, row_scope_type, row_scope_id = _policy_scope_key(tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id) row = _mail_policy_row(session, tenant_id=tenant_id, scope_type=row_scope_type, scope_id=row_scope_id) if row is None: row = MailProfilePolicy( tenant_id=row_tenant_id, scope_type=row_scope_type, scope_id=row_scope_id, policy=policy, ) else: row.policy = policy session.add(row) return policy def _mail_policy_source_fields(policy: dict[str, Any], *, baseline: bool = False) -> list[str]: fields: list[str] = [] if _meaningful_allow_patterns(policy.get("allowed_profile_ids") or []): fields.append("allowed_profile_ids") for key in ("allow_user_profiles", "allow_group_profiles", "allow_campaign_profiles"): if isinstance(policy.get(key), bool): fields.append(key) for key in ("smtp_credentials", "imap_credentials"): credential = policy.get(key) or {} if isinstance(credential, dict): if isinstance(credential.get("inherit"), bool): fields.append(f"{key}.inherit") for kind in ("whitelist", "blacklist"): rules = policy.get(kind) or {} if isinstance(rules, dict): for key in PROFILE_PATTERN_KEYS: if _clean_string_list(rules.get(key, [])): fields.append(f"{kind}.{key}") allow_lower = policy.get("allow_lower_level_limits") or {} if isinstance(allow_lower, dict): for key in MAIL_POLICY_LIMIT_KEYS: if isinstance(allow_lower.get(key), bool): fields.append(f"allow_lower_level_limits.{key}") if baseline and not fields: fields.append("defaults") return fields def _mail_policy_source_policy(policy: dict[str, Any], *, baseline: bool = False) -> dict[str, Any]: normalized = normalize_mail_profile_policy(policy) if not baseline: return normalized source_policy = EffectiveMailProfilePolicy().as_dict() allowed_ids = _meaningful_allow_patterns(normalized.get("allowed_profile_ids") or []) if allowed_ids: source_policy["allowed_profile_ids"] = allowed_ids for key in ("allow_user_profiles", "allow_group_profiles", "allow_campaign_profiles"): value = normalized.get(key) if isinstance(value, bool): source_policy[key] = value for key in ("smtp_credentials", "imap_credentials"): credential = normalized.get(key) or {} source_credential = dict(source_policy.get(key) or {}) if isinstance(credential, dict): if isinstance(credential.get("inherit"), bool): source_credential["inherit"] = credential["inherit"] source_policy[key] = source_credential source_policy["whitelist"] = dict(normalized.get("whitelist") or {}) source_policy["blacklist"] = dict(normalized.get("blacklist") or {}) source_policy["allow_lower_level_limits"] = { **default_mail_policy_lower_level_limits(), **(normalized.get("allow_lower_level_limits") or {}), } return source_policy def _mail_policy_source_step(source: str, source_id: str | None, label: str, policy: dict[str, Any], *, baseline: bool = False) -> dict[str, Any]: return policy_source_step( source, label, source_id, applied_fields=_mail_policy_source_fields(policy, baseline=baseline), policy=_mail_policy_source_policy(policy, baseline=baseline), ) def _merge_credential_policy( current: EffectiveCredentialPolicy, data: dict[str, Any], *, source: str, lower_limits: dict[str, bool], inherit_limit_key: str, ) -> None: inherit = data.get("inherit") if isinstance(inherit, bool) and lower_limits.get(inherit_limit_key, True): current.inherit = inherit current.explicit_inherit = True current.inherit_source = source def _merge_policy(policy: EffectiveMailProfilePolicy, data: dict[str, Any], *, source: str, source_id: str | None = None, label: str | None = None, baseline: bool = False) -> None: normalized = normalize_mail_profile_policy(data) policy.source_policies.append(_mail_policy_source_step(source, source_id, label or source.capitalize(), normalized, baseline=baseline)) lower_limits = dict(policy.allow_lower_level_limits) allowed_ids = _meaningful_allow_patterns(normalized.get("allowed_profile_ids") or []) if allowed_ids and lower_limits.get("allowed_profile_ids", True): policy.allowed_profile_id_sets.append(set(allowed_ids)) if normalized.get("allow_user_profiles") is False and lower_limits.get("allow_user_profiles", True): policy.allow_user_profiles = False if normalized.get("allow_group_profiles") is False and lower_limits.get("allow_group_profiles", True): policy.allow_group_profiles = False if normalized.get("allow_campaign_profiles") is False and lower_limits.get("allow_campaign_profiles", True): policy.allow_campaign_profiles = False _merge_credential_policy( policy.smtp_credentials, normalized.get("smtp_credentials") or {}, source=source, lower_limits=lower_limits, inherit_limit_key="smtp_credentials.inherit", ) _merge_credential_policy( policy.imap_credentials, normalized.get("imap_credentials") or {}, source=source, lower_limits=lower_limits, inherit_limit_key="imap_credentials.inherit", ) whitelist = normalized.get("whitelist") or {} blacklist = normalized.get("blacklist") or {} for key in PROFILE_PATTERN_KEYS: allow_patterns = _meaningful_allow_patterns(whitelist.get(key, [])) if allow_patterns and lower_limits.get(f"whitelist.{key}", True): policy.whitelist_groups.setdefault(key, []).append(allow_patterns) deny_patterns = _clean_string_list(blacklist.get(key, [])) if deny_patterns and lower_limits.get(f"blacklist.{key}", True): policy.blacklist_patterns.setdefault(key, []).extend(deny_patterns) local_lower_limits = normalized.get("allow_lower_level_limits") or {} if local_lower_limits: policy.allow_lower_level_limits = { key: lower_limits.get(key, True) and local_lower_limits.get(key, lower_limits.get(key, True)) for key in MAIL_POLICY_LIMIT_KEYS } def _owner_context( session: Session, *, tenant_id: str, campaign_id: str | None = None, owner_user_id: str | None = None, owner_group_id: str | None = None, ) -> tuple[CampaignMailPolicyContext | None, str | None, str | None]: campaign_context = None if campaign_id: campaign_context = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id) owner_user_id = campaign_context.owner_user_id owner_group_id = campaign_context.owner_group_id return campaign_context, owner_user_id, owner_group_id def effective_mail_profile_policy( session: Session, *, tenant_id: str, campaign_id: str | None = None, owner_user_id: str | None = None, owner_group_id: str | None = None, ) -> EffectiveMailProfilePolicy: campaign, owner_user_id, owner_group_id = _owner_context( session, tenant_id=tenant_id, campaign_id=campaign_id, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) tenant_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=tenant_id) if tenant_policy is None: raise MailProfileError("Tenant not found for mail-server profile policy") policy = EffectiveMailProfilePolicy() system_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {} _merge_policy(policy, system_policy, source="system", label="System", baseline=True) _merge_policy(policy, tenant_policy, source="tenant", source_id=tenant_id, label="Tenant") if owner_user_id: user_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=owner_user_id) if user_policy is not None: _merge_policy(policy, user_policy, source="user", source_id=owner_user_id, label="Owner user") if owner_group_id: group_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=owner_group_id) if group_policy is not None: _merge_policy(policy, group_policy, source="group", source_id=owner_group_id, label="Owner group") if campaign is not None: _merge_policy(policy, _policy_from_attr(campaign.mail_profile_policy), source="campaign", source_id=campaign.id, label="Campaign") return policy def effective_mail_profile_policy_for_scope( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None = None, campaign_id: str | None = None, ) -> EffectiveMailProfilePolicy: clean_scope = _normalize_scope_type(scope_type) if campaign_id: return effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign_id) if clean_scope == "system": policy = EffectiveMailProfilePolicy() system_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {} _merge_policy(policy, system_policy, source="system", label="System", baseline=True) return policy if clean_scope == "tenant": return effective_mail_profile_policy(session, tenant_id=tenant_id) if clean_scope == "user": if not scope_id: raise MailProfileError("User mail profile policy requires scope_id") return effective_mail_profile_policy(session, tenant_id=tenant_id, owner_user_id=scope_id) if clean_scope == "group": if not scope_id: raise MailProfileError("Group mail profile policy requires scope_id") return effective_mail_profile_policy(session, tenant_id=tenant_id, owner_group_id=scope_id) if not scope_id: raise MailProfileError("Campaign mail profile policy requires scope_id") return effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=scope_id) def parent_mail_profile_policy( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None = None, ) -> EffectiveMailProfilePolicy: clean_scope = _normalize_scope_type(scope_type) tenant_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=tenant_id) if tenant_policy is None: raise MailProfileError("Tenant not found for mail-server profile policy") policy = EffectiveMailProfilePolicy() system_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {} _merge_policy(policy, system_policy, source="system", label="System", baseline=True) if clean_scope == "tenant": return policy _merge_policy(policy, tenant_policy, source="tenant", source_id=tenant_id, label="Tenant") if clean_scope in {"user", "group"}: return policy if clean_scope != "campaign" or not scope_id: return policy campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=scope_id) if campaign.owner_user_id: user_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=campaign.owner_user_id) if user_policy is not None: _merge_policy(policy, user_policy, source="user", source_id=campaign.owner_user_id, label="Owner user") if campaign.owner_group_id: group_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=campaign.owner_group_id) if group_policy is not None: _merge_policy(policy, group_policy, source="group", source_id=campaign.owner_group_id, label="Owner group") return policy def _normalize_match_value(value: str) -> str: return str(value).strip().casefold() def _pattern_matches(value: str, pattern: str) -> bool: clean_pattern = _normalize_match_value(pattern) if clean_pattern == "*": return True return fnmatch.fnmatchcase(value, clean_pattern) def _assert_policy_value(policy: EffectiveMailProfilePolicy, key: str, value: str | None) -> None: allowed, reason = policy.value_allowed(key, value) if not allowed: raise MailProfileError(reason or "Mail profile policy blocked this value") def _domain_from_email(value: str | None) -> str | None: if not value: return None text = str(value).strip().casefold() if "@" not in text: return text or None return text.rsplit("@", 1)[1] or None def _assert_transport_values_allowed(policy: EffectiveMailProfilePolicy, smtp: SmtpConfig | dict[str, Any] | None, imap: ImapConfig | dict[str, Any] | None) -> None: smtp_host = smtp.host if isinstance(smtp, SmtpConfig) else smtp.get("host") if isinstance(smtp, dict) else None imap_host = imap.host if isinstance(imap, ImapConfig) else imap.get("host") if isinstance(imap, dict) else None _assert_policy_value(policy, "smtp_hosts", str(smtp_host) if smtp_host else None) _assert_policy_value(policy, "imap_hosts", str(imap_host) if imap_host else None) def assert_mail_policy_allows_transport( session: Session, *, tenant_id: str, smtp: SmtpConfig | dict[str, Any] | None, imap: ImapConfig | dict[str, Any] | None = None, campaign_id: str | None = None, owner_user_id: str | None = None, owner_group_id: str | None = None, ) -> None: policy = effective_mail_profile_policy( session, tenant_id=tenant_id, campaign_id=campaign_id, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) _assert_transport_values_allowed(policy, smtp, imap) def assert_mail_policy_allows_send( session: Session, *, tenant_id: str, campaign_id: str, smtp: SmtpConfig | dict[str, Any] | None, imap: ImapConfig | dict[str, Any] | None = None, envelope_sender: str | None = None, from_header: str | None = None, recipients: Iterable[str] = (), ) -> None: policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign_id) _assert_transport_values_allowed(policy, smtp, imap) _assert_policy_value(policy, "envelope_senders", envelope_sender) _assert_policy_value(policy, "from_headers", from_header) for recipient in recipients: _assert_policy_value(policy, "recipient_domains", _domain_from_email(recipient)) def _scope_tuple_for_create( session: Session, *, tenant_id: str, scope_type: str | None, scope_id: str | None, user_id: str | None, ) -> tuple[str | None, str, str | None]: clean_scope = _normalize_scope_type(scope_type) clean_scope_id = str(scope_id).strip() if scope_id else None if clean_scope == "system": return None, clean_scope, None if clean_scope == "tenant": return tenant_id, clean_scope, tenant_id if clean_scope == "user": target_id = clean_scope_id or user_id if not target_id: raise MailProfileError("User-scoped mail-server profiles require a user scope_id") if not _user_exists(session, tenant_id=tenant_id, user_id=target_id): raise MailProfileError("User scope is not part of the active tenant") return tenant_id, clean_scope, target_id if clean_scope == "group": if not clean_scope_id: raise MailProfileError("Group-scoped mail-server profiles require a group scope_id") if not _group_exists(session, tenant_id=tenant_id, group_id=clean_scope_id): raise MailProfileError("Group scope is not part of the active tenant") return tenant_id, clean_scope, clean_scope_id if not clean_scope_id: raise MailProfileError("Campaign-scoped mail-server profiles require a campaign scope_id") _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=clean_scope_id) return tenant_id, clean_scope, clean_scope_id def _ensure_scope_allows_profile_creation( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None, ) -> None: if scope_type in {"system", "tenant"}: return if scope_type == "campaign": policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=scope_id) if not policy.allow_campaign_profiles: raise MailProfileError("Campaign-local mail profiles are disabled by policy") return if scope_type == "user": policy = effective_mail_profile_policy(session, tenant_id=tenant_id, owner_user_id=scope_id) if not policy.allow_user_profiles: raise MailProfileError("User-scoped mail-server profiles are disabled by policy") return if scope_type == "group": policy = effective_mail_profile_policy(session, tenant_id=tenant_id, owner_group_id=scope_id) if not policy.allow_group_profiles: raise MailProfileError("Group-scoped mail-server profiles are disabled by policy") def _ensure_unique_slug( session: Session, *, scope_type: str, scope_id: str | None, slug: str, profile_id: str | None = None, ) -> None: query = session.query(MailServerProfile).filter( MailServerProfile.scope_type == scope_type, MailServerProfile.scope_id.is_(None) if scope_id is None else MailServerProfile.scope_id == scope_id, MailServerProfile.slug == slug, ) if profile_id: query = query.filter(MailServerProfile.id != profile_id) if query.one_or_none() is not None: raise MailProfileError("A mail-server profile with this slug already exists in this scope") def _profile_visible_filter(tenant_id: str): return or_(MailServerProfile.scope_type == "system", MailServerProfile.tenant_id == tenant_id) def _context_profile_clauses(campaign: CampaignMailPolicyContext, policy: EffectiveMailProfilePolicy) -> list[Any]: clauses: list[Any] = [ MailServerProfile.scope_type == "system", and_(MailServerProfile.tenant_id == campaign.tenant_id, MailServerProfile.scope_type == "tenant"), ] if policy.allow_user_profiles and campaign.owner_user_id: clauses.append(and_(MailServerProfile.tenant_id == campaign.tenant_id, MailServerProfile.scope_type == "user", MailServerProfile.scope_id == campaign.owner_user_id)) if policy.allow_group_profiles and campaign.owner_group_id: clauses.append(and_(MailServerProfile.tenant_id == campaign.tenant_id, MailServerProfile.scope_type == "group", MailServerProfile.scope_id == campaign.owner_group_id)) if policy.allow_campaign_profiles: clauses.append(and_(MailServerProfile.tenant_id == campaign.tenant_id, MailServerProfile.scope_type == "campaign", MailServerProfile.scope_id == campaign.id)) return clauses def _profile_transport_allowed(profile: MailServerProfile, policy: EffectiveMailProfilePolicy) -> bool: try: _assert_transport_values_allowed(policy, profile.smtp_config or {}, profile.imap_config or {}) return True except MailProfileError: return False def _profile_allowed_for_context( profile: MailServerProfile, *, tenant_id: str, policy: EffectiveMailProfilePolicy, campaign: CampaignMailPolicyContext | None = None, owner_user_id: str | None = None, owner_group_id: str | None = None, ) -> bool: scope_type = _profile_scope_type(profile) scope_id = _profile_scope_id(profile) if not profile.is_active: return False if not policy.profile_id_allowed(profile.id): return False if scope_type == "system": pass elif scope_type == "tenant": if profile.tenant_id != tenant_id: return False elif scope_type == "user": if not policy.allow_user_profiles or profile.tenant_id != tenant_id or scope_id != owner_user_id: return False elif scope_type == "group": if not policy.allow_group_profiles or profile.tenant_id != tenant_id or scope_id != owner_group_id: return False elif scope_type == "campaign": if not campaign or not policy.allow_campaign_profiles or profile.tenant_id != tenant_id or scope_id != campaign.id: return False else: return False return _profile_transport_allowed(profile, policy) def list_mail_server_profiles( session: Session, *, tenant_id: str, include_inactive: bool = False, campaign_id: str | None = None, ) -> list[MailServerProfile]: if campaign_id: campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id) policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign.id) query = session.query(MailServerProfile).filter(or_(*_context_profile_clauses(campaign, policy))) if not include_inactive: query = query.filter(MailServerProfile.is_active.is_(True)) profiles = query.all() profiles = [ profile for profile in profiles if (include_inactive or profile.is_active) and _profile_allowed_for_context( profile, tenant_id=tenant_id, policy=policy, campaign=campaign, owner_user_id=campaign.owner_user_id, owner_group_id=campaign.owner_group_id, ) ] else: query = session.query(MailServerProfile).filter(_profile_visible_filter(tenant_id)) if not include_inactive: query = query.filter(MailServerProfile.is_active.is_(True)) profiles = query.all() return sorted(profiles, key=lambda item: (PROFILE_SCOPE_ORDER.get(_profile_scope_type(item), 99), (item.name or "").casefold())) def get_mail_server_profile( session: Session, *, tenant_id: str, profile_id: str, require_active: bool = False, ) -> MailServerProfile: profile = session.get(MailServerProfile, profile_id) if profile is None or (_profile_scope_type(profile) != "system" and profile.tenant_id != tenant_id): raise MailProfileError("Mail-server profile not found") if require_active and not profile.is_active: raise MailProfileError("Mail-server profile is inactive") return profile def ensure_mail_profile_allowed_for_campaign( session: Session, *, tenant_id: str, campaign_id: str, profile_id: str, require_active: bool = True, ) -> MailServerProfile: campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id) profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=profile_id, require_active=require_active) policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign.id) if not _profile_allowed_for_context( profile, tenant_id=tenant_id, policy=policy, campaign=campaign, owner_user_id=campaign.owner_user_id, owner_group_id=campaign.owner_group_id, ): raise MailProfileError("Mail-server profile is not allowed for this campaign owner or policy") return profile def _credential_policy_for_protocol(policy: EffectiveMailProfilePolicy, protocol: str) -> EffectiveCredentialPolicy: if protocol == "smtp": return policy.smtp_credentials if protocol == "imap": return policy.imap_credentials raise MailProfileError("Credential policy protocol must be smtp or imap") def _legacy_profile_credentials_inherited(server: dict[str, Any], protocol: str) -> bool: return server.get(f"inherit_{protocol}_credentials") is not False def _local_credential_inheritance_override_allowed(policy: EffectiveMailProfilePolicy, protocol: str) -> bool: credential_policy = _credential_policy_for_protocol(policy, protocol) return ( credential_policy.inherit_source != "campaign" and policy.allow_lower_level_limits.get(f"{protocol}_credentials.inherit", True) ) def profile_credentials_inherited_for_server(policy: EffectiveMailProfilePolicy, server: dict[str, Any], protocol: str) -> bool: credential_policy = _credential_policy_for_protocol(policy, protocol) legacy_key = f"inherit_{protocol}_credentials" if legacy_key in server and _local_credential_inheritance_override_allowed(policy, protocol): return _legacy_profile_credentials_inherited(server, protocol) return credential_policy.inherit def effective_profile_credentials_inherited( session: Session, *, tenant_id: str, server: dict[str, Any], protocol: str, campaign_id: str | None = None, owner_user_id: str | None = None, owner_group_id: str | None = None, ) -> bool: policy = effective_mail_profile_policy( session, tenant_id=tenant_id, campaign_id=campaign_id, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) return profile_credentials_inherited_for_server(policy, server, protocol) def _profile_has_password(profile: MailServerProfile, protocol: str) -> bool: if protocol == "smtp": return bool(profile.smtp_password_encrypted) if protocol == "imap": return bool(profile.imap_password_encrypted) return False def _profile_has_transport(profile: MailServerProfile, protocol: str) -> bool: if protocol == "smtp": return True return bool(profile.imap_config) def _assert_profile_credentials_allowed_for_server(profile: MailServerProfile, policy: EffectiveMailProfilePolicy, server: dict[str, Any]) -> None: for protocol in ("smtp", "imap"): if not _profile_has_transport(profile, protocol): continue credential_policy = _credential_policy_for_protocol(policy, protocol) legacy_key = f"inherit_{protocol}_credentials" if legacy_key in server: legacy_inherit = _legacy_profile_credentials_inherited(server, protocol) if not _local_credential_inheritance_override_allowed(policy, protocol) and legacy_inherit != credential_policy.inherit: raise MailProfileError(f"{protocol.upper()} credential inheritance is locked by the effective mail profile policy") inherited = profile_credentials_inherited_for_server(policy, server, protocol) username, password = _transport_credentials_from_server(server, protocol) if inherited and (username or password): raise MailProfileError(f"{protocol.upper()} credentials must not be stored on the campaign while inherited profile credentials are enforced") if not inherited and _profile_has_password(profile, protocol) and not password: raise MailProfileError(f"{protocol.upper()} credentials must be supplied by this campaign or scope policy") def _inline_transport_configured(server: dict[str, Any], protocol: str) -> bool: value = server.get(protocol) if isinstance(server.get(protocol), dict) else {} credentials = server.get("credentials") if isinstance(server.get("credentials"), dict) else {} credential_value = credentials.get(protocol) if isinstance(credentials.get(protocol), dict) else {} for key in ("host",): if str(value.get(key) or "").strip(): return True for key in ("username", "password"): if str(credential_value.get(key) or value.get(key) or "").strip(): return True return False def assert_campaign_mail_policy_allows_json( session: Session, *, tenant_id: str, raw_json: dict[str, Any] | None, campaign_id: str | None = None, owner_user_id: str | None = None, owner_group_id: str | None = None, ) -> None: data = raw_json if isinstance(raw_json, dict) else {} server = data.get("server") if isinstance(data.get("server"), dict) else {} profile_id = server.get("mail_profile_id") if isinstance(server, dict) else None if profile_id: if campaign_id: profile = ensure_mail_profile_allowed_for_campaign( session, tenant_id=tenant_id, campaign_id=campaign_id, profile_id=str(profile_id), require_active=True, ) policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign_id) _assert_profile_credentials_allowed_for_server(profile, policy, server) return profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=str(profile_id), require_active=True) policy = effective_mail_profile_policy( session, tenant_id=tenant_id, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) if not _profile_allowed_for_context( profile, tenant_id=tenant_id, policy=policy, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ): raise MailProfileError("Mail-server profile is not allowed by the effective policy") _assert_profile_credentials_allowed_for_server(profile, policy, server) return smtp = server.get("smtp") if isinstance(server, dict) and isinstance(server.get("smtp"), dict) else None imap = server.get("imap") if isinstance(server, dict) and isinstance(server.get("imap"), dict) else None if campaign_id: policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign_id) if not policy.allow_campaign_profiles and (_inline_transport_configured(server, "smtp") or _inline_transport_configured(server, "imap")): raise MailProfileError("Campaign-local inline mail settings are disabled by policy") _assert_transport_values_allowed(policy, smtp, imap) return assert_mail_policy_allows_transport( session, tenant_id=tenant_id, smtp=smtp, imap=imap, campaign_id=campaign_id, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) def create_mail_server_profile( session: Session, *, tenant_id: str, user_id: str | None, name: str, slug: str | None, description: str | None, smtp: SmtpConfig, imap: ImapConfig | None, is_active: bool = True, scope_type: str = "tenant", scope_id: str | None = None, ) -> MailServerProfile: clean_name = name.strip() if not clean_name: raise MailProfileError("Profile name cannot be empty") profile_tenant_id, clean_scope_type, clean_scope_id = _scope_tuple_for_create( session, tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id, user_id=user_id, ) _ensure_scope_allows_profile_creation(session, tenant_id=tenant_id, scope_type=clean_scope_type, scope_id=clean_scope_id) if clean_scope_type == "campaign": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=smtp, imap=imap, campaign_id=clean_scope_id) elif clean_scope_type == "user": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=smtp, imap=imap, owner_user_id=clean_scope_id) elif clean_scope_type == "group": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=smtp, imap=imap, owner_group_id=clean_scope_id) elif clean_scope_type == "tenant": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=smtp, imap=imap) clean_slug = slugify_profile_name(slug or clean_name) _ensure_unique_slug(session, scope_type=clean_scope_type, scope_id=clean_scope_id, slug=clean_slug) smtp_payload, smtp_username, smtp_password, _, _ = _transport_payload(smtp) imap_payload = None imap_username = None imap_password = None if imap is not None: imap_payload, imap_username, imap_password, _, _ = _transport_payload(imap) profile = MailServerProfile( tenant_id=profile_tenant_id, scope_type=clean_scope_type, scope_id=clean_scope_id, name=clean_name, slug=clean_slug, description=description, is_active=is_active, smtp_config=smtp_payload, smtp_username=smtp_username, smtp_password_encrypted=encrypt_secret(smtp_password), imap_config=imap_payload, imap_username=imap_username, imap_password_encrypted=encrypt_secret(imap_password), created_by_user_id=user_id, updated_by_user_id=user_id, ) session.add(profile) session.flush() return profile def update_mail_server_profile( session: Session, profile: MailServerProfile, *, user_id: str | None, tenant_id: str, name: str | None = None, slug: str | None = None, description: str | None = None, is_active: bool | None = None, smtp: SmtpConfig | None = None, imap: ImapConfig | None = None, clear_imap: bool = False, ) -> MailServerProfile: if name is not None: clean_name = name.strip() if not clean_name: raise MailProfileError("Profile name cannot be empty") profile.name = clean_name if slug is not None: clean_slug = slugify_profile_name(slug) _ensure_unique_slug( session, scope_type=_profile_scope_type(profile), scope_id=_profile_scope_id(profile), slug=clean_slug, profile_id=profile.id, ) profile.slug = clean_slug if description is not None: profile.description = description if is_active is not None: profile.is_active = is_active next_smtp = smtp or SmtpConfig.model_validate({**(profile.smtp_config or {}), "username": _profile_username(profile, "smtp"), "password": decrypt_secret(profile.smtp_password_encrypted)}) if clear_imap: next_imap = None elif imap is not None: next_imap = imap elif profile.imap_config: next_imap = ImapConfig.model_validate({**(profile.imap_config or {}), "username": _profile_username(profile, "imap"), "password": decrypt_secret(profile.imap_password_encrypted)}) else: next_imap = None scope_type = _profile_scope_type(profile) scope_id = _profile_scope_id(profile) if scope_type == "campaign": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=next_smtp, imap=next_imap, campaign_id=scope_id) elif scope_type == "user": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=next_smtp, imap=next_imap, owner_user_id=scope_id) elif scope_type == "group": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=next_smtp, imap=next_imap, owner_group_id=scope_id) elif scope_type == "tenant": assert_mail_policy_allows_transport(session, tenant_id=tenant_id, smtp=next_smtp, imap=next_imap) if smtp is not None: smtp_payload, smtp_username, smtp_password, username_supplied, password_supplied = _transport_payload(smtp) profile.smtp_config = smtp_payload if username_supplied: profile.smtp_username = smtp_username if password_supplied: profile.smtp_password_encrypted = encrypt_secret(smtp_password) if clear_imap: profile.imap_config = None profile.imap_username = None profile.imap_password_encrypted = None elif imap is not None: imap_payload, imap_username, imap_password, username_supplied, password_supplied = _transport_payload(imap) profile.imap_config = imap_payload if username_supplied: profile.imap_username = imap_username if password_supplied: profile.imap_password_encrypted = encrypt_secret(imap_password) profile.updated_by_user_id = user_id session.add(profile) session.flush() return profile def _profile_username(profile: MailServerProfile, protocol: str) -> str | None: if protocol == "smtp": return profile.smtp_username or (profile.smtp_config or {}).get("username") if protocol == "imap": return profile.imap_username or (profile.imap_config or {}).get("username") return None def smtp_config_from_profile(profile: MailServerProfile) -> SmtpConfig: payload = dict(profile.smtp_config or {}) payload.pop("username", None) payload["username"] = _profile_username(profile, "smtp") payload["password"] = decrypt_secret(profile.smtp_password_encrypted) return SmtpConfig.model_validate(payload) def imap_config_from_profile(profile: MailServerProfile) -> ImapConfig | None: if not profile.imap_config: return None payload = dict(profile.imap_config or {}) payload.pop("username", None) payload.pop("enabled", None) payload["username"] = _profile_username(profile, "imap") payload["password"] = decrypt_secret(profile.imap_password_encrypted) return ImapConfig.model_validate(payload) def inherit_profile_credentials(server: dict[str, Any], protocol: str) -> bool: return _legacy_profile_credentials_inherited(server, protocol) def _transport_credentials_from_server(server: dict[str, Any], protocol: str) -> tuple[str | None, str | None]: credentials = server.get("credentials") if isinstance(server.get("credentials"), dict) else {} config = credentials.get(protocol) if isinstance(credentials.get(protocol), dict) else None legacy_config = server.get(protocol) if isinstance(server.get(protocol), dict) else None username = config.get("username") if config is not None else None password = config.get("password") if config is not None else None if username in (None, "") and legacy_config is not None: username = legacy_config.get("username") if password in (None, "") and legacy_config is not None: password = legacy_config.get("password") return (str(username) if username not in (None, "") else None, str(password) if password not in (None, "") else None) def apply_campaign_credentials(payload: dict[str, Any], server: dict[str, Any], protocol: str) -> dict[str, Any]: username, password = _transport_credentials_from_server(server, protocol) payload["username"] = username payload["password"] = password return payload def _server_config_payload(value: dict[str, Any] | None) -> dict[str, Any]: payload = dict(value or {}) payload.pop("username", None) payload.pop("password", None) payload.pop("enabled", None) return payload def profile_response_payload(profile: MailServerProfile) -> dict[str, Any]: return { "id": profile.id, "tenant_id": profile.tenant_id, "scope_type": _profile_scope_type(profile), "scope_id": _profile_scope_id(profile), "name": profile.name, "slug": profile.slug, "description": profile.description, "is_active": profile.is_active, "smtp": _server_config_payload(profile.smtp_config), "imap": _server_config_payload(profile.imap_config) if profile.imap_config else None, "credentials": { "smtp": {"username": _profile_username(profile, "smtp")}, "imap": {"username": _profile_username(profile, "imap")}, }, "smtp_password_configured": bool(profile.smtp_password_encrypted), "imap_password_configured": bool(profile.imap_password_encrypted), "created_at": profile.created_at, "updated_at": profile.updated_at, } def get_mail_profile_policy( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None = None, ) -> dict[str, Any]: clean_scope = _normalize_scope_type(scope_type) if clean_scope == "system": return _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {} if clean_scope == "tenant": policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=scope_id or tenant_id) if policy is None: raise MailProfileError("Tenant policy not found") return policy if not scope_id: raise MailProfileError(f"{clean_scope.capitalize()} mail profile policy requires scope_id") if clean_scope == "user": policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=scope_id) if policy is None: raise MailProfileError("User policy not found") return policy if clean_scope == "group": policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=scope_id) if policy is None: raise MailProfileError("Group policy not found") return policy campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=scope_id) return _policy_from_attr(campaign.mail_profile_policy) def _validate_policy_against_parent(parent: EffectiveMailProfilePolicy, normalized: dict[str, Any]) -> None: parent_limits = parent.allow_lower_level_limits allowed_ids = _meaningful_allow_patterns(normalized.get("allowed_profile_ids") or []) if allowed_ids and not parent_limits.get("allowed_profile_ids", True): raise MailProfileError("Mail profile allow-list is locked by an ancestor policy") for key in ("allow_user_profiles", "allow_group_profiles", "allow_campaign_profiles"): if isinstance(normalized.get(key), bool) and not parent_limits.get(key, True): raise MailProfileError(f"{key} is locked by an ancestor policy") for kind in ("whitelist", "blacklist"): rules = normalized.get(kind) or {} if isinstance(rules, dict): for key in PROFILE_PATTERN_KEYS: if _clean_string_list(rules.get(key, [])) and not parent_limits.get(f"{kind}.{key}", True): raise MailProfileError(f"{kind}.{key} is locked by an ancestor policy") for protocol in ("smtp", "imap"): local = normalized.get(f"{protocol}_credentials") or {} local_inherit = local.get("inherit") if isinstance(local_inherit, bool) and not parent_limits.get(f"{protocol}_credentials.inherit", True): raise MailProfileError(f"{protocol.upper()} credential inheritance is locked by an ancestor policy") local_lower_limits = normalized.get("allow_lower_level_limits") or {} if isinstance(local_lower_limits, dict): for key, allowed in local_lower_limits.items(): if allowed is True and not parent_limits.get(key, True): raise MailProfileError(f"{key} lower-level limiting cannot be re-enabled below a locked ancestor policy") def set_mail_profile_policy( session: Session, *, tenant_id: str, scope_type: str, scope_id: str | None = None, policy: dict[str, Any] | None = None, ) -> dict[str, Any]: clean_scope = _normalize_scope_type(scope_type) normalized = normalize_mail_profile_policy(policy or {}) if clean_scope != "system": _validate_policy_against_parent( parent_mail_profile_policy(session, tenant_id=tenant_id, scope_type=clean_scope, scope_id=scope_id or (tenant_id if clean_scope == "tenant" else None)), normalized, ) if clean_scope == "system": return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system", scope_id=None, policy=normalized) if clean_scope == "tenant": if (scope_id and scope_id != tenant_id) or not _tenant_exists(session, tenant_id): raise MailProfileError("Tenant policy not found") return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=tenant_id, policy=normalized) if not scope_id: raise MailProfileError(f"{clean_scope.capitalize()} mail profile policy requires scope_id") if clean_scope == "user": if not _user_exists(session, tenant_id=tenant_id, user_id=scope_id): raise MailProfileError("User policy not found") return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=scope_id, policy=normalized) if clean_scope == "group": if not _group_exists(session, tenant_id=tenant_id, group_id=scope_id): raise MailProfileError("Group policy not found") return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=scope_id, policy=normalized) provider = _campaign_policy_provider() if provider is None: raise MailProfileError("Campaign module is not available") try: provider.set_campaign_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=scope_id, policy=normalized) except ValueError as exc: raise MailProfileError("Campaign policy not found") from exc return normalized def _write_transport_to_server(server: dict[str, Any], protocol: str, payload: dict[str, Any]) -> None: server_payload = dict(payload) username = server_payload.pop("username", None) password = server_payload.pop("password", None) server_payload.pop("enabled", None) server[protocol] = server_payload if username not in (None, "") or password not in (None, ""): credentials = server.get("credentials") if isinstance(server.get("credentials"), dict) else {} protocol_credentials = credentials.get(protocol) if isinstance(credentials.get(protocol), dict) else {} if username not in (None, ""): protocol_credentials["username"] = username if password not in (None, ""): protocol_credentials["password"] = password credentials[protocol] = protocol_credentials server["credentials"] = credentials def materialize_campaign_mail_profile_config( session: Session, *, tenant_id: str, raw_json: dict[str, Any], campaign_id: str | None = None, owner_user_id: str | None = None, owner_group_id: str | None = None, ) -> dict[str, Any]: data = copy.deepcopy(raw_json if isinstance(raw_json, dict) else {}) server = data.get("server") if not isinstance(server, dict): return data profile_id = server.get("mail_profile_id") if not profile_id: assert_campaign_mail_policy_allows_json( session, tenant_id=tenant_id, raw_json=data, campaign_id=campaign_id, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) return data if campaign_id: profile = ensure_mail_profile_allowed_for_campaign(session, tenant_id=tenant_id, campaign_id=campaign_id, profile_id=str(profile_id), require_active=True) else: profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=str(profile_id), require_active=True) assert_campaign_mail_policy_allows_json( session, tenant_id=tenant_id, raw_json=data, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) policy = effective_mail_profile_policy( session, tenant_id=tenant_id, campaign_id=campaign_id, owner_user_id=owner_user_id, owner_group_id=owner_group_id, ) _assert_profile_credentials_allowed_for_server(profile, policy, server) resolved_server = dict(server) smtp_payload = smtp_config_from_profile(profile).model_dump(mode="json") if not profile_credentials_inherited_for_server(policy, server, "smtp"): smtp_payload = apply_campaign_credentials(smtp_payload, server, "smtp") _write_transport_to_server(resolved_server, "smtp", smtp_payload) imap = imap_config_from_profile(profile) if imap is not None: imap_payload = imap.model_dump(mode="json") if not profile_credentials_inherited_for_server(policy, server, "imap"): imap_payload = apply_campaign_credentials(imap_payload, server, "imap") _write_transport_to_server(resolved_server, "imap", imap_payload) else: resolved_server.pop("imap", None) data["server"] = resolved_server return data def mail_profile_id_from_campaign_json(raw_json: dict[str, Any] | None) -> str | None: server = raw_json.get("server") if isinstance(raw_json, dict) else None value = server.get("mail_profile_id") if isinstance(server, dict) else None return str(value) if value else None def group_ids_for_user(session: Session, *, tenant_id: str, user_id: str) -> list[str]: directory = _access_directory() if directory is not None: return [group.id for group in directory.groups_for_user(user_id, tenant_id=tenant_id)] rows = session.execute( text("select group_id from access_user_group_memberships where tenant_id = :tenant_id and user_id = :user_id"), {"tenant_id": tenant_id, "user_id": user_id}, ).all() return [row[0] for row in rows]