Add hierarchical mail servers and credentials
This commit is contained in:
@@ -11,6 +11,7 @@ from govoplan_mail.backend.mail_profiles import (
|
||||
_assert_campaign_inherits_profile_credentials,
|
||||
assert_campaign_mail_policy_allows_json,
|
||||
assert_mail_policy_allows_send,
|
||||
campaign_mail_owner_context,
|
||||
campaign_profile_transport_revisions,
|
||||
effective_mail_profile_policy,
|
||||
ensure_mail_profile_allowed_for_campaign,
|
||||
@@ -19,6 +20,12 @@ from govoplan_mail.backend.mail_profiles import (
|
||||
mail_profile_id_from_campaign_json,
|
||||
smtp_config_from_profile,
|
||||
)
|
||||
from govoplan_mail.backend.server_hierarchy import (
|
||||
MailHierarchyContext,
|
||||
MailServerHierarchyError,
|
||||
resolve_mail_transport,
|
||||
select_mail_transport,
|
||||
)
|
||||
from govoplan_mail.backend.runtime import configure_runtime
|
||||
from govoplan_mail.backend.sending.imap import (
|
||||
ImapAppendError,
|
||||
@@ -104,6 +111,7 @@ def _authorized_campaign_profile(
|
||||
tenant_id: str,
|
||||
campaign_id: str,
|
||||
profile_id: str,
|
||||
selection: dict[str, str | None] | None = None,
|
||||
):
|
||||
profile = ensure_mail_profile_allowed_for_campaign(
|
||||
session,
|
||||
@@ -113,10 +121,55 @@ def _authorized_campaign_profile(
|
||||
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
|
||||
|
||||
|
||||
def _campaign_hierarchy_context(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
campaign_id: str,
|
||||
) -> MailHierarchyContext:
|
||||
campaign = campaign_mail_owner_context(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
return MailHierarchyContext(
|
||||
tenant_id=tenant_id,
|
||||
user_id=campaign.owner_user_id,
|
||||
group_ids=(
|
||||
frozenset({campaign.owner_group_id})
|
||||
if campaign.owner_group_id
|
||||
else frozenset()
|
||||
),
|
||||
target_scope_type="campaign",
|
||||
target_scope_id=campaign.id,
|
||||
)
|
||||
|
||||
|
||||
def _selection_payload(
|
||||
*,
|
||||
profile_id: str,
|
||||
smtp_server_id: str | None = None,
|
||||
smtp_credential_id: str | None = None,
|
||||
imap_server_id: str | None = None,
|
||||
imap_credential_id: str | None = None,
|
||||
) -> dict[str, str | None]:
|
||||
return {
|
||||
"mail_profile_id": profile_id,
|
||||
"smtp_server_id": smtp_server_id,
|
||||
"smtp_credential_id": smtp_credential_id,
|
||||
"imap_server_id": imap_server_id,
|
||||
"imap_credential_id": imap_credential_id,
|
||||
}
|
||||
|
||||
|
||||
def _supports_hierarchy(session: object) -> bool:
|
||||
return callable(getattr(session, "execute", None))
|
||||
|
||||
|
||||
def campaign_profile_delivery_summary(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -125,21 +178,33 @@ def campaign_profile_delivery_summary(
|
||||
campaign_id: str | None = None,
|
||||
owner_user_id: str | None = None,
|
||||
owner_group_id: str | None = None,
|
||||
smtp_server_id: str | None = None,
|
||||
smtp_credential_id: str | None = None,
|
||||
imap_server_id: str | None = None,
|
||||
imap_credential_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return only non-secret capabilities and opaque drift evidence."""
|
||||
|
||||
selection = _selection_payload(
|
||||
profile_id=profile_id,
|
||||
smtp_server_id=smtp_server_id,
|
||||
smtp_credential_id=smtp_credential_id,
|
||||
imap_server_id=imap_server_id,
|
||||
imap_credential_id=imap_credential_id,
|
||||
)
|
||||
if campaign_id:
|
||||
profile = _authorized_campaign_profile(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
profile_id=profile_id,
|
||||
selection=selection,
|
||||
)
|
||||
else:
|
||||
assert_campaign_mail_policy_allows_json(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
raw_json={"server": {"mail_profile_id": profile_id}},
|
||||
raw_json={"server": {key: value for key, value in selection.items() if value}},
|
||||
owner_user_id=owner_user_id,
|
||||
owner_group_id=owner_group_id,
|
||||
)
|
||||
@@ -149,15 +214,61 @@ def campaign_profile_delivery_summary(
|
||||
profile_id=profile_id,
|
||||
require_active=True,
|
||||
)
|
||||
revisions = campaign_profile_transport_revisions(profile)
|
||||
smtp = profile.smtp_config or {}
|
||||
imap = profile.imap_config or {}
|
||||
if campaign_id and _supports_hierarchy(session):
|
||||
context = _campaign_hierarchy_context(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
try:
|
||||
smtp = select_mail_transport(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="smtp",
|
||||
context=context,
|
||||
server_id=smtp_server_id,
|
||||
credential_id=smtp_credential_id,
|
||||
)
|
||||
imap = select_mail_transport(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="imap",
|
||||
context=context,
|
||||
server_id=imap_server_id,
|
||||
credential_id=imap_credential_id,
|
||||
)
|
||||
except MailServerHierarchyError as exc:
|
||||
raise MailProfileError(str(exc)) from exc
|
||||
smtp_available = smtp.available
|
||||
imap_available = imap.available
|
||||
smtp_revision = smtp.transport_revision
|
||||
imap_revision = imap.transport_revision if imap.available else None
|
||||
resolved_smtp_server_id = smtp.server.id if smtp.server else None
|
||||
resolved_smtp_credential_id = smtp.credential.id if smtp.credential else None
|
||||
resolved_imap_server_id = imap.server.id if imap.server else None
|
||||
resolved_imap_credential_id = imap.credential.id if imap.credential else None
|
||||
else:
|
||||
revisions = campaign_profile_transport_revisions(profile)
|
||||
smtp_config = profile.smtp_config or {}
|
||||
imap_config = profile.imap_config or {}
|
||||
smtp_available = bool(smtp_config.get("host") and smtp_config.get("port"))
|
||||
imap_available = bool(imap_config.get("host") and imap_config.get("port"))
|
||||
smtp_revision = revisions["smtp"]
|
||||
imap_revision = revisions["imap"]
|
||||
resolved_smtp_server_id = smtp_server_id
|
||||
resolved_smtp_credential_id = smtp_credential_id
|
||||
resolved_imap_server_id = imap_server_id
|
||||
resolved_imap_credential_id = imap_credential_id
|
||||
return {
|
||||
"mail_profile_id": profile_id,
|
||||
"smtp_available": bool(smtp.get("host") and smtp.get("port")),
|
||||
"imap_available": bool(imap.get("host") and imap.get("port")),
|
||||
"smtp_transport_revision": revisions["smtp"],
|
||||
"imap_transport_revision": revisions["imap"],
|
||||
"smtp_server_id": resolved_smtp_server_id,
|
||||
"smtp_credential_id": resolved_smtp_credential_id,
|
||||
"imap_server_id": resolved_imap_server_id,
|
||||
"imap_credential_id": resolved_imap_credential_id,
|
||||
"smtp_available": smtp_available,
|
||||
"imap_available": imap_available,
|
||||
"smtp_transport_revision": smtp_revision,
|
||||
"imap_transport_revision": imap_revision,
|
||||
}
|
||||
|
||||
|
||||
@@ -172,26 +283,65 @@ def send_campaign_email_bytes(
|
||||
envelope_recipients: list[str],
|
||||
from_header: str | None,
|
||||
expected_smtp_transport_revision: str,
|
||||
smtp_server_id: str | None = None,
|
||||
smtp_credential_id: str | None = None,
|
||||
) -> CampaignSmtpDeliveryResult:
|
||||
selection = _selection_payload(
|
||||
profile_id=profile_id,
|
||||
smtp_server_id=smtp_server_id,
|
||||
smtp_credential_id=smtp_credential_id,
|
||||
)
|
||||
try:
|
||||
profile = _authorized_campaign_profile(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
profile_id=profile_id,
|
||||
selection=selection,
|
||||
)
|
||||
except MailProfileError:
|
||||
raise
|
||||
except Exception:
|
||||
raise SmtpConfigurationError("The selected Mail profile's SMTP configuration is unusable.") from None
|
||||
revisions = campaign_profile_transport_revisions(profile)
|
||||
if revisions["smtp"] != expected_smtp_transport_revision:
|
||||
resolved_smtp = None
|
||||
if _supports_hierarchy(session):
|
||||
context = _campaign_hierarchy_context(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
try:
|
||||
selected_smtp = select_mail_transport(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="smtp",
|
||||
context=context,
|
||||
server_id=smtp_server_id,
|
||||
credential_id=smtp_credential_id,
|
||||
)
|
||||
except MailServerHierarchyError as exc:
|
||||
raise MailProfileError(str(exc)) from exc
|
||||
current_smtp_revision = selected_smtp.transport_revision
|
||||
else:
|
||||
current_smtp_revision = campaign_profile_transport_revisions(profile)["smtp"]
|
||||
if current_smtp_revision != expected_smtp_transport_revision:
|
||||
raise MailProfileError(
|
||||
"The selected Mail profile's SMTP settings changed after this campaign was built. "
|
||||
"Revalidate and rebuild the campaign before delivery."
|
||||
)
|
||||
try:
|
||||
smtp = smtp_config_from_profile(profile)
|
||||
if _supports_hierarchy(session):
|
||||
resolved_smtp = resolve_mail_transport(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="smtp",
|
||||
context=context,
|
||||
server_id=smtp_server_id,
|
||||
credential_id=smtp_credential_id,
|
||||
)
|
||||
smtp = resolved_smtp.config
|
||||
else:
|
||||
smtp = smtp_config_from_profile(profile)
|
||||
except MailProfileError:
|
||||
raise
|
||||
except Exception:
|
||||
@@ -202,7 +352,7 @@ def send_campaign_email_bytes(
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
smtp=smtp,
|
||||
imap=profile.imap_config or None,
|
||||
imap=None,
|
||||
envelope_sender=envelope_from,
|
||||
from_header=from_header,
|
||||
recipients=envelope_recipients,
|
||||
@@ -241,31 +391,83 @@ def append_campaign_message_to_sent(
|
||||
folder: str | None,
|
||||
expected_smtp_transport_revision: str,
|
||||
expected_imap_transport_revision: str | None,
|
||||
smtp_server_id: str | None = None,
|
||||
smtp_credential_id: str | None = None,
|
||||
imap_server_id: str | None = None,
|
||||
imap_credential_id: str | None = None,
|
||||
) -> CampaignImapAppendResult:
|
||||
selection = _selection_payload(
|
||||
profile_id=profile_id,
|
||||
smtp_server_id=smtp_server_id,
|
||||
smtp_credential_id=smtp_credential_id,
|
||||
imap_server_id=imap_server_id,
|
||||
imap_credential_id=imap_credential_id,
|
||||
)
|
||||
try:
|
||||
profile = _authorized_campaign_profile(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
profile_id=profile_id,
|
||||
selection=selection,
|
||||
)
|
||||
except MailProfileError:
|
||||
raise
|
||||
except Exception:
|
||||
raise ImapConfigurationError("The selected Mail profile's IMAP configuration is unusable.") from None
|
||||
revisions = campaign_profile_transport_revisions(profile)
|
||||
if revisions["smtp"] != expected_smtp_transport_revision:
|
||||
if _supports_hierarchy(session):
|
||||
context = _campaign_hierarchy_context(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
try:
|
||||
selected_smtp = select_mail_transport(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="smtp",
|
||||
context=context,
|
||||
server_id=smtp_server_id,
|
||||
credential_id=smtp_credential_id,
|
||||
)
|
||||
selected_imap = select_mail_transport(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="imap",
|
||||
context=context,
|
||||
server_id=imap_server_id,
|
||||
credential_id=imap_credential_id,
|
||||
)
|
||||
except MailServerHierarchyError as exc:
|
||||
raise MailProfileError(str(exc)) from exc
|
||||
smtp_revision = selected_smtp.transport_revision
|
||||
imap_revision = selected_imap.transport_revision
|
||||
else:
|
||||
revisions = campaign_profile_transport_revisions(profile)
|
||||
smtp_revision = revisions["smtp"]
|
||||
imap_revision = revisions["imap"]
|
||||
if smtp_revision != expected_smtp_transport_revision:
|
||||
raise MailProfileError(
|
||||
"The selected Mail profile's SMTP settings changed after this campaign was built. "
|
||||
"Revalidate and rebuild the campaign before append-to-Sent delivery."
|
||||
)
|
||||
if revisions["imap"] != expected_imap_transport_revision:
|
||||
if imap_revision != expected_imap_transport_revision:
|
||||
raise MailProfileError(
|
||||
"The selected Mail profile's IMAP settings changed after this campaign was built. "
|
||||
"Revalidate and rebuild the campaign before append-to-Sent delivery."
|
||||
)
|
||||
try:
|
||||
imap = imap_config_from_profile(profile)
|
||||
if _supports_hierarchy(session):
|
||||
imap = resolve_mail_transport(
|
||||
session,
|
||||
profile=profile,
|
||||
protocol="imap",
|
||||
context=context,
|
||||
server_id=imap_server_id,
|
||||
credential_id=imap_credential_id,
|
||||
).config
|
||||
else:
|
||||
imap = imap_config_from_profile(profile)
|
||||
except MailProfileError:
|
||||
raise
|
||||
except Exception:
|
||||
@@ -277,7 +479,7 @@ def append_campaign_message_to_sent(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
campaign_id=campaign_id,
|
||||
smtp=profile.smtp_config or None,
|
||||
smtp=None,
|
||||
imap=imap,
|
||||
)
|
||||
except MailProfileError:
|
||||
|
||||
Reference in New Issue
Block a user