feat: enforce Mail-owned campaign transport boundary
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import fnmatch
|
||||
import json
|
||||
import re
|
||||
@@ -11,6 +10,7 @@ from sqlalchemy import and_, or_, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.admin.settings import get_system_settings
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.core.access import CAPABILITY_ACCESS_DIRECTORY, AccessDirectory
|
||||
from govoplan_core.core.campaigns import (
|
||||
CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT,
|
||||
@@ -19,7 +19,7 @@ from govoplan_core.core.campaigns import (
|
||||
)
|
||||
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.db.models import MailProfilePolicy, MailServerProfile, new_uuid
|
||||
from govoplan_mail.backend.config import ImapConfig, SmtpConfig
|
||||
from govoplan_core.security.secrets import decrypt_secret, encrypt_secret
|
||||
|
||||
@@ -961,89 +961,44 @@ def _credential_policy_for_protocol(policy: EffectiveMailProfilePolicy, protocol
|
||||
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:
|
||||
_CAMPAIGN_MAIL_REFERENCE_KEYS = frozenset({"mail_profile_id"})
|
||||
|
||||
|
||||
def _campaign_mail_profile_reference_id(server: dict[str, Any]) -> str | None:
|
||||
unexpected = sorted(key for key in server if key not in _CAMPAIGN_MAIL_REFERENCE_KEYS)
|
||||
if unexpected:
|
||||
paths = ", ".join(f"server.{key}" for key in unexpected)
|
||||
raise MailProfileError(
|
||||
"Campaign JSON may only reference a Mail-owned profile through server.mail_profile_id; "
|
||||
f"remove campaign-local SMTP/IMAP settings ({paths}), select a Mail profile, and save a new campaign version."
|
||||
)
|
||||
value = server.get("mail_profile_id")
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise MailProfileError("server.mail_profile_id must be a non-empty Mail profile identifier")
|
||||
return value.strip()
|
||||
|
||||
|
||||
def _assert_campaign_inherits_profile_credentials(
|
||||
profile: MailServerProfile,
|
||||
policy: EffectiveMailProfilePolicy,
|
||||
) -> 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
|
||||
if not _credential_policy_for_protocol(policy, protocol).inherit:
|
||||
raise MailProfileError(
|
||||
f"Campaign delivery cannot use the selected profile because the effective {protocol.upper()} "
|
||||
"credential policy requires campaign-local credentials. Store the credentials on a Mail profile "
|
||||
"and change the policy to inherit them."
|
||||
)
|
||||
|
||||
|
||||
def assert_campaign_mail_policy_allows_json(
|
||||
@@ -1057,7 +1012,7 @@ def assert_campaign_mail_policy_allows_json(
|
||||
) -> 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
|
||||
profile_id = _campaign_mail_profile_reference_id(server)
|
||||
if profile_id:
|
||||
if campaign_id:
|
||||
profile = ensure_mail_profile_allowed_for_campaign(
|
||||
@@ -1068,7 +1023,7 @@ def assert_campaign_mail_policy_allows_json(
|
||||
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)
|
||||
_assert_campaign_inherits_profile_credentials(profile, policy)
|
||||
return
|
||||
profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=str(profile_id), require_active=True)
|
||||
policy = effective_mail_profile_policy(
|
||||
@@ -1085,25 +1040,9 @@ def assert_campaign_mail_policy_allows_json(
|
||||
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)
|
||||
_assert_campaign_inherits_profile_credentials(profile, policy)
|
||||
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,
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def create_mail_server_profile(
|
||||
@@ -1174,6 +1113,7 @@ def update_mail_server_profile(
|
||||
profile: MailServerProfile,
|
||||
*,
|
||||
user_id: str | None,
|
||||
api_key_id: str | None = None,
|
||||
tenant_id: str,
|
||||
name: str | None = None,
|
||||
slug: str | None = None,
|
||||
@@ -1205,7 +1145,15 @@ def update_mail_server_profile(
|
||||
|
||||
next_smtp, next_imap = _next_profile_transport_state(profile, smtp=smtp, imap=imap, clear_imap=clear_imap)
|
||||
_assert_profile_transport_allowed(session, tenant_id=tenant_id, profile=profile, smtp=next_smtp, imap=next_imap)
|
||||
_apply_profile_transport_update(profile, smtp=smtp, imap=imap, clear_imap=clear_imap)
|
||||
_apply_profile_transport_update(
|
||||
session,
|
||||
profile,
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
smtp=smtp,
|
||||
imap=imap,
|
||||
clear_imap=clear_imap,
|
||||
)
|
||||
profile.updated_by_user_id = user_id
|
||||
session.add(profile)
|
||||
session.flush()
|
||||
@@ -1248,36 +1196,148 @@ def _assert_profile_transport_allowed(
|
||||
|
||||
|
||||
def _apply_profile_transport_update(
|
||||
session: Session,
|
||||
profile: MailServerProfile,
|
||||
*,
|
||||
user_id: str | None,
|
||||
api_key_id: str | None,
|
||||
smtp: SmtpConfig | None,
|
||||
imap: ImapConfig | None,
|
||||
clear_imap: bool,
|
||||
) -> None:
|
||||
if smtp is not None:
|
||||
_apply_profile_transport_payload(profile, "smtp", smtp)
|
||||
_apply_profile_transport_payload(
|
||||
session,
|
||||
profile,
|
||||
"smtp",
|
||||
smtp,
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
)
|
||||
if clear_imap:
|
||||
had_imap_identity = bool(
|
||||
profile.imap_config
|
||||
or profile.imap_username
|
||||
or profile.imap_password_encrypted
|
||||
)
|
||||
_audit_profile_credential_mutation(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="imap",
|
||||
password_supplied=True,
|
||||
next_password=None,
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
)
|
||||
profile.imap_config = None
|
||||
profile.imap_username = None
|
||||
profile.imap_password_encrypted = None
|
||||
if had_imap_identity:
|
||||
profile.imap_transport_revision = new_uuid()
|
||||
elif imap is not None:
|
||||
_apply_profile_transport_payload(profile, "imap", imap)
|
||||
_apply_profile_transport_payload(
|
||||
session,
|
||||
profile,
|
||||
"imap",
|
||||
imap,
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
)
|
||||
|
||||
|
||||
def _apply_profile_transport_payload(profile: MailServerProfile, protocol: str, config: SmtpConfig | ImapConfig) -> None:
|
||||
def _apply_profile_transport_payload(
|
||||
session: Session,
|
||||
profile: MailServerProfile,
|
||||
protocol: str,
|
||||
config: SmtpConfig | ImapConfig,
|
||||
*,
|
||||
user_id: str | None,
|
||||
api_key_id: str | None,
|
||||
) -> None:
|
||||
payload, username, password, username_supplied, password_supplied = _transport_payload(config)
|
||||
current_payload = dict(profile.smtp_config or {}) if protocol == "smtp" else dict(profile.imap_config or {})
|
||||
for field_name in ("username", "password", "enabled"):
|
||||
current_payload.pop(field_name, None)
|
||||
current_username = _profile_username(profile, protocol)
|
||||
current_password_encrypted = (
|
||||
profile.smtp_password_encrypted
|
||||
if protocol == "smtp"
|
||||
else profile.imap_password_encrypted
|
||||
)
|
||||
current_password = decrypt_secret(current_password_encrypted) if password_supplied else None
|
||||
identity_changed = (
|
||||
current_payload != payload
|
||||
or (username_supplied and current_username != username)
|
||||
)
|
||||
_audit_profile_credential_mutation(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol=protocol,
|
||||
password_supplied=password_supplied and current_password != password,
|
||||
next_password=password,
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
)
|
||||
if protocol == "smtp":
|
||||
profile.smtp_config = payload
|
||||
if username_supplied:
|
||||
profile.smtp_username = username
|
||||
if password_supplied:
|
||||
if password_supplied and current_password != password:
|
||||
profile.smtp_password_encrypted = encrypt_secret(password)
|
||||
if identity_changed:
|
||||
profile.smtp_transport_revision = new_uuid()
|
||||
return
|
||||
profile.imap_config = payload
|
||||
if username_supplied:
|
||||
profile.imap_username = username
|
||||
if password_supplied:
|
||||
if password_supplied and current_password != password:
|
||||
profile.imap_password_encrypted = encrypt_secret(password)
|
||||
if identity_changed:
|
||||
profile.imap_transport_revision = new_uuid()
|
||||
|
||||
|
||||
def _audit_profile_credential_mutation(
|
||||
session: Session,
|
||||
*,
|
||||
profile: MailServerProfile,
|
||||
protocol: str,
|
||||
password_supplied: bool,
|
||||
next_password: str | None,
|
||||
user_id: str | None,
|
||||
api_key_id: str | None,
|
||||
) -> None:
|
||||
encrypted = (
|
||||
profile.smtp_password_encrypted
|
||||
if protocol == "smtp"
|
||||
else profile.imap_password_encrypted
|
||||
)
|
||||
if not password_supplied or not encrypted:
|
||||
return
|
||||
replacing = next_password not in (None, "")
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=profile.tenant_id,
|
||||
scope="system" if profile.scope_type == "system" else "tenant",
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
action=(
|
||||
"mail.profile_credentials_replaced"
|
||||
if replacing
|
||||
else "mail.profile_credentials_deleted"
|
||||
),
|
||||
object_type="mail_server_profile",
|
||||
object_id=profile.id,
|
||||
details={
|
||||
"scope_type": profile.scope_type,
|
||||
"scope_id": profile.scope_id,
|
||||
"protocol": protocol,
|
||||
"reason": "profile_updated",
|
||||
},
|
||||
)
|
||||
# Force the non-secret audit record to durable storage before replacing or
|
||||
# clearing the in-memory ciphertext. The surrounding transaction keeps the
|
||||
# two changes atomic at commit time.
|
||||
session.flush()
|
||||
|
||||
|
||||
def _profile_username(profile: MailServerProfile, protocol: str) -> str | None:
|
||||
@@ -1307,28 +1367,100 @@ def imap_config_from_profile(profile: MailServerProfile) -> ImapConfig | None:
|
||||
return ImapConfig.model_validate(payload)
|
||||
|
||||
|
||||
def inherit_profile_credentials(server: dict[str, Any], protocol: str) -> bool:
|
||||
return _legacy_profile_credentials_inherited(server, protocol)
|
||||
def campaign_profile_transport_revisions(profile: MailServerProfile) -> dict[str, str | None]:
|
||||
"""Return Mail-owned opaque revisions without exposing transport identity.
|
||||
|
||||
Revisions are random persisted values, backfilled by migration and rotated
|
||||
whenever the corresponding transport or account identity changes.
|
||||
They cannot be dictionary-tested to recover a host or account name.
|
||||
"""
|
||||
|
||||
smtp_revision = str(getattr(profile, "smtp_transport_revision", "") or "")
|
||||
if not smtp_revision:
|
||||
raise MailProfileError("Mail profile has no SMTP transport revision; run the Mail database migrations")
|
||||
imap_revision = None
|
||||
if profile.imap_config:
|
||||
imap_revision = str(getattr(profile, "imap_transport_revision", "") or "")
|
||||
if not imap_revision:
|
||||
raise MailProfileError("Mail profile has no IMAP transport revision; run the Mail database migrations")
|
||||
return {"smtp": smtp_revision, "imap": imap_revision}
|
||||
|
||||
|
||||
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 delete_mail_profile_credentials(
|
||||
session: Session,
|
||||
*,
|
||||
profile: MailServerProfile,
|
||||
deletion_reason: str,
|
||||
user_id: str | None = None,
|
||||
api_key_id: str | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Immediately scrub Mail-owned profile secrets and record the deletion.
|
||||
|
||||
The audit record contains only protocol names and non-secret profile scope
|
||||
metadata. Any audit/storage failure aborts the caller's transaction, so a
|
||||
profile cannot appear deleted while its encrypted passwords remain.
|
||||
"""
|
||||
|
||||
deleted_protocols = tuple(
|
||||
protocol
|
||||
for protocol, encrypted in (
|
||||
("smtp", profile.smtp_password_encrypted),
|
||||
("imap", profile.imap_password_encrypted),
|
||||
)
|
||||
if encrypted
|
||||
)
|
||||
if not deleted_protocols:
|
||||
return ()
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=profile.tenant_id,
|
||||
scope="system" if profile.scope_type == "system" else "tenant",
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
action="mail.profile_credentials_deleted",
|
||||
object_type="mail_server_profile",
|
||||
object_id=profile.id,
|
||||
details={
|
||||
"scope_type": profile.scope_type,
|
||||
"scope_id": profile.scope_id,
|
||||
"deleted_protocols": list(deleted_protocols),
|
||||
"deletion_reason": deletion_reason,
|
||||
},
|
||||
)
|
||||
# Fail before mutating the ORM object if the audit record cannot be stored.
|
||||
session.flush()
|
||||
profile.smtp_password_encrypted = None
|
||||
profile.imap_password_encrypted = None
|
||||
session.add(profile)
|
||||
session.flush()
|
||||
return deleted_protocols
|
||||
|
||||
|
||||
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 delete_mail_profile_credentials_for_retirement(session: Session) -> int:
|
||||
"""Scrub and audit every remaining Mail-owned secret before table drop."""
|
||||
|
||||
profiles = (
|
||||
session.query(MailServerProfile)
|
||||
.filter(
|
||||
or_(
|
||||
MailServerProfile.smtp_password_encrypted.is_not(None),
|
||||
MailServerProfile.imap_password_encrypted.is_not(None),
|
||||
)
|
||||
)
|
||||
.order_by(MailServerProfile.id.asc())
|
||||
.all()
|
||||
)
|
||||
deleted = 0
|
||||
for profile in profiles:
|
||||
deleted += len(
|
||||
delete_mail_profile_credentials(
|
||||
session,
|
||||
profile=profile,
|
||||
deletion_reason="module_data_retired",
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
return deleted
|
||||
|
||||
|
||||
def _server_config_payload(value: dict[str, Any] | None) -> dict[str, Any]:
|
||||
@@ -1530,87 +1662,9 @@ def set_mail_profile_policy(
|
||||
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
|
||||
return _campaign_mail_profile_reference_id(server) if isinstance(server, dict) else None
|
||||
|
||||
|
||||
def group_ids_for_user(session: Session, *, tenant_id: str, user_id: str) -> list[str]:
|
||||
|
||||
Reference in New Issue
Block a user