Files
govoplan-mail/src/govoplan_mail/backend/configuration_provider.py
T
zemion c62c7783d6
Module Package Release / publish-packages (push) Successful in 12s
feat: inventory SMTP infrastructure dependencies
2026-08-24 14:56:27 +02:00

1309 lines
45 KiB
Python

from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, replace
import re
from typing import Any
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from govoplan_core.core.configuration_packages import (
ConfigurationApplyResult,
ConfigurationDiagnostic,
ConfigurationExportResult,
ConfigurationExportSelection,
ConfigurationPackageFragment,
ConfigurationPlanItem,
ConfigurationPreflightContext,
ConfigurationPreflightResult,
ConfigurationProvider,
ConfigurationProviderDescription,
ConfigurationRequiredData,
)
from govoplan_core.core.infrastructure_capabilities import (
InfrastructureCapability,
InfrastructureCapabilityReceipt,
InfrastructureDependency,
InfrastructureDependencyProvider,
)
from govoplan_core.security.credential_envelopes import (
CredentialAccessContext,
CredentialEnvelope,
credential_visible_to_context,
)
from govoplan_core.db.session import get_database
from govoplan_mail.backend.config import SmtpConfig, SmtpServerConfig
from govoplan_mail.backend.db.models import (
MailServerCredentialBinding,
MailServerEndpoint,
MailServerProfile,
)
from govoplan_mail.backend.mail_profiles import (
create_mail_server_profile,
slugify_profile_name,
update_mail_server_profile,
)
from govoplan_mail.backend.server_hierarchy import (
bind_mail_credential,
initialize_profile_hierarchy,
mail_server_ref,
sync_default_profile_server,
)
MAIL_CONFIGURATION_CAPABILITY = "mail.configuration"
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY = (
"infrastructure.dependency_inventory.mail"
)
SMTP_PROFILE_FRAGMENT = "smtp_profile"
_SYSTEM_CONFIGURATION_SCOPES = frozenset(
{"system:settings:write", "system:governance:write"}
)
_PAYLOAD_KEYS = frozenset(
{
"capability_id",
"profile",
"smtp",
"credential_envelope_id",
"credential_is_default",
"input_keys",
"on_conflict",
}
)
_PROFILE_KEYS = frozenset(
{
"name",
"slug",
"description",
"scope_type",
"inherit_to_lower_scopes",
}
)
_SMTP_KEYS = frozenset({"host", "port", "security", "timeout_seconds"})
_INPUT_KEY_FIELDS = frozenset({"host", "port", "security", "credential_envelope_id"})
@dataclass(frozen=True, slots=True)
class _DesiredSmtpProfile:
fragment_ref: str
tenant_id: str | None
scope_type: str
scope_id: str | None
name: str
slug: str
description: str | None
inherit_to_lower_scopes: bool
smtp: SmtpConfig
credential_envelope_id: str | None
credential_is_default: bool
on_conflict: str
capability_source: str
installation_id: str
operator_user_id: str | None
@property
def smtp_payload(self) -> dict[str, object]:
payload = self.smtp.model_dump(mode="json")
payload.pop("username", None)
payload.pop("password", None)
return payload
@dataclass(frozen=True, slots=True)
class _ParsedMailFragment:
payload: Mapping[str, Any]
profile: Mapping[str, Any]
smtp: Mapping[str, Any]
input_keys: Mapping[str, Any]
@dataclass(frozen=True, slots=True)
class _ResolvedSmtpInputs:
smtp: SmtpConfig
credential_envelope_id: str | None
@dataclass(frozen=True, slots=True)
class _ProfileTarget:
tenant_id: str | None
scope_type: str
scope_id: str | None
name: str
slug: str
description: str | None
inherit_to_lower_scopes: bool
credential_is_default: bool
on_conflict: str
class SqlMailConfigurationProvider(
ConfigurationProvider,
InfrastructureDependencyProvider,
):
module_id = "mail"
capability_ids = ("mail.smtp",)
def describe(self) -> ConfigurationProviderDescription:
return ConfigurationProviderDescription(
module_id=self.module_id,
fragment_types=(SMTP_PROFILE_FRAGMENT,),
schema_refs={
SMTP_PROFILE_FRAGMENT: "govoplan/mail/configuration/smtp-profile.v1"
},
exported_scopes=("system", "tenant"),
)
def preflight(
self,
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> ConfigurationPreflightResult:
if fragment.fragment_type != SMTP_PROFILE_FRAGMENT:
return ConfigurationPreflightResult(
diagnostics=(_unsupported(fragment),),
plan=(_blocked_plan(fragment, "Fragment type is unsupported."),),
)
with get_database().session() as session:
return _preflight_smtp_profile(session, fragment, context)
def apply(
self,
fragment: ConfigurationPackageFragment,
supplied_data: Mapping[str, Any],
context: ConfigurationPreflightContext,
) -> ConfigurationApplyResult:
if fragment.fragment_type != SMTP_PROFILE_FRAGMENT:
return ConfigurationApplyResult(diagnostics=(_unsupported(fragment),))
apply_context = replace(context, supplied_data=supplied_data, dry_run=False)
with get_database().session() as session:
preflight = _preflight_smtp_profile(session, fragment, apply_context)
blockers = tuple(
item for item in preflight.diagnostics if item.severity == "blocker"
)
if blockers:
return ConfigurationApplyResult(diagnostics=blockers)
desired, diagnostics, _required_data = _desired_smtp_profile(
fragment,
apply_context,
)
if desired is None:
return ConfigurationApplyResult(diagnostics=tuple(diagnostics))
result = _apply_smtp_profile(session, desired)
if not any(item.severity == "blocker" for item in result.diagnostics):
session.commit()
return result
def export(
self,
selection: ConfigurationExportSelection,
context: ConfigurationPreflightContext,
) -> ConfigurationExportResult:
del context
with get_database().session() as session:
return _export_smtp_profiles(session, selection)
def health(
self,
import_result: ConfigurationApplyResult,
context: ConfigurationPreflightContext,
) -> tuple[ConfigurationDiagnostic, ...]:
del context
return tuple(
item for item in import_result.diagnostics if item.severity == "blocker"
)
def infrastructure_dependencies(self) -> tuple[InfrastructureDependency, ...]:
with get_database().session() as session:
return _smtp_infrastructure_dependencies(session)
def _smtp_infrastructure_dependencies(
session: Session,
) -> tuple[InfrastructureDependency, ...]:
endpoints = tuple(
session.execute(
select(MailServerEndpoint)
.where(MailServerEndpoint.protocol == "smtp")
.order_by(MailServerEndpoint.id)
).scalars()
)
binding_counts = {
str(server_id): int(count)
for server_id, count in session.execute(
select(
MailServerCredentialBinding.server_id,
func.count(MailServerCredentialBinding.id),
).group_by(MailServerCredentialBinding.server_id)
)
}
dependencies: list[InfrastructureDependency] = []
endpoint_profile_ids: set[str] = set()
for endpoint in endpoints:
endpoint_profile_ids.add(endpoint.profile_id)
dependencies.append(
InfrastructureDependency(
capability_id="mail.smtp",
module_id="mail",
dependency_type="smtp_endpoint",
dependency_ref=mail_server_ref(endpoint.id) or f"mail:{endpoint.id}",
state="active" if endpoint.is_active else "inactive",
scope=str(endpoint.scope_type or "tenant"),
summary=(
"A persisted Mail SMTP endpoint is bound to the deployment relay."
),
metrics={
"credential_binding_count": binding_counts.get(endpoint.id, 0),
"default_endpoint": int(bool(endpoint.is_default)),
},
required_action=(
"Rebind, migrate, or explicitly retire this SMTP endpoint and its credential-envelope references before changing the relay capability."
),
)
)
legacy_profiles = tuple(
session.execute(
select(MailServerProfile)
.where(MailServerProfile.smtp_config.is_not(None))
.order_by(MailServerProfile.id)
).scalars()
)
for profile in legacy_profiles:
if profile.id in endpoint_profile_ids or not dict(profile.smtp_config or {}):
continue
dependencies.append(
InfrastructureDependency(
capability_id="mail.smtp",
module_id="mail",
dependency_type="legacy_smtp_profile",
dependency_ref=f"mail-profile:{profile.id}",
state="active" if profile.is_active else "inactive",
scope=str(profile.scope_type or "tenant"),
summary=(
"A persisted legacy Mail profile still contains SMTP transport configuration."
),
metrics={"credential_binding_count": 0},
required_action=(
"Migrate or explicitly retire this legacy profile before changing the relay capability."
),
)
)
return tuple(dependencies)
def _preflight_smtp_profile(
session: Session,
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> ConfigurationPreflightResult:
desired, diagnostics, required_data = _desired_smtp_profile(fragment, context)
if desired is None:
return ConfigurationPreflightResult(
diagnostics=tuple(diagnostics),
required_data=tuple(required_data),
plan=(_blocked_plan(fragment, "SMTP profile inputs are incomplete."),),
)
profile = _profile_by_identity(session, desired)
if profile is None:
credential_diagnostics = _credential_diagnostics(
session,
desired,
server=None,
)
diagnostics.extend(credential_diagnostics)
action = "blocked" if _has_blockers(diagnostics) else "create"
return ConfigurationPreflightResult(
diagnostics=tuple(diagnostics),
required_data=tuple(required_data),
plan=(
ConfigurationPlanItem(
action=action,
module_id="mail",
fragment_type=SMTP_PROFILE_FRAGMENT,
fragment_id=desired.fragment_ref,
summary=(
"Create a receipt-bound SMTP profile and optional credential binding."
if action == "create"
else "SMTP profile creation is blocked."
),
),
),
)
server = _default_smtp_server(session, profile.id)
credential_diagnostics = _credential_diagnostics(
session,
desired,
server=server,
)
diagnostics.extend(credential_diagnostics)
changes = _profile_changes(session, profile, server, desired)
if changes and desired.on_conflict == "preserve":
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="mail_configuration_conflict",
message=(
f"Mail profile {desired.slug!r} already exists with different "
f"configuration ({', '.join(changes)})."
),
module_id="mail",
object_ref=desired.fragment_ref,
resolution=(
"Keep the existing profile, choose another stable slug, or review "
"the package with on_conflict set to update."
),
)
)
if _has_blockers(diagnostics):
action = "blocked"
summary = (
"Existing SMTP profile is preserved because the reviewed change is blocked."
)
elif changes:
action = "update"
summary = f"Update receipt-bound SMTP profile fields: {', '.join(changes)}."
else:
action = "skip"
summary = "SMTP profile and credential binding already match the receipt."
return ConfigurationPreflightResult(
diagnostics=tuple(diagnostics),
required_data=tuple(required_data),
plan=(
ConfigurationPlanItem(
action=action,
module_id="mail",
fragment_type=SMTP_PROFILE_FRAGMENT,
fragment_id=desired.fragment_ref,
summary=summary,
),
),
)
def _desired_smtp_profile(
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
) -> tuple[
_DesiredSmtpProfile | None,
list[ConfigurationDiagnostic],
list[ConfigurationRequiredData],
]:
diagnostics: list[ConfigurationDiagnostic] = []
parsed = _parse_mail_fragment(fragment, diagnostics)
if parsed is None:
return None, diagnostics, []
receipt_state = _mail_receipt_capability(
fragment,
context,
parsed.payload,
diagnostics,
)
if receipt_state is None:
return None, diagnostics, []
receipt, capability = receipt_state
resolved, required_data = _resolve_smtp_inputs(
fragment,
context,
parsed,
receipt,
capability,
diagnostics,
)
if resolved is None:
return None, diagnostics, required_data
target = _resolve_profile_target(
fragment,
context,
parsed,
receipt,
diagnostics,
)
if target is None:
return None, diagnostics, required_data
fragment_ref = (
fragment.fragment_id or f"{receipt.installation_id}:mail.smtp:{target.slug}"
)
return (
_DesiredSmtpProfile(
fragment_ref=fragment_ref,
tenant_id=target.tenant_id,
scope_type=target.scope_type,
scope_id=target.scope_id,
name=target.name,
slug=target.slug,
description=target.description,
inherit_to_lower_scopes=target.inherit_to_lower_scopes,
smtp=resolved.smtp,
credential_envelope_id=resolved.credential_envelope_id,
credential_is_default=target.credential_is_default,
on_conflict=target.on_conflict,
capability_source=capability.source,
installation_id=receipt.installation_id,
operator_user_id=context.operator_user_id,
),
diagnostics,
required_data,
)
def _parse_mail_fragment(
fragment: ConfigurationPackageFragment,
diagnostics: list[ConfigurationDiagnostic],
) -> _ParsedMailFragment | None:
payload = fragment.payload
if not isinstance(payload, Mapping):
diagnostics.append(
_invalid(fragment, "SMTP profile payload must be an object.")
)
return None
unknown_payload = sorted(set(payload) - _PAYLOAD_KEYS)
if unknown_payload:
diagnostics.append(_unknown_payload_diagnostic(fragment, unknown_payload))
profile = _mapping(
payload.get("profile"),
field_name="profile",
fragment=fragment,
diagnostics=diagnostics,
)
smtp = _mapping(
payload.get("smtp"),
field_name="smtp",
fragment=fragment,
diagnostics=diagnostics,
)
input_keys = _mapping(
payload.get("input_keys"),
field_name="input_keys",
fragment=fragment,
diagnostics=diagnostics,
)
_unknown_fields(profile, _PROFILE_KEYS, "profile", fragment, diagnostics)
unknown_smtp = sorted(set(smtp) - _SMTP_KEYS)
if unknown_smtp:
diagnostics.append(_unknown_smtp_diagnostic(fragment, unknown_smtp))
_unknown_fields(input_keys, _INPUT_KEY_FIELDS, "input_keys", fragment, diagnostics)
if _has_blockers(diagnostics):
return None
return _ParsedMailFragment(
payload=payload,
profile=profile,
smtp=smtp,
input_keys=input_keys,
)
def _mail_receipt_capability(
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
payload: Mapping[str, Any],
diagnostics: list[ConfigurationDiagnostic],
) -> tuple[InfrastructureCapabilityReceipt, InfrastructureCapability] | None:
if context.infrastructure_receipt_error:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_receipt_invalid",
message=f"The deployment capability receipt is invalid: {context.infrastructure_receipt_error}",
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Repair or regenerate the deployment receipt before importing Mail configuration.",
)
)
return None
receipt = context.infrastructure_receipt
if receipt is None:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_receipt_missing",
message="Mail SMTP configuration requires the mounted deployment capability receipt.",
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Mount the installer-generated capability receipt and rerun preflight.",
)
)
return None
capability_id = _text(payload.get("capability_id")) or "mail.smtp"
if capability_id != "mail.smtp":
diagnostics.append(
_invalid(
fragment,
"Mail SMTP fragments must reference capability 'mail.smtp'.",
)
)
return None
capability = receipt.capability(capability_id)
if capability is None:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_capability_missing",
message="The deployment receipt does not declare Mail SMTP infrastructure.",
module_id="mail",
object_ref=capability_id,
resolution="Regenerate the receipt from a deployment profile that declares Mail infrastructure.",
)
)
return None
if capability.state == "unavailable":
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_capability_unavailable",
message="The deployment receipt states that SMTP infrastructure is unavailable.",
module_id="mail",
object_ref=capability_id,
resolution="Reconfigure the deployment with test mail or an external SMTP relay before importing this profile.",
)
)
return None
return receipt, capability
def _resolve_smtp_inputs(
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
parsed: _ParsedMailFragment,
receipt: InfrastructureCapabilityReceipt,
capability: InfrastructureCapability,
diagnostics: list[ConfigurationDiagnostic],
) -> tuple[_ResolvedSmtpInputs | None, list[ConfigurationRequiredData]]:
prefix = _input_prefix(fragment, receipt.installation_id)
key_map = {
field: _text(parsed.input_keys.get(field)) or f"{prefix}.{field}"
for field in _INPUT_KEY_FIELDS
}
endpoint = capability.endpoint
values = {
field: _receipt_or_input_value(
field=field,
receipt_value=endpoint.get(field),
payload=parsed.smtp,
supplied_data=context.supplied_data,
input_key=key_map[field],
fragment=fragment,
diagnostics=diagnostics,
)
for field in ("host", "port", "security")
}
if values["security"] is None:
values["security"] = _inferred_security(
source=capability.source,
scheme=_text(endpoint.get("scheme")),
port=values["port"],
)
required_data = _smtp_required_data(values, key_map)
credential_envelope_id = _text(
parsed.payload.get("credential_envelope_id")
) or _text(context.supplied_data.get(key_map["credential_envelope_id"]))
credential_required = bool(capability.secret_refs)
required_data.append(
ConfigurationRequiredData(
key=key_map["credential_envelope_id"],
label="SMTP credential envelope id",
data_type="reference:credential-envelope",
required=credential_required and credential_envelope_id is None,
secret=False,
description=(
"An existing credential envelope is required by the receipt; plaintext credentials are not accepted."
if credential_required
else "Optional existing credential envelope; plaintext credentials are not accepted."
),
)
)
missing = [item for item in required_data if item.required]
if missing:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="mail_configuration_input_required",
message=(
"Mail SMTP configuration still needs reviewed operator data: "
+ ", ".join(item.label for item in missing)
+ "."
),
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Provide the listed non-secret values or credential-envelope reference and rerun preflight.",
)
)
return None, required_data
try:
smtp = SmtpConfig.model_validate(
{
**values,
"timeout_seconds": parsed.smtp.get("timeout_seconds", 30),
}
)
except Exception as exc:
diagnostics.append(
_invalid(fragment, f"SMTP server metadata is invalid: {exc}")
)
return None, required_data
return (
_ResolvedSmtpInputs(
smtp=smtp,
credential_envelope_id=credential_envelope_id,
),
required_data,
)
def _resolve_profile_target(
fragment: ConfigurationPackageFragment,
context: ConfigurationPreflightContext,
parsed: _ParsedMailFragment,
receipt: InfrastructureCapabilityReceipt,
diagnostics: list[ConfigurationDiagnostic],
) -> _ProfileTarget | None:
scope_type = (_text(parsed.profile.get("scope_type")) or "tenant").casefold()
if scope_type not in {"system", "tenant"}:
diagnostics.append(
_invalid(
fragment,
"Deployment SMTP profiles may use only system or tenant scope.",
)
)
return None
if scope_type == "tenant" and not context.tenant_id:
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="tenant_required",
message="A tenant-scoped Mail profile requires a selected tenant.",
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Select a tenant or review a system-scoped package with system authority.",
)
)
return None
if scope_type == "system" and not _SYSTEM_CONFIGURATION_SCOPES.intersection(
context.operator_scopes
):
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="system_configuration_authority_required",
message="Creating a system-wide Mail profile requires system configuration authority.",
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Use a tenant profile or ask a system administrator to apply the package.",
)
)
return None
on_conflict = (_text(parsed.payload.get("on_conflict")) or "preserve").casefold()
if on_conflict not in {"preserve", "update"}:
diagnostics.append(
_invalid(fragment, "on_conflict must be 'preserve' or 'update'.")
)
return None
name = _text(parsed.profile.get("name")) or (
f"Deployment SMTP ({receipt.installation_id})"
)
slug = slugify_profile_name(
_text(parsed.profile.get("slug"))
or f"deployment-{receipt.installation_id}-smtp"
)[:100]
return _ProfileTarget(
tenant_id=None if scope_type == "system" else context.tenant_id,
scope_type=scope_type,
scope_id=None if scope_type == "system" else context.tenant_id,
name=name[:255],
slug=slug,
description=(
_text(parsed.profile.get("description"))
or f"SMTP profile bound to deployment capability receipt {receipt.installation_id}."
),
inherit_to_lower_scopes=_bool(
parsed.profile.get("inherit_to_lower_scopes"),
default=True,
),
credential_is_default=_bool(
parsed.payload.get("credential_is_default"),
default=True,
),
on_conflict=on_conflict,
)
def _apply_smtp_profile(
session: Session,
desired: _DesiredSmtpProfile,
) -> ConfigurationApplyResult:
profile = _profile_by_identity(session, desired)
created: dict[str, str] = {}
updated: dict[str, str] = {}
if profile is None:
profile = create_mail_server_profile(
session,
tenant_id=desired.tenant_id or "system",
user_id=desired.operator_user_id,
name=desired.name,
slug=desired.slug,
description=desired.description,
smtp=desired.smtp,
imap=None,
is_active=True,
inherit_to_lower_scopes=desired.inherit_to_lower_scopes,
scope_type=desired.scope_type,
scope_id=desired.scope_id,
)
initialize_profile_hierarchy(
session,
profile=profile,
smtp=desired.smtp,
imap=None,
user_id=desired.operator_user_id,
)
created[desired.fragment_ref] = f"mail_profile:{profile.id}"
else:
server = _default_smtp_server(session, profile.id)
changes = _profile_changes(session, profile, server, desired)
if changes:
update_mail_server_profile(
session,
profile,
user_id=desired.operator_user_id,
tenant_id=desired.tenant_id or "system",
name=desired.name,
description=desired.description,
is_active=True if not profile.is_active else None,
inherit_to_lower_scopes=desired.inherit_to_lower_scopes,
smtp=desired.smtp,
)
sync_default_profile_server(
session,
profile=profile,
protocol="smtp",
config=desired.smtp_payload,
user_id=desired.operator_user_id,
)
updated[desired.fragment_ref] = f"mail_profile:{profile.id}"
server = _default_smtp_server(session, profile.id)
if server is None:
server = sync_default_profile_server(
session,
profile=profile,
protocol="smtp",
config=desired.smtp_payload,
user_id=desired.operator_user_id,
)
if desired.credential_envelope_id and server is not None:
credential = session.get(CredentialEnvelope, desired.credential_envelope_id)
if credential is None:
return ConfigurationApplyResult(
diagnostics=(
ConfigurationDiagnostic(
severity="blocker",
code="mail_credential_envelope_missing",
message="The reviewed SMTP credential envelope no longer exists.",
module_id="mail",
object_ref=desired.fragment_ref,
resolution="Rerun preflight and select an available credential envelope.",
),
)
)
binding = _credential_binding(
session,
server.id,
desired.credential_envelope_id,
)
was_default = bool(binding and binding.is_default)
bind_mail_credential(
session,
server=server,
credential=credential,
user_id=desired.operator_user_id,
is_default=desired.credential_is_default,
)
if binding is None or (desired.credential_is_default and not was_default):
updated.setdefault(
desired.fragment_ref,
f"mail_profile:{profile.id}",
)
return ConfigurationApplyResult(created_refs=created, updated_refs=updated)
def _export_smtp_profiles(
session: Session,
selection: ConfigurationExportSelection,
) -> ConfigurationExportResult:
clauses = []
if selection.tenant_id:
clauses.append(
(MailServerProfile.scope_type == "tenant")
& (MailServerProfile.scope_id == selection.tenant_id)
)
if "system" in selection.scopes:
clauses.append(MailServerProfile.scope_type == "system")
if not clauses:
return ConfigurationExportResult(
diagnostics=(
ConfigurationDiagnostic(
severity="blocker",
code="tenant_required",
message="Mail configuration export requires a tenant or system scope.",
module_id="mail",
resolution="Select a tenant or explicitly include system scope.",
),
)
)
statement = select(MailServerProfile)
if len(clauses) == 1:
statement = statement.where(clauses[0])
else:
statement = statement.where(or_(*clauses))
requested = {item.removeprefix("mail_profile:") for item in selection.object_refs}
if requested:
statement = statement.where(MailServerProfile.id.in_(requested))
profiles = session.execute(statement.order_by(MailServerProfile.slug)).scalars()
fragments: list[ConfigurationPackageFragment] = []
requirements: list[ConfigurationRequiredData] = []
for profile in profiles:
server = _default_smtp_server(session, profile.id)
smtp_payload = dict(
server.config if server is not None else profile.smtp_config or {}
)
for key in ("username", "password", "enabled"):
smtp_payload.pop(key, None)
fragment_id = f"mail-profile-{profile.slug}"
credential_key = f"mail.smtp.{_safe_key(profile.slug)}.credential_envelope_id"
fragments.append(
ConfigurationPackageFragment(
module_id="mail",
fragment_type=SMTP_PROFILE_FRAGMENT,
fragment_id=fragment_id,
payload={
"capability_id": "mail.smtp",
"profile": {
"name": profile.name,
"slug": profile.slug,
"description": profile.description,
"scope_type": profile.scope_type,
"inherit_to_lower_scopes": bool(
profile.inherit_to_lower_scopes
),
},
"smtp": smtp_payload,
"input_keys": {"credential_envelope_id": credential_key},
"on_conflict": "preserve",
},
)
)
requirements.append(
ConfigurationRequiredData(
key=credential_key,
label=f"Credential envelope for {profile.name}",
data_type="reference:credential-envelope",
required=False,
description="Select a target-environment credential envelope; credentials are never exported.",
)
)
return ConfigurationExportResult(
fragments=tuple(fragments),
data_requirements=tuple(requirements),
)
def _profile_by_identity(
session: Session,
desired: _DesiredSmtpProfile,
) -> MailServerProfile | None:
statement = select(MailServerProfile).where(
MailServerProfile.scope_type == desired.scope_type,
MailServerProfile.slug == desired.slug,
)
if desired.scope_id is None:
statement = statement.where(MailServerProfile.scope_id.is_(None))
else:
statement = statement.where(MailServerProfile.scope_id == desired.scope_id)
return session.execute(statement).scalar_one_or_none()
def _default_smtp_server(
session: Session,
profile_id: str,
) -> MailServerEndpoint | None:
return (
session.execute(
select(MailServerEndpoint)
.where(
MailServerEndpoint.profile_id == profile_id,
MailServerEndpoint.protocol == "smtp",
)
.order_by(
MailServerEndpoint.is_default.desc(),
MailServerEndpoint.is_active.desc(),
MailServerEndpoint.name,
MailServerEndpoint.id,
)
)
.scalars()
.first()
)
def _profile_changes(
session: Session,
profile: MailServerProfile,
server: MailServerEndpoint | None,
desired: _DesiredSmtpProfile,
) -> list[str]:
changes = _profile_metadata_changes(profile, desired)
changes.extend(_smtp_server_changes(profile, server, desired))
changes.extend(_credential_binding_changes(session, server, desired))
return list(dict.fromkeys(changes))
def _profile_metadata_changes(
profile: MailServerProfile,
desired: _DesiredSmtpProfile,
) -> list[str]:
changes: list[str] = []
if profile.name != desired.name:
changes.append("name")
if (profile.description or None) != (desired.description or None):
changes.append("description")
if not profile.is_active:
changes.append("active state")
if bool(profile.inherit_to_lower_scopes) != desired.inherit_to_lower_scopes:
changes.append("scope inheritance")
return changes
def _smtp_server_changes(
profile: MailServerProfile,
server: MailServerEndpoint | None,
desired: _DesiredSmtpProfile,
) -> list[str]:
changes: list[str] = []
current_payload = dict(
server.config if server is not None else profile.smtp_config or {}
)
for key in ("username", "password", "enabled"):
current_payload.pop(key, None)
try:
current_payload = SmtpServerConfig.model_validate(current_payload).model_dump(
mode="json"
)
except Exception:
changes.append("SMTP server metadata")
else:
if current_payload != desired.smtp_payload:
changes.append("SMTP server metadata")
if server is None:
changes.append("SMTP server hierarchy")
return changes
def _credential_binding_changes(
session: Session,
server: MailServerEndpoint | None,
desired: _DesiredSmtpProfile,
) -> list[str]:
if not desired.credential_envelope_id or server is None:
return []
binding = _credential_binding(
session,
server.id,
desired.credential_envelope_id,
)
if binding is None:
return ["credential binding"]
if desired.credential_is_default and not binding.is_default:
return ["default credential binding"]
return []
def _credential_diagnostics(
session: Session,
desired: _DesiredSmtpProfile,
*,
server: MailServerEndpoint | None,
) -> list[ConfigurationDiagnostic]:
credential_id = desired.credential_envelope_id
if not credential_id:
return []
credential = session.get(CredentialEnvelope, credential_id)
if (
credential is None
or credential.deleted_at is not None
or not credential.is_active
):
return [
ConfigurationDiagnostic(
severity="blocker",
code="mail_credential_envelope_missing",
message="The selected SMTP credential envelope is missing or inactive.",
module_id="mail",
object_ref=desired.fragment_ref,
resolution="Select an active credential envelope visible to this profile scope.",
)
]
server_ref = mail_server_ref(server.id) if server is not None else None
context = CredentialAccessContext(
tenant_id=desired.tenant_id,
target_scope_type=desired.scope_type,
target_scope_id=desired.scope_id,
module_id="mail",
server_ref=server_ref,
)
if not credential_visible_to_context(credential, context):
reason = (
"A server-restricted credential cannot be bound before the package-owned server exists."
if server is None and credential.allowed_server_refs
else "The selected credential is outside the profile scope or is not allowed for Mail and this server."
)
return [
ConfigurationDiagnostic(
severity="blocker",
code="mail_credential_envelope_not_available",
message=reason,
module_id="mail",
object_ref=desired.fragment_ref,
resolution="Choose a compatible credential envelope or update its governed scope and allowed references first.",
)
]
server_tenant_id = desired.tenant_id
if credential.tenant_id not in {None, server_tenant_id}:
return [
ConfigurationDiagnostic(
severity="blocker",
code="mail_credential_tenant_mismatch",
message="The credential envelope and SMTP profile belong to different tenants.",
module_id="mail",
object_ref=desired.fragment_ref,
resolution="Choose a system credential or a credential from the selected tenant.",
)
]
return []
def _credential_binding(
session: Session,
server_id: str,
credential_id: str,
) -> MailServerCredentialBinding | None:
return session.execute(
select(MailServerCredentialBinding).where(
MailServerCredentialBinding.server_id == server_id,
MailServerCredentialBinding.credential_id == credential_id,
)
).scalar_one_or_none()
def _receipt_or_input_value(
*,
field: str,
receipt_value: object,
payload: Mapping[str, Any],
supplied_data: Mapping[str, Any],
input_key: str,
fragment: ConfigurationPackageFragment,
diagnostics: list[ConfigurationDiagnostic],
) -> object:
explicit = payload.get(field)
if receipt_value is not None:
if explicit is not None and str(explicit) != str(receipt_value):
diagnostics.append(
ConfigurationDiagnostic(
severity="blocker",
code="infrastructure_endpoint_mismatch",
message=f"SMTP {field} conflicts with the deployment capability receipt.",
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Use the receipt endpoint or regenerate the deployment receipt before importing.",
)
)
return receipt_value
if explicit is not None:
return explicit
return supplied_data.get(input_key)
def _inferred_security(*, source: str, scheme: str | None, port: object) -> str | None:
if source == "installer-managed-test":
return "plain"
if scheme in {"smtps", "smtp+tls"} or str(port) == "465":
return "tls"
return None
def _smtp_required_data(
values: Mapping[str, object],
key_map: Mapping[str, str],
) -> list[ConfigurationRequiredData]:
requirements: list[ConfigurationRequiredData] = []
definitions = (
("host", "SMTP host", "string", "Non-secret SMTP server hostname."),
("port", "SMTP port", "integer", "SMTP server port from 1 to 65535."),
(
"security",
"SMTP transport security",
"enum:plain,starttls,tls",
"Choose plain, STARTTLS, or implicit TLS.",
),
)
for field, label, data_type, description in definitions:
value = values.get(field)
if value is None or (isinstance(value, str) and not value.strip()):
requirements.append(
ConfigurationRequiredData(
key=key_map[field],
label=label,
data_type=data_type,
description=description,
)
)
return requirements
def _unknown_payload_diagnostic(
fragment: ConfigurationPackageFragment,
unknown: list[str],
) -> ConfigurationDiagnostic:
if _contains_secret_field(unknown):
return ConfigurationDiagnostic(
severity="blocker",
code="mail_configuration_secret_forbidden",
message="SMTP configuration packages accept credential-envelope references, never inline credentials.",
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Remove inline credentials and select an existing credential envelope by id.",
)
return _invalid(
fragment,
f"SMTP profile payload contains unsupported fields: {', '.join(unknown)}.",
)
def _unknown_smtp_diagnostic(
fragment: ConfigurationPackageFragment,
unknown: list[str],
) -> ConfigurationDiagnostic:
if _contains_secret_field(unknown):
return ConfigurationDiagnostic(
severity="blocker",
code="mail_configuration_secret_forbidden",
message=(
"SMTP configuration packages accept non-secret server metadata "
"and credential-envelope references only."
),
module_id="mail",
object_ref=fragment.fragment_id or SMTP_PROFILE_FRAGMENT,
resolution="Remove inline credentials and select an existing credential envelope by id.",
)
return _invalid(
fragment,
f"SMTP payload contains unsupported fields: {', '.join(unknown)}.",
)
def _contains_secret_field(fields: list[str]) -> bool:
return any(
marker in field.casefold()
for field in fields
for marker in ("password", "secret", "token", "username")
)
def _mapping(
value: object,
*,
field_name: str,
fragment: ConfigurationPackageFragment,
diagnostics: list[ConfigurationDiagnostic],
) -> Mapping[str, Any]:
if value is None:
return {}
if isinstance(value, Mapping):
return value
diagnostics.append(_invalid(fragment, f"{field_name} must be an object."))
return {}
def _unknown_fields(
value: Mapping[str, Any],
allowed: frozenset[str],
field_name: str,
fragment: ConfigurationPackageFragment,
diagnostics: list[ConfigurationDiagnostic],
) -> None:
unknown = sorted(set(value) - allowed)
if unknown:
diagnostics.append(
_invalid(
fragment,
f"{field_name} contains unsupported fields: {', '.join(unknown)}.",
)
)
def _input_prefix(fragment: ConfigurationPackageFragment, installation_id: str) -> str:
return f"mail.smtp.{_safe_key(fragment.fragment_id or installation_id)}"
def _safe_key(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-.") or "deployment"
def _text(value: object) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None
def _bool(value: object, *, default: bool) -> bool:
if value is None:
return default
if isinstance(value, bool):
return value
return str(value).strip().casefold() in {"1", "true", "yes", "on"}
def _has_blockers(diagnostics: list[ConfigurationDiagnostic]) -> bool:
return any(item.severity == "blocker" for item in diagnostics)
def _invalid(
fragment: ConfigurationPackageFragment,
message: str,
) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="mail_configuration_payload_invalid",
message=message,
module_id="mail",
object_ref=fragment.fragment_id or fragment.fragment_type,
resolution="Review the Mail configuration-package fragment and rerun preflight.",
)
def _unsupported(fragment: ConfigurationPackageFragment) -> ConfigurationDiagnostic:
return ConfigurationDiagnostic(
severity="blocker",
code="fragment_type_unsupported",
message=f"Mail configuration does not support fragment type {fragment.fragment_type!r}.",
module_id="mail",
object_ref=fragment.fragment_id or fragment.fragment_type,
)
def _blocked_plan(
fragment: ConfigurationPackageFragment,
summary: str,
) -> ConfigurationPlanItem:
return ConfigurationPlanItem(
action="blocked",
module_id="mail",
fragment_type=fragment.fragment_type,
fragment_id=fragment.fragment_id,
summary=summary,
)
__all__ = ["MAIL_CONFIGURATION_CAPABILITY", "SqlMailConfigurationProvider"]