Files
govoplan-mail/src/govoplan_mail/backend/mail_profiles.py

1503 lines
64 KiB
Python

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]