Add hierarchical mail servers and credentials
This commit is contained in:
@@ -4,7 +4,7 @@ import fnmatch
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable
|
||||
from typing import Any, Iterable, Mapping
|
||||
|
||||
from sqlalchemy import and_, or_, select, text
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -919,6 +919,19 @@ def campaign_mail_context_visible_to_actor(
|
||||
)
|
||||
|
||||
|
||||
def campaign_mail_owner_context(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
campaign_id: str,
|
||||
) -> CampaignMailPolicyContext:
|
||||
return _campaign_policy_context(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
|
||||
|
||||
def mail_profile_scope_visible_to_actor(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -1335,7 +1348,15 @@ def _profile_has_transport(profile: MailServerProfile, protocol: str) -> bool:
|
||||
return bool(profile.imap_config)
|
||||
|
||||
|
||||
_CAMPAIGN_MAIL_REFERENCE_KEYS = frozenset({"mail_profile_id"})
|
||||
_CAMPAIGN_MAIL_REFERENCE_KEYS = frozenset(
|
||||
{
|
||||
"mail_profile_id",
|
||||
"smtp_server_id",
|
||||
"smtp_credential_id",
|
||||
"imap_server_id",
|
||||
"imap_credential_id",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _campaign_mail_profile_reference_id(server: dict[str, Any]) -> str | None:
|
||||
@@ -1343,8 +1364,8 @@ def _campaign_mail_profile_reference_id(server: dict[str, Any]) -> str | None:
|
||||
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."
|
||||
"Campaign JSON may only reference Mail-owned profile, server, and credential identifiers; "
|
||||
f"remove campaign-local SMTP/IMAP settings ({paths}), select Mail resources, and save a new campaign version."
|
||||
)
|
||||
value = server.get("mail_profile_id")
|
||||
if value is None:
|
||||
@@ -1354,18 +1375,63 @@ def _campaign_mail_profile_reference_id(server: dict[str, Any]) -> str | None:
|
||||
return value.strip()
|
||||
|
||||
|
||||
def campaign_mail_selection_from_json(
|
||||
raw_json: dict[str, Any] | None,
|
||||
) -> dict[str, str | None]:
|
||||
data = raw_json if isinstance(raw_json, dict) else {}
|
||||
server = data.get("server") if isinstance(data.get("server"), dict) else {}
|
||||
profile_id = _campaign_mail_profile_reference_id(server)
|
||||
selection: dict[str, str | None] = {"mail_profile_id": profile_id}
|
||||
for key in (
|
||||
"smtp_server_id",
|
||||
"smtp_credential_id",
|
||||
"imap_server_id",
|
||||
"imap_credential_id",
|
||||
):
|
||||
value = server.get(key)
|
||||
if value is None:
|
||||
selection[key] = None
|
||||
continue
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise MailProfileError(f"server.{key} must be a non-empty Mail identifier")
|
||||
selection[key] = value.strip()
|
||||
if profile_id is None and any(
|
||||
selection[key]
|
||||
for key in selection
|
||||
if key != "mail_profile_id"
|
||||
):
|
||||
raise MailProfileError(
|
||||
"Mail server or credential selections require server.mail_profile_id"
|
||||
)
|
||||
for protocol in ("smtp", "imap"):
|
||||
if (
|
||||
selection[f"{protocol}_credential_id"]
|
||||
and not selection[f"{protocol}_server_id"]
|
||||
):
|
||||
raise MailProfileError(
|
||||
f"server.{protocol}_credential_id requires server.{protocol}_server_id"
|
||||
)
|
||||
return selection
|
||||
|
||||
|
||||
def _assert_campaign_inherits_profile_credentials(
|
||||
profile: MailServerProfile,
|
||||
policy: EffectiveMailProfilePolicy,
|
||||
selection: Mapping[str, str | None] | None = None,
|
||||
) -> None:
|
||||
for protocol in ("smtp", "imap"):
|
||||
if not _profile_has_transport(profile, protocol):
|
||||
continue
|
||||
if not _credential_policy_for_protocol(policy, protocol).inherit:
|
||||
explicit_credential = (
|
||||
selection or {}
|
||||
).get(f"{protocol}_credential_id")
|
||||
if (
|
||||
not _credential_policy_for_protocol(policy, protocol).inherit
|
||||
and not explicit_credential
|
||||
):
|
||||
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."
|
||||
"credential policy requires an explicit credential selection for this campaign."
|
||||
)
|
||||
|
||||
|
||||
@@ -1379,8 +1445,8 @@ def assert_campaign_mail_policy_allows_json(
|
||||
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 = _campaign_mail_profile_reference_id(server)
|
||||
selection = campaign_mail_selection_from_json(data)
|
||||
profile_id = selection["mail_profile_id"]
|
||||
if profile_id:
|
||||
if campaign_id:
|
||||
profile = ensure_mail_profile_allowed_for_campaign(
|
||||
@@ -1391,7 +1457,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_campaign_inherits_profile_credentials(profile, policy)
|
||||
_assert_campaign_inherits_profile_credentials(profile, policy, selection)
|
||||
return
|
||||
profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=str(profile_id), require_active=True)
|
||||
policy = effective_mail_profile_policy(
|
||||
@@ -1408,7 +1474,7 @@ 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_campaign_inherits_profile_credentials(profile, policy)
|
||||
_assert_campaign_inherits_profile_credentials(profile, policy, selection)
|
||||
return
|
||||
return
|
||||
|
||||
@@ -1424,6 +1490,7 @@ def create_mail_server_profile(
|
||||
smtp: SmtpConfig,
|
||||
imap: ImapConfig | None,
|
||||
is_active: bool = True,
|
||||
inherit_to_lower_scopes: bool = True,
|
||||
scope_type: str = "tenant",
|
||||
scope_id: str | None = None,
|
||||
) -> MailServerProfile:
|
||||
@@ -1466,6 +1533,7 @@ def create_mail_server_profile(
|
||||
slug=clean_slug,
|
||||
description=description,
|
||||
is_active=is_active,
|
||||
inherit_to_lower_scopes=bool(inherit_to_lower_scopes),
|
||||
smtp_config=smtp_payload,
|
||||
smtp_username=smtp_username,
|
||||
smtp_password_encrypted=encrypt_secret(smtp_password),
|
||||
@@ -1491,6 +1559,7 @@ def update_mail_server_profile(
|
||||
slug: str | None = None,
|
||||
description: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
inherit_to_lower_scopes: bool | None = None,
|
||||
smtp: SmtpConfig | None = None,
|
||||
imap: ImapConfig | None = None,
|
||||
clear_imap: bool = False,
|
||||
@@ -1538,6 +1607,8 @@ def update_mail_server_profile(
|
||||
profile.description = description
|
||||
if is_active is not None:
|
||||
profile.is_active = is_active
|
||||
if inherit_to_lower_scopes is not None:
|
||||
profile.inherit_to_lower_scopes = bool(inherit_to_lower_scopes)
|
||||
|
||||
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)
|
||||
@@ -1883,6 +1954,9 @@ def profile_response_payload(profile: MailServerProfile) -> dict[str, Any]:
|
||||
"slug": profile.slug,
|
||||
"description": profile.description,
|
||||
"is_active": profile.is_active,
|
||||
"inherit_to_lower_scopes": bool(
|
||||
getattr(profile, "inherit_to_lower_scopes", True)
|
||||
),
|
||||
"smtp": _server_config_payload(profile.smtp_config),
|
||||
"imap": _server_config_payload(profile.imap_config) if profile.imap_config else None,
|
||||
"credentials": {
|
||||
|
||||
Reference in New Issue
Block a user