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:
|
||||
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Index, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from govoplan_core.db.base import Base, TimestampMixin
|
||||
from govoplan_core.security import credential_envelopes as core_credential_models # noqa: F401
|
||||
|
||||
|
||||
def new_uuid() -> str:
|
||||
@@ -30,6 +31,7 @@ class MailServerProfile(Base, TimestampMixin):
|
||||
slug: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True)
|
||||
inherit_to_lower_scopes: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
smtp_config: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
smtp_username: Mapped[str | None] = mapped_column(String(320))
|
||||
smtp_password_encrypted: Mapped[str | None] = mapped_column(Text)
|
||||
@@ -42,6 +44,56 @@ class MailServerProfile(Base, TimestampMixin):
|
||||
updated_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
|
||||
|
||||
class MailServerEndpoint(Base, TimestampMixin):
|
||||
__tablename__ = "mail_server_endpoints"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("profile_id", "protocol", "name", name="uq_mail_server_endpoints_profile_protocol_name"),
|
||||
Index("ix_mail_server_endpoints_profile_protocol", "profile_id", "protocol", "is_active"),
|
||||
Index("ix_mail_server_endpoints_scope", "tenant_id", "scope_type", "scope_id"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
profile_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("mail_server_profiles.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
tenant_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
||||
protocol: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
config: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
scope_type: Mapped[str] = mapped_column(String(20), default="tenant", nullable=False, index=True)
|
||||
scope_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
||||
inherit_to_lower_scopes: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True)
|
||||
transport_revision: Mapped[str] = mapped_column(String(36), default=new_uuid, nullable=False)
|
||||
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
updated_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
|
||||
|
||||
class MailServerCredentialBinding(Base, TimestampMixin):
|
||||
__tablename__ = "mail_server_credential_bindings"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("server_id", "credential_id", name="uq_mail_server_credential_bindings_server_credential"),
|
||||
Index("ix_mail_server_credential_bindings_default", "server_id", "is_default"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
server_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("mail_server_endpoints.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
credential_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("core_credential_envelopes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
|
||||
created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
|
||||
|
||||
class MailProfilePolicy(Base, TimestampMixin):
|
||||
__tablename__ = "mail_profile_policies"
|
||||
__table_args__ = (
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -27,6 +27,8 @@ from govoplan_mail.backend.db import models as mail_models # noqa: F401 - popul
|
||||
|
||||
|
||||
_mail_table_retirement_provider = drop_table_retirement_provider(
|
||||
mail_models.MailServerCredentialBinding,
|
||||
mail_models.MailServerEndpoint,
|
||||
mail_models.MailServerProfile,
|
||||
mail_models.MailProfilePolicy,
|
||||
mail_models.MailMailboxFolderIndex,
|
||||
@@ -194,6 +196,8 @@ manifest = ModuleManifest(
|
||||
),
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
mail_models.MailServerCredentialBinding,
|
||||
mail_models.MailServerEndpoint,
|
||||
mail_models.MailServerProfile,
|
||||
mail_models.MailProfilePolicy,
|
||||
mail_models.MailMailboxFolderIndex,
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
"""split mail envelopes into servers and reusable credential bindings
|
||||
|
||||
Revision ID: 7192a3bcdef0
|
||||
Revises: 608192abcdef
|
||||
Create Date: 2026-07-23 00:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
|
||||
hierarchy = import_module(
|
||||
"govoplan_mail.backend.migrations.versions.7192a3bcdef0_mail_server_hierarchy"
|
||||
)
|
||||
|
||||
|
||||
revision = hierarchy.revision
|
||||
down_revision = hierarchy.down_revision
|
||||
branch_labels = hierarchy.branch_labels
|
||||
depends_on = hierarchy.depends_on
|
||||
upgrade = hierarchy.upgrade
|
||||
downgrade = hierarchy.downgrade
|
||||
@@ -0,0 +1,265 @@
|
||||
"""split mail envelopes into servers and reusable credential bindings
|
||||
|
||||
Revision ID: 7192a3bcdef0
|
||||
Revises: 608192abcdef
|
||||
Create Date: 2026-07-23 00:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "7192a3bcdef0"
|
||||
down_revision = "608192abcdef"
|
||||
branch_labels = None
|
||||
depends_on = "c91f0a72be34"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
tables = set(inspector.get_table_names())
|
||||
if "mail_server_profiles" not in tables:
|
||||
return
|
||||
|
||||
profile_columns = {column["name"] for column in inspector.get_columns("mail_server_profiles")}
|
||||
if "inherit_to_lower_scopes" not in profile_columns:
|
||||
with op.batch_alter_table("mail_server_profiles") as batch:
|
||||
batch.add_column(
|
||||
sa.Column(
|
||||
"inherit_to_lower_scopes",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.true(),
|
||||
)
|
||||
)
|
||||
|
||||
if "mail_server_endpoints" not in tables:
|
||||
op.create_table(
|
||||
"mail_server_endpoints",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("profile_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("protocol", sa.String(length=20), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column("config", sa.JSON(), nullable=False),
|
||||
sa.Column("scope_type", sa.String(length=20), nullable=False),
|
||||
sa.Column("scope_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("inherit_to_lower_scopes", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_default", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("transport_revision", sa.String(length=36), nullable=False),
|
||||
sa.Column("created_by_user_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("updated_by_user_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["profile_id"],
|
||||
["mail_server_profiles.id"],
|
||||
name=op.f("fk_mail_server_endpoints_profile_id_mail_server_profiles"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["tenant_id"],
|
||||
["core_scopes.id"],
|
||||
name=op.f("fk_mail_server_endpoints_tenant_id_core_scopes"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by_user_id"],
|
||||
["access_users.id"],
|
||||
name=op.f("fk_mail_server_endpoints_created_by_user_id_access_users"),
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["updated_by_user_id"],
|
||||
["access_users.id"],
|
||||
name=op.f("fk_mail_server_endpoints_updated_by_user_id_access_users"),
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_mail_server_endpoints")),
|
||||
sa.UniqueConstraint(
|
||||
"profile_id",
|
||||
"protocol",
|
||||
"name",
|
||||
name="uq_mail_server_endpoints_profile_protocol_name",
|
||||
),
|
||||
)
|
||||
_create_endpoint_indexes()
|
||||
|
||||
inspector = sa.inspect(bind)
|
||||
if "mail_server_credential_bindings" not in inspector.get_table_names():
|
||||
op.create_table(
|
||||
"mail_server_credential_bindings",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("server_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("credential_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("is_default", sa.Boolean(), nullable=False),
|
||||
sa.Column("created_by_user_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["server_id"],
|
||||
["mail_server_endpoints.id"],
|
||||
name=op.f("fk_mail_server_credential_bindings_server_id_mail_server_endpoints"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["credential_id"],
|
||||
["core_credential_envelopes.id"],
|
||||
name=op.f("fk_mail_server_credential_bindings_credential_id_core_credential_envelopes"),
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by_user_id"],
|
||||
["access_users.id"],
|
||||
name=op.f("fk_mail_server_credential_bindings_created_by_user_id_access_users"),
|
||||
ondelete="SET NULL",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_mail_server_credential_bindings")),
|
||||
sa.UniqueConstraint(
|
||||
"server_id",
|
||||
"credential_id",
|
||||
name="uq_mail_server_credential_bindings_server_credential",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_mail_server_credential_bindings_default",
|
||||
"mail_server_credential_bindings",
|
||||
["server_id", "is_default"],
|
||||
unique=False,
|
||||
)
|
||||
for column in ("server_id", "credential_id", "is_default", "created_by_user_id"):
|
||||
op.create_index(
|
||||
op.f(f"ix_mail_server_credential_bindings_{column}"),
|
||||
"mail_server_credential_bindings",
|
||||
[column],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
_seed_legacy_endpoints(bind)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
tables = set(inspector.get_table_names())
|
||||
if "mail_server_credential_bindings" in tables:
|
||||
op.drop_table("mail_server_credential_bindings")
|
||||
if "mail_server_endpoints" in tables:
|
||||
op.drop_table("mail_server_endpoints")
|
||||
if "mail_server_profiles" in tables:
|
||||
columns = {column["name"] for column in inspector.get_columns("mail_server_profiles")}
|
||||
if "inherit_to_lower_scopes" in columns:
|
||||
with op.batch_alter_table("mail_server_profiles") as batch:
|
||||
batch.drop_column("inherit_to_lower_scopes")
|
||||
|
||||
|
||||
def _create_endpoint_indexes() -> None:
|
||||
op.create_index(
|
||||
"ix_mail_server_endpoints_profile_protocol",
|
||||
"mail_server_endpoints",
|
||||
["profile_id", "protocol", "is_active"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
"ix_mail_server_endpoints_scope",
|
||||
"mail_server_endpoints",
|
||||
["tenant_id", "scope_type", "scope_id"],
|
||||
unique=False,
|
||||
)
|
||||
for column in (
|
||||
"profile_id",
|
||||
"tenant_id",
|
||||
"protocol",
|
||||
"scope_type",
|
||||
"scope_id",
|
||||
"is_default",
|
||||
"is_active",
|
||||
"created_by_user_id",
|
||||
"updated_by_user_id",
|
||||
):
|
||||
op.create_index(
|
||||
op.f(f"ix_mail_server_endpoints_{column}"),
|
||||
"mail_server_endpoints",
|
||||
[column],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def _seed_legacy_endpoints(bind) -> None:
|
||||
existing = {
|
||||
(row.profile_id, row.protocol)
|
||||
for row in bind.execute(
|
||||
sa.text("SELECT profile_id, protocol FROM mail_server_endpoints WHERE is_default = :is_default"),
|
||||
{"is_default": True},
|
||||
)
|
||||
}
|
||||
rows = bind.execute(
|
||||
sa.text(
|
||||
"SELECT id, tenant_id, scope_type, scope_id, smtp_config, imap_config, "
|
||||
"smtp_transport_revision, imap_transport_revision, created_by_user_id, updated_by_user_id "
|
||||
"FROM mail_server_profiles"
|
||||
)
|
||||
).mappings()
|
||||
now = datetime.now(timezone.utc)
|
||||
table = sa.table(
|
||||
"mail_server_endpoints",
|
||||
sa.column("id", sa.String),
|
||||
sa.column("profile_id", sa.String),
|
||||
sa.column("tenant_id", sa.String),
|
||||
sa.column("protocol", sa.String),
|
||||
sa.column("name", sa.String),
|
||||
sa.column("config", sa.JSON),
|
||||
sa.column("scope_type", sa.String),
|
||||
sa.column("scope_id", sa.String),
|
||||
sa.column("inherit_to_lower_scopes", sa.Boolean),
|
||||
sa.column("is_default", sa.Boolean),
|
||||
sa.column("is_active", sa.Boolean),
|
||||
sa.column("transport_revision", sa.String),
|
||||
sa.column("created_by_user_id", sa.String),
|
||||
sa.column("updated_by_user_id", sa.String),
|
||||
sa.column("created_at", sa.DateTime(timezone=True)),
|
||||
sa.column("updated_at", sa.DateTime(timezone=True)),
|
||||
)
|
||||
for row in rows:
|
||||
for protocol, config_key, revision_key in (
|
||||
("smtp", "smtp_config", "smtp_transport_revision"),
|
||||
("imap", "imap_config", "imap_transport_revision"),
|
||||
):
|
||||
config = _json_object(row[config_key])
|
||||
if not config or (row["id"], protocol) in existing:
|
||||
continue
|
||||
bind.execute(
|
||||
table.insert().values(
|
||||
id=str(uuid.uuid4()),
|
||||
profile_id=row["id"],
|
||||
tenant_id=row["tenant_id"],
|
||||
protocol=protocol,
|
||||
name=protocol.upper(),
|
||||
config=config,
|
||||
scope_type=row["scope_type"] or "tenant",
|
||||
scope_id=row["scope_id"],
|
||||
inherit_to_lower_scopes=True,
|
||||
is_default=True,
|
||||
is_active=True,
|
||||
transport_revision=row[revision_key] or str(uuid.uuid4()),
|
||||
created_by_user_id=row["created_by_user_id"],
|
||||
updated_by_user_id=row["updated_by_user_id"],
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _json_object(value):
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str) and value.strip():
|
||||
parsed = json.loads(value)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
return {}
|
||||
+1078
-30
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
||||
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
from govoplan_mail.backend.config import (
|
||||
@@ -103,6 +103,7 @@ class MailServerProfileCreateRequest(BaseModel):
|
||||
slug: str | None = Field(default=None, max_length=100)
|
||||
description: str | None = None
|
||||
is_active: bool = True
|
||||
inherit_to_lower_scopes: bool = True
|
||||
scope_type: MailProfileScope = "tenant"
|
||||
scope_id: str | None = None
|
||||
smtp: SmtpServerConfig
|
||||
@@ -137,6 +138,7 @@ class MailServerProfileUpdateRequest(BaseModel):
|
||||
slug: str | None = Field(default=None, max_length=100)
|
||||
description: str | None = None
|
||||
is_active: bool | None = None
|
||||
inherit_to_lower_scopes: bool | None = None
|
||||
smtp: SmtpServerConfig | None = None
|
||||
imap: ImapServerConfig | None = None
|
||||
credentials: MailServerProfileCredentialsPayload = Field(default_factory=MailServerProfileCredentialsPayload)
|
||||
@@ -167,6 +169,127 @@ class MailServerProfileUpdateRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class MailCredentialEnvelopeResponse(BaseModel):
|
||||
id: str
|
||||
binding_id: str | None = None
|
||||
server_id: str | None = None
|
||||
tenant_id: str | None = None
|
||||
scope_type: MailProfileScope
|
||||
scope_id: str | None = None
|
||||
name: str
|
||||
description: str | None = None
|
||||
credential_kind: str
|
||||
public_data: dict[str, Any] = Field(default_factory=dict)
|
||||
secret_keys: list[str] = Field(default_factory=list)
|
||||
secret_configured: bool = False
|
||||
allowed_modules: list[str] = Field(default_factory=list)
|
||||
allowed_server_refs: list[str] = Field(default_factory=list)
|
||||
inherit_to_lower_scopes: bool = False
|
||||
is_default: bool = False
|
||||
is_active: bool = True
|
||||
revision: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
deleted_at: datetime | None = None
|
||||
|
||||
|
||||
class MailServerEndpointResponse(BaseModel):
|
||||
id: str
|
||||
profile_id: str
|
||||
tenant_id: str | None = None
|
||||
protocol: Literal["smtp", "imap"]
|
||||
name: str
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
scope_type: MailProfileScope
|
||||
scope_id: str | None = None
|
||||
inherit_to_lower_scopes: bool = True
|
||||
is_default: bool = False
|
||||
is_active: bool = True
|
||||
transport_revision: str
|
||||
credentials: list[MailCredentialEnvelopeResponse] = Field(default_factory=list)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class MailServerEndpointCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
protocol: Literal["smtp", "imap"]
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
config: dict[str, Any] = Field(default_factory=dict)
|
||||
inherit_to_lower_scopes: bool | None = None
|
||||
is_default: bool = False
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class MailServerEndpointUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str | None = Field(default=None, max_length=255)
|
||||
config: dict[str, Any] | None = None
|
||||
inherit_to_lower_scopes: bool | None = None
|
||||
is_default: bool | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class MailCredentialCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
description: str | None = None
|
||||
credential_kind: str = "username_password"
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
public_data: dict[str, Any] = Field(default_factory=dict)
|
||||
secret_data: dict[str, Any] = Field(default_factory=dict)
|
||||
inherit_to_lower_scopes: bool | None = None
|
||||
allowed_modules: list[str] = Field(default_factory=lambda: ["mail"])
|
||||
allowed_server_refs: list[str] = Field(default_factory=list)
|
||||
is_default: bool = False
|
||||
|
||||
|
||||
class MailCampaignCredentialCreateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
username: str = Field(min_length=1, max_length=320)
|
||||
password: SecretStr
|
||||
server_ids: list[str] = Field(min_length=1)
|
||||
|
||||
|
||||
class MailCredentialBindRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
credential_id: str = Field(min_length=1)
|
||||
is_default: bool = False
|
||||
|
||||
|
||||
class MailCredentialUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: str | None = Field(default=None, max_length=255)
|
||||
description: str | None = None
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
public_data: dict[str, Any] | None = None
|
||||
secret_data: dict[str, Any] | None = None
|
||||
inherit_to_lower_scopes: bool | None = None
|
||||
allowed_modules: list[str] | None = None
|
||||
allowed_server_refs: list[str] | None = None
|
||||
is_default: bool | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class MailCredentialUnlinkRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
retire_if_unused: bool = False
|
||||
|
||||
|
||||
class MailCredentialListResponse(BaseModel):
|
||||
credentials: list[MailCredentialEnvelopeResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MailServerProfileResponse(BaseModel):
|
||||
id: str
|
||||
tenant_id: str | None = None
|
||||
@@ -176,11 +299,13 @@ class MailServerProfileResponse(BaseModel):
|
||||
slug: str
|
||||
description: str | None = None
|
||||
is_active: bool
|
||||
inherit_to_lower_scopes: bool = True
|
||||
smtp: dict[str, Any]
|
||||
imap: dict[str, Any] | None = None
|
||||
credentials: dict[str, Any] = Field(default_factory=dict)
|
||||
smtp_password_configured: bool = False
|
||||
imap_password_configured: bool = False
|
||||
servers: list[MailServerEndpointResponse] = Field(default_factory=list)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user