Release v0.1.5
This commit is contained in:
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: "Bug"
|
||||||
|
about: "Report a reproducible defect, regression, or incorrect behavior"
|
||||||
|
title: "[Bug] "
|
||||||
|
labels:
|
||||||
|
- type/bug
|
||||||
|
- status/triage
|
||||||
|
- module/mail
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Repository:
|
||||||
|
- Area/module:
|
||||||
|
- Affected version or commit:
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
Actual:
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Logs, screenshots, traces, or failing test output:
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Command or workflow that should pass when fixed:
|
||||||
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal file
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "Docs / workflow"
|
||||||
|
about: "Request documentation, process, or developer workflow changes"
|
||||||
|
title: "[Docs] "
|
||||||
|
labels:
|
||||||
|
- type/docs
|
||||||
|
- status/triage
|
||||||
|
- module/mail
|
||||||
|
- area/docs
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Repository:
|
||||||
|
- Document or workflow:
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
What is missing, unclear, duplicated, or stale?
|
||||||
|
|
||||||
|
## Desired State
|
||||||
|
|
||||||
|
What should the docs or workflow make clear?
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
How should this be checked?
|
||||||
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: "Feature"
|
||||||
|
about: "Propose new user-visible behavior or platform capability"
|
||||||
|
title: "[Feature] "
|
||||||
|
labels:
|
||||||
|
- type/feature
|
||||||
|
- status/triage
|
||||||
|
- module/mail
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
What user, operator, or developer problem should this solve?
|
||||||
|
|
||||||
|
## Proposed Capability
|
||||||
|
|
||||||
|
What should exist when this is done?
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Owning repository:
|
||||||
|
- Related module repositories:
|
||||||
|
- Extension point or integration boundary:
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Command, scenario, or UI flow that should prove completion:
|
||||||
28
.gitea/ISSUE_TEMPLATE/task.md
Normal file
28
.gitea/ISSUE_TEMPLATE/task.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: "Task"
|
||||||
|
about: "Track implementation, maintenance, or migration work"
|
||||||
|
title: "[Task] "
|
||||||
|
labels:
|
||||||
|
- type/task
|
||||||
|
- status/triage
|
||||||
|
- module/mail
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
What needs to be completed?
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Owning repository:
|
||||||
|
- In-scope:
|
||||||
|
- Out-of-scope:
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Command or manual check:
|
||||||
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal file
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: "Tech debt"
|
||||||
|
about: "Track cleanup, refactoring, risk reduction, or deferred engineering work"
|
||||||
|
title: "[Debt] "
|
||||||
|
labels:
|
||||||
|
- type/debt
|
||||||
|
- status/triage
|
||||||
|
- module/mail
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Cost
|
||||||
|
|
||||||
|
What does this make harder, riskier, slower, or more fragile?
|
||||||
|
|
||||||
|
## Desired Shape
|
||||||
|
|
||||||
|
What should the code, tests, or architecture look like afterwards?
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
What behavior, compatibility, or module boundary must be preserved?
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
Focused checks that should pass:
|
||||||
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## Issue
|
||||||
|
|
||||||
|
Closes #
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Follow-up issues:
|
||||||
10
README.md
10
README.md
@@ -50,7 +50,15 @@ Frontend package:
|
|||||||
@govoplan/mail-webui
|
@govoplan/mail-webui
|
||||||
```
|
```
|
||||||
|
|
||||||
The campaign module depends on this module for sending, append-to-Sent behavior, profile selection, and read-only mailbox inspection.
|
The campaign module consumes `mail.campaign_delivery` for sending,
|
||||||
|
append-to-Sent behavior, profile selection, and policy checks. Mail does not
|
||||||
|
import campaign internals; campaign-scoped policy and owner context are resolved
|
||||||
|
through the core `campaigns.mailPolicyContext` capability when the campaign
|
||||||
|
module is installed.
|
||||||
|
|
||||||
|
Development mailbox routes are registered by the mail module only when the
|
||||||
|
core runtime is in `dev` mode and `dev_mailbox_api_enabled` is enabled. Core
|
||||||
|
does not contribute these routes directly.
|
||||||
|
|
||||||
Platform RBAC and governance rules are documented in `govoplan-core/docs/`.
|
Platform RBAC and governance rules are documented in `govoplan-core/docs/`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/mail-webui",
|
"name": "@govoplan/mail-webui",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.4",
|
"@govoplan/core-webui": "^0.1.5",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^0.555.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-mail"
|
name = "govoplan-mail"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
description = "GovOPlaN mail module with backend and WebUI integration."
|
description = "GovOPlaN mail module with backend and WebUI integration."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.4",
|
"govoplan-core>=0.1.5",
|
||||||
"pydantic>=2,<3",
|
"pydantic>=2,<3",
|
||||||
"redis>=5,<6",
|
"redis>=5,<6",
|
||||||
"SQLAlchemy>=2,<3",
|
"SQLAlchemy>=2,<3",
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ class MailCampaignCapability:
|
|||||||
append_message_to_sent = staticmethod(append_message_to_sent)
|
append_message_to_sent = staticmethod(append_message_to_sent)
|
||||||
send_email_message = staticmethod(send_email_message)
|
send_email_message = staticmethod(send_email_message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mock_mailbox():
|
||||||
|
from govoplan_mail.backend.dev import mock_mailbox
|
||||||
|
|
||||||
|
return mock_mailbox
|
||||||
|
|
||||||
|
|
||||||
def campaign_capability(context: ModuleContext) -> MailCampaignCapability:
|
def campaign_capability(context: ModuleContext) -> MailCampaignCapability:
|
||||||
configure_runtime(settings=context.settings)
|
configure_runtime(settings=context.settings)
|
||||||
|
|||||||
@@ -40,3 +40,16 @@ class MailServerProfile(Base, TimestampMixin):
|
|||||||
tenant: Mapped["Tenant"] = relationship()
|
tenant: Mapped["Tenant"] = relationship()
|
||||||
|
|
||||||
|
|
||||||
|
class MailProfilePolicy(Base, TimestampMixin):
|
||||||
|
__tablename__ = "mail_profile_policies"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("tenant_id", "scope_type", "scope_id", name="uq_mail_profile_policies_scope"),
|
||||||
|
Index("ix_mail_profile_policies_scope", "scope_type", "scope_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||||
|
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True)
|
||||||
|
scope_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||||
|
scope_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
||||||
|
policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
from govoplan_core.auth.dependencies import ApiPrincipal, require_scope
|
from govoplan_access.backend.auth.dependencies import ApiPrincipal, require_scope
|
||||||
from govoplan_mail.backend.dev.mock_mailbox import (
|
from govoplan_mail.backend.dev.mock_mailbox import (
|
||||||
clear_records,
|
clear_records,
|
||||||
get_failures,
|
get_failures,
|
||||||
|
|||||||
@@ -2,17 +2,23 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
from sqlalchemy import and_, or_
|
from sqlalchemy import and_, or_, text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from govoplan_core.admin.governance import get_system_settings
|
from govoplan_core.admin.settings import get_system_settings
|
||||||
from govoplan_core.core.optional import reraise_unless_missing_package
|
from govoplan_core.core.access import CAPABILITY_ACCESS_DIRECTORY, AccessDirectory
|
||||||
from govoplan_core.db.models import Group, Tenant, User, UserGroupMembership
|
from govoplan_core.core.campaigns import (
|
||||||
from govoplan_mail.backend.db.models import MailServerProfile
|
CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT,
|
||||||
|
CampaignMailPolicyContext,
|
||||||
|
CampaignMailPolicyContextProvider,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.runtime import get_registry
|
||||||
|
from govoplan_mail.backend.db.models import MailProfilePolicy, MailServerProfile
|
||||||
from govoplan_mail.backend.config import ImapConfig, SmtpConfig
|
from govoplan_mail.backend.config import ImapConfig, SmtpConfig
|
||||||
from govoplan_core.security.secrets import decrypt_secret, encrypt_secret
|
from govoplan_core.security.secrets import decrypt_secret, encrypt_secret
|
||||||
|
|
||||||
@@ -49,15 +55,6 @@ def default_mail_policy_lower_level_limits() -> dict[str, bool]:
|
|||||||
return {key: True for key in MAIL_POLICY_LIMIT_KEYS}
|
return {key: True for key in MAIL_POLICY_LIMIT_KEYS}
|
||||||
|
|
||||||
|
|
||||||
def _campaign_model():
|
|
||||||
try:
|
|
||||||
from govoplan_campaign.backend.db.models import Campaign
|
|
||||||
except ModuleNotFoundError as exc:
|
|
||||||
reraise_unless_missing_package(exc, "govoplan_campaign")
|
|
||||||
raise MailProfileError("Campaign module is not installed") from exc
|
|
||||||
return Campaign
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class EffectiveCredentialPolicy:
|
class EffectiveCredentialPolicy:
|
||||||
inherit: bool = True
|
inherit: bool = True
|
||||||
@@ -240,6 +237,174 @@ def _policy_from_attr(value: Any) -> dict[str, Any]:
|
|||||||
return normalize_mail_profile_policy(value if isinstance(value, dict) else {})
|
return normalize_mail_profile_policy(value if isinstance(value, dict) else {})
|
||||||
|
|
||||||
|
|
||||||
|
def _json_object(value: Any) -> dict[str, Any]:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
try:
|
||||||
|
parsed = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
return parsed if isinstance(parsed, dict) else {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _access_directory() -> AccessDirectory | None:
|
||||||
|
registry = get_registry()
|
||||||
|
if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_ACCESS_DIRECTORY):
|
||||||
|
return None
|
||||||
|
capability = registry.require_capability(CAPABILITY_ACCESS_DIRECTORY)
|
||||||
|
if not isinstance(capability, AccessDirectory):
|
||||||
|
raise MailProfileError("Access directory capability is invalid")
|
||||||
|
return capability
|
||||||
|
|
||||||
|
|
||||||
|
def _campaign_policy_provider() -> CampaignMailPolicyContextProvider | None:
|
||||||
|
registry = get_registry()
|
||||||
|
if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT):
|
||||||
|
return None
|
||||||
|
capability = registry.require_capability(CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT)
|
||||||
|
if not isinstance(capability, CampaignMailPolicyContextProvider):
|
||||||
|
raise MailProfileError("Campaign mail policy context capability is invalid")
|
||||||
|
return capability
|
||||||
|
|
||||||
|
|
||||||
|
def _campaign_policy_context(session: Session, *, tenant_id: str, campaign_id: str) -> CampaignMailPolicyContext:
|
||||||
|
provider = _campaign_policy_provider()
|
||||||
|
if provider is None:
|
||||||
|
raise MailProfileError("Campaign module is not available")
|
||||||
|
context = provider.get_campaign_mail_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
||||||
|
if context is None:
|
||||||
|
raise MailProfileError("Campaign not found for mail-server profile policy")
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_tenant_settings(session: Session, tenant_id: str) -> dict[str, Any] | None:
|
||||||
|
row = session.execute(text("select settings from tenants where id = :tenant_id"), {"tenant_id": tenant_id}).first()
|
||||||
|
return _json_object(row[0]) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_subject_policy(session: Session, *, table_name: str, tenant_id: str, scope_id: str) -> dict[str, Any] | None:
|
||||||
|
if table_name not in {"users", "groups"}:
|
||||||
|
raise MailProfileError("Unsupported mail policy subject")
|
||||||
|
row = session.execute(
|
||||||
|
text(f"select mail_profile_policy from {table_name} where id = :scope_id and tenant_id = :tenant_id"),
|
||||||
|
{"scope_id": scope_id, "tenant_id": tenant_id},
|
||||||
|
).first()
|
||||||
|
return _json_object(row[0]) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def _tenant_exists(session: Session, tenant_id: str) -> bool:
|
||||||
|
return _legacy_tenant_settings(session, tenant_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _user_exists(session: Session, *, tenant_id: str, user_id: str) -> bool:
|
||||||
|
directory = _access_directory()
|
||||||
|
if directory is not None:
|
||||||
|
user = directory.get_user(user_id)
|
||||||
|
return bool(user and user.tenant_id == tenant_id and user.status == "active")
|
||||||
|
return _legacy_subject_policy(session, table_name="users", tenant_id=tenant_id, scope_id=user_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _group_exists(session: Session, *, tenant_id: str, group_id: str) -> bool:
|
||||||
|
directory = _access_directory()
|
||||||
|
if directory is not None:
|
||||||
|
group = directory.get_group(group_id)
|
||||||
|
return bool(group and group.tenant_id == tenant_id and group.status == "active")
|
||||||
|
return _legacy_subject_policy(session, table_name="groups", tenant_id=tenant_id, scope_id=group_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _policy_scope_key(*, tenant_id: str, scope_type: str, scope_id: str | None) -> tuple[str | None, str, str | None]:
|
||||||
|
clean_scope = _normalize_scope_type(scope_type)
|
||||||
|
if clean_scope == "system":
|
||||||
|
return None, clean_scope, None
|
||||||
|
if clean_scope == "tenant":
|
||||||
|
return tenant_id, clean_scope, scope_id or tenant_id
|
||||||
|
return tenant_id, clean_scope, scope_id
|
||||||
|
|
||||||
|
|
||||||
|
def _mail_policy_row(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
scope_type: str,
|
||||||
|
scope_id: str | None,
|
||||||
|
) -> MailProfilePolicy | None:
|
||||||
|
row_tenant_id, row_scope_type, row_scope_id = _policy_scope_key(tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id)
|
||||||
|
query = session.query(MailProfilePolicy).filter(MailProfilePolicy.scope_type == row_scope_type)
|
||||||
|
query = query.filter(MailProfilePolicy.tenant_id.is_(None) if row_tenant_id is None else MailProfilePolicy.tenant_id == row_tenant_id)
|
||||||
|
query = query.filter(MailProfilePolicy.scope_id.is_(None) if row_scope_id is None else MailProfilePolicy.scope_id == row_scope_id)
|
||||||
|
return query.one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_mail_profile_policy(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
scope_type: str,
|
||||||
|
scope_id: str | None,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
clean_scope = _normalize_scope_type(scope_type)
|
||||||
|
if clean_scope == "system":
|
||||||
|
return _policy_from_settings(get_system_settings(session).settings or {})
|
||||||
|
if clean_scope == "tenant":
|
||||||
|
if scope_id and scope_id != tenant_id:
|
||||||
|
return None
|
||||||
|
settings = _legacy_tenant_settings(session, tenant_id)
|
||||||
|
return _policy_from_settings(settings or {}) if settings is not None else None
|
||||||
|
if not scope_id:
|
||||||
|
return None
|
||||||
|
if clean_scope == "user":
|
||||||
|
legacy = _legacy_subject_policy(session, table_name="users", tenant_id=tenant_id, scope_id=scope_id)
|
||||||
|
return _policy_from_attr(legacy) if legacy is not None else None
|
||||||
|
if clean_scope == "group":
|
||||||
|
legacy = _legacy_subject_policy(session, table_name="groups", tenant_id=tenant_id, scope_id=scope_id)
|
||||||
|
return _policy_from_attr(legacy) if legacy is not None else None
|
||||||
|
try:
|
||||||
|
context = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=scope_id)
|
||||||
|
except MailProfileError:
|
||||||
|
return None
|
||||||
|
return _policy_from_attr(context.mail_profile_policy)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_mail_profile_policy(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
scope_type: str,
|
||||||
|
scope_id: str | None = None,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
clean_scope = _normalize_scope_type(scope_type)
|
||||||
|
if clean_scope != "campaign":
|
||||||
|
row = _mail_policy_row(session, tenant_id=tenant_id, scope_type=clean_scope, scope_id=scope_id)
|
||||||
|
if row is not None:
|
||||||
|
return _policy_from_attr(row.policy)
|
||||||
|
return _legacy_mail_profile_policy(session, tenant_id=tenant_id, scope_type=clean_scope, scope_id=scope_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_mail_profile_policy(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
scope_type: str,
|
||||||
|
scope_id: str | None,
|
||||||
|
policy: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
row_tenant_id, row_scope_type, row_scope_id = _policy_scope_key(tenant_id=tenant_id, scope_type=scope_type, scope_id=scope_id)
|
||||||
|
row = _mail_policy_row(session, tenant_id=tenant_id, scope_type=row_scope_type, scope_id=row_scope_id)
|
||||||
|
if row is None:
|
||||||
|
row = MailProfilePolicy(
|
||||||
|
tenant_id=row_tenant_id,
|
||||||
|
scope_type=row_scope_type,
|
||||||
|
scope_id=row_scope_id,
|
||||||
|
policy=policy,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
row.policy = policy
|
||||||
|
session.add(row)
|
||||||
|
return policy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _mail_policy_source_fields(policy: dict[str, Any], *, baseline: bool = False) -> list[str]:
|
def _mail_policy_source_fields(policy: dict[str, Any], *, baseline: bool = False) -> list[str]:
|
||||||
fields: list[str] = []
|
fields: list[str] = []
|
||||||
@@ -373,15 +538,13 @@ def _owner_context(
|
|||||||
campaign_id: str | None = None,
|
campaign_id: str | None = None,
|
||||||
owner_user_id: str | None = None,
|
owner_user_id: str | None = None,
|
||||||
owner_group_id: str | None = None,
|
owner_group_id: str | None = None,
|
||||||
) -> tuple[Any | None, str | None, str | None]:
|
) -> tuple[CampaignMailPolicyContext | None, str | None, str | None]:
|
||||||
campaign = None
|
campaign_context = None
|
||||||
if campaign_id:
|
if campaign_id:
|
||||||
campaign = session.get(_campaign_model(), campaign_id)
|
campaign_context = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
||||||
if campaign is None or campaign.tenant_id != tenant_id:
|
owner_user_id = campaign_context.owner_user_id
|
||||||
raise MailProfileError("Campaign not found for mail-server profile policy")
|
owner_group_id = campaign_context.owner_group_id
|
||||||
owner_user_id = campaign.owner_user_id
|
return campaign_context, owner_user_id, owner_group_id
|
||||||
owner_group_id = campaign.owner_group_id
|
|
||||||
return campaign, owner_user_id, owner_group_id
|
|
||||||
|
|
||||||
|
|
||||||
def effective_mail_profile_policy(
|
def effective_mail_profile_policy(
|
||||||
@@ -399,22 +562,22 @@ def effective_mail_profile_policy(
|
|||||||
owner_user_id=owner_user_id,
|
owner_user_id=owner_user_id,
|
||||||
owner_group_id=owner_group_id,
|
owner_group_id=owner_group_id,
|
||||||
)
|
)
|
||||||
tenant = session.get(Tenant, tenant_id)
|
tenant_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=tenant_id)
|
||||||
if tenant is None:
|
if tenant_policy is None:
|
||||||
raise MailProfileError("Tenant not found for mail-server profile policy")
|
raise MailProfileError("Tenant not found for mail-server profile policy")
|
||||||
|
|
||||||
policy = EffectiveMailProfilePolicy()
|
policy = EffectiveMailProfilePolicy()
|
||||||
system_settings = get_system_settings(session)
|
system_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {}
|
||||||
_merge_policy(policy, _policy_from_settings(system_settings.settings or {}), source="system", label="System", baseline=True)
|
_merge_policy(policy, system_policy, source="system", label="System", baseline=True)
|
||||||
_merge_policy(policy, _policy_from_settings(tenant.settings or {}), source="tenant", source_id=tenant.id, label="Tenant")
|
_merge_policy(policy, tenant_policy, source="tenant", source_id=tenant_id, label="Tenant")
|
||||||
if owner_user_id:
|
if owner_user_id:
|
||||||
user = session.get(User, owner_user_id)
|
user_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=owner_user_id)
|
||||||
if user and user.tenant_id == tenant_id:
|
if user_policy is not None:
|
||||||
_merge_policy(policy, _policy_from_attr(user.mail_profile_policy), source="user", source_id=user.id, label="Owner user")
|
_merge_policy(policy, user_policy, source="user", source_id=owner_user_id, label="Owner user")
|
||||||
if owner_group_id:
|
if owner_group_id:
|
||||||
group = session.get(Group, owner_group_id)
|
group_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=owner_group_id)
|
||||||
if group and group.tenant_id == tenant_id:
|
if group_policy is not None:
|
||||||
_merge_policy(policy, _policy_from_attr(group.mail_profile_policy), source="group", source_id=group.id, label="Owner group")
|
_merge_policy(policy, group_policy, source="group", source_id=owner_group_id, label="Owner group")
|
||||||
if campaign is not None:
|
if campaign is not None:
|
||||||
_merge_policy(policy, _policy_from_attr(campaign.mail_profile_policy), source="campaign", source_id=campaign.id, label="Campaign")
|
_merge_policy(policy, _policy_from_attr(campaign.mail_profile_policy), source="campaign", source_id=campaign.id, label="Campaign")
|
||||||
return policy
|
return policy
|
||||||
@@ -434,8 +597,8 @@ def effective_mail_profile_policy_for_scope(
|
|||||||
return effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
return effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
||||||
if clean_scope == "system":
|
if clean_scope == "system":
|
||||||
policy = EffectiveMailProfilePolicy()
|
policy = EffectiveMailProfilePolicy()
|
||||||
system_settings = get_system_settings(session)
|
system_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {}
|
||||||
_merge_policy(policy, _policy_from_settings(system_settings.settings or {}), source="system", label="System", baseline=True)
|
_merge_policy(policy, system_policy, source="system", label="System", baseline=True)
|
||||||
return policy
|
return policy
|
||||||
if clean_scope == "tenant":
|
if clean_scope == "tenant":
|
||||||
return effective_mail_profile_policy(session, tenant_id=tenant_id)
|
return effective_mail_profile_policy(session, tenant_id=tenant_id)
|
||||||
@@ -460,33 +623,31 @@ def parent_mail_profile_policy(
|
|||||||
scope_id: str | None = None,
|
scope_id: str | None = None,
|
||||||
) -> EffectiveMailProfilePolicy:
|
) -> EffectiveMailProfilePolicy:
|
||||||
clean_scope = _normalize_scope_type(scope_type)
|
clean_scope = _normalize_scope_type(scope_type)
|
||||||
tenant = session.get(Tenant, tenant_id)
|
tenant_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=tenant_id)
|
||||||
if tenant is None:
|
if tenant_policy is None:
|
||||||
raise MailProfileError("Tenant not found for mail-server profile policy")
|
raise MailProfileError("Tenant not found for mail-server profile policy")
|
||||||
|
|
||||||
policy = EffectiveMailProfilePolicy()
|
policy = EffectiveMailProfilePolicy()
|
||||||
system_settings = get_system_settings(session)
|
system_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {}
|
||||||
_merge_policy(policy, _policy_from_settings(system_settings.settings or {}), source="system", label="System", baseline=True)
|
_merge_policy(policy, system_policy, source="system", label="System", baseline=True)
|
||||||
if clean_scope == "tenant":
|
if clean_scope == "tenant":
|
||||||
return policy
|
return policy
|
||||||
|
|
||||||
_merge_policy(policy, _policy_from_settings(tenant.settings or {}), source="tenant", source_id=tenant.id, label="Tenant")
|
_merge_policy(policy, tenant_policy, source="tenant", source_id=tenant_id, label="Tenant")
|
||||||
if clean_scope in {"user", "group"}:
|
if clean_scope in {"user", "group"}:
|
||||||
return policy
|
return policy
|
||||||
|
|
||||||
if clean_scope != "campaign" or not scope_id:
|
if clean_scope != "campaign" or not scope_id:
|
||||||
return policy
|
return policy
|
||||||
campaign = session.get(_campaign_model(), scope_id)
|
campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=scope_id)
|
||||||
if campaign is None or campaign.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("Campaign policy not found")
|
|
||||||
if campaign.owner_user_id:
|
if campaign.owner_user_id:
|
||||||
user = session.get(User, campaign.owner_user_id)
|
user_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=campaign.owner_user_id)
|
||||||
if user and user.tenant_id == tenant_id:
|
if user_policy is not None:
|
||||||
_merge_policy(policy, _policy_from_attr(user.mail_profile_policy), source="user", source_id=user.id, label="Owner user")
|
_merge_policy(policy, user_policy, source="user", source_id=campaign.owner_user_id, label="Owner user")
|
||||||
if campaign.owner_group_id:
|
if campaign.owner_group_id:
|
||||||
group = session.get(Group, campaign.owner_group_id)
|
group_policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=campaign.owner_group_id)
|
||||||
if group and group.tenant_id == tenant_id:
|
if group_policy is not None:
|
||||||
_merge_policy(policy, _policy_from_attr(group.mail_profile_policy), source="group", source_id=group.id, label="Owner group")
|
_merge_policy(policy, group_policy, source="group", source_id=campaign.owner_group_id, label="Owner group")
|
||||||
return policy
|
return policy
|
||||||
|
|
||||||
|
|
||||||
@@ -580,22 +741,18 @@ def _scope_tuple_for_create(
|
|||||||
target_id = clean_scope_id or user_id
|
target_id = clean_scope_id or user_id
|
||||||
if not target_id:
|
if not target_id:
|
||||||
raise MailProfileError("User-scoped mail-server profiles require a user scope_id")
|
raise MailProfileError("User-scoped mail-server profiles require a user scope_id")
|
||||||
user = session.get(User, target_id)
|
if not _user_exists(session, tenant_id=tenant_id, user_id=target_id):
|
||||||
if user is None or user.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("User scope is not part of the active tenant")
|
raise MailProfileError("User scope is not part of the active tenant")
|
||||||
return tenant_id, clean_scope, target_id
|
return tenant_id, clean_scope, target_id
|
||||||
if clean_scope == "group":
|
if clean_scope == "group":
|
||||||
if not clean_scope_id:
|
if not clean_scope_id:
|
||||||
raise MailProfileError("Group-scoped mail-server profiles require a group scope_id")
|
raise MailProfileError("Group-scoped mail-server profiles require a group scope_id")
|
||||||
group = session.get(Group, clean_scope_id)
|
if not _group_exists(session, tenant_id=tenant_id, group_id=clean_scope_id):
|
||||||
if group is None or group.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("Group scope is not part of the active tenant")
|
raise MailProfileError("Group scope is not part of the active tenant")
|
||||||
return tenant_id, clean_scope, clean_scope_id
|
return tenant_id, clean_scope, clean_scope_id
|
||||||
if not clean_scope_id:
|
if not clean_scope_id:
|
||||||
raise MailProfileError("Campaign-scoped mail-server profiles require a campaign scope_id")
|
raise MailProfileError("Campaign-scoped mail-server profiles require a campaign scope_id")
|
||||||
campaign = session.get(_campaign_model(), clean_scope_id)
|
_campaign_policy_context(session, tenant_id=tenant_id, campaign_id=clean_scope_id)
|
||||||
if campaign is None or campaign.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("Campaign scope is not part of the active tenant")
|
|
||||||
return tenant_id, clean_scope, clean_scope_id
|
return tenant_id, clean_scope, clean_scope_id
|
||||||
|
|
||||||
|
|
||||||
@@ -647,7 +804,7 @@ def _profile_visible_filter(tenant_id: str):
|
|||||||
return or_(MailServerProfile.scope_type == "system", MailServerProfile.tenant_id == tenant_id)
|
return or_(MailServerProfile.scope_type == "system", MailServerProfile.tenant_id == tenant_id)
|
||||||
|
|
||||||
|
|
||||||
def _context_profile_clauses(campaign: Any, policy: EffectiveMailProfilePolicy) -> list[Any]:
|
def _context_profile_clauses(campaign: CampaignMailPolicyContext, policy: EffectiveMailProfilePolicy) -> list[Any]:
|
||||||
clauses: list[Any] = [
|
clauses: list[Any] = [
|
||||||
MailServerProfile.scope_type == "system",
|
MailServerProfile.scope_type == "system",
|
||||||
and_(MailServerProfile.tenant_id == campaign.tenant_id, MailServerProfile.scope_type == "tenant"),
|
and_(MailServerProfile.tenant_id == campaign.tenant_id, MailServerProfile.scope_type == "tenant"),
|
||||||
@@ -674,7 +831,7 @@ def _profile_allowed_for_context(
|
|||||||
*,
|
*,
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
policy: EffectiveMailProfilePolicy,
|
policy: EffectiveMailProfilePolicy,
|
||||||
campaign: Any | None = None,
|
campaign: CampaignMailPolicyContext | None = None,
|
||||||
owner_user_id: str | None = None,
|
owner_user_id: str | None = None,
|
||||||
owner_group_id: str | None = None,
|
owner_group_id: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -711,9 +868,7 @@ def list_mail_server_profiles(
|
|||||||
campaign_id: str | None = None,
|
campaign_id: str | None = None,
|
||||||
) -> list[MailServerProfile]:
|
) -> list[MailServerProfile]:
|
||||||
if campaign_id:
|
if campaign_id:
|
||||||
campaign = session.get(_campaign_model(), campaign_id)
|
campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
||||||
if campaign is None or campaign.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("Campaign not found for mail-server profiles")
|
|
||||||
policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign.id)
|
policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign.id)
|
||||||
query = session.query(MailServerProfile).filter(or_(*_context_profile_clauses(campaign, policy)))
|
query = session.query(MailServerProfile).filter(or_(*_context_profile_clauses(campaign, policy)))
|
||||||
if not include_inactive:
|
if not include_inactive:
|
||||||
@@ -762,9 +917,7 @@ def ensure_mail_profile_allowed_for_campaign(
|
|||||||
profile_id: str,
|
profile_id: str,
|
||||||
require_active: bool = True,
|
require_active: bool = True,
|
||||||
) -> MailServerProfile:
|
) -> MailServerProfile:
|
||||||
campaign = session.get(_campaign_model(), campaign_id)
|
campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=campaign_id)
|
||||||
if campaign is None or campaign.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("Campaign not found for mail-server profile policy")
|
|
||||||
profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=profile_id, require_active=require_active)
|
profile = get_mail_server_profile(session, tenant_id=tenant_id, profile_id=profile_id, require_active=require_active)
|
||||||
policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign.id)
|
policy = effective_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=campaign.id)
|
||||||
if not _profile_allowed_for_context(
|
if not _profile_allowed_for_context(
|
||||||
@@ -1165,27 +1318,25 @@ def get_mail_profile_policy(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
clean_scope = _normalize_scope_type(scope_type)
|
clean_scope = _normalize_scope_type(scope_type)
|
||||||
if clean_scope == "system":
|
if clean_scope == "system":
|
||||||
return _policy_from_settings(get_system_settings(session).settings or {})
|
return _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system") or {}
|
||||||
if clean_scope == "tenant":
|
if clean_scope == "tenant":
|
||||||
tenant = session.get(Tenant, scope_id or tenant_id)
|
policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=scope_id or tenant_id)
|
||||||
if tenant is None or tenant.id != tenant_id:
|
if policy is None:
|
||||||
raise MailProfileError("Tenant policy not found")
|
raise MailProfileError("Tenant policy not found")
|
||||||
return _policy_from_settings(tenant.settings or {})
|
return policy
|
||||||
if not scope_id:
|
if not scope_id:
|
||||||
raise MailProfileError(f"{clean_scope.capitalize()} mail profile policy requires scope_id")
|
raise MailProfileError(f"{clean_scope.capitalize()} mail profile policy requires scope_id")
|
||||||
if clean_scope == "user":
|
if clean_scope == "user":
|
||||||
user = session.get(User, scope_id)
|
policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=scope_id)
|
||||||
if user is None or user.tenant_id != tenant_id:
|
if policy is None:
|
||||||
raise MailProfileError("User policy not found")
|
raise MailProfileError("User policy not found")
|
||||||
return _policy_from_attr(user.mail_profile_policy)
|
return policy
|
||||||
if clean_scope == "group":
|
if clean_scope == "group":
|
||||||
group = session.get(Group, scope_id)
|
policy = _read_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=scope_id)
|
||||||
if group is None or group.tenant_id != tenant_id:
|
if policy is None:
|
||||||
raise MailProfileError("Group policy not found")
|
raise MailProfileError("Group policy not found")
|
||||||
return _policy_from_attr(group.mail_profile_policy)
|
return policy
|
||||||
campaign = session.get(_campaign_model(), scope_id)
|
campaign = _campaign_policy_context(session, tenant_id=tenant_id, campaign_id=scope_id)
|
||||||
if campaign is None or campaign.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("Campaign policy not found")
|
|
||||||
return _policy_from_attr(campaign.mail_profile_policy)
|
return _policy_from_attr(campaign.mail_profile_policy)
|
||||||
|
|
||||||
|
|
||||||
@@ -1231,42 +1382,28 @@ def set_mail_profile_policy(
|
|||||||
normalized,
|
normalized,
|
||||||
)
|
)
|
||||||
if clean_scope == "system":
|
if clean_scope == "system":
|
||||||
item = get_system_settings(session)
|
return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="system", scope_id=None, policy=normalized)
|
||||||
settings_payload = dict(item.settings or {})
|
|
||||||
settings_payload[MAIL_PROFILE_POLICY_SETTINGS_KEY] = normalized
|
|
||||||
item.settings = settings_payload
|
|
||||||
session.add(item)
|
|
||||||
return normalized
|
|
||||||
if clean_scope == "tenant":
|
if clean_scope == "tenant":
|
||||||
tenant = session.get(Tenant, scope_id or tenant_id)
|
if (scope_id and scope_id != tenant_id) or not _tenant_exists(session, tenant_id):
|
||||||
if tenant is None or tenant.id != tenant_id:
|
|
||||||
raise MailProfileError("Tenant policy not found")
|
raise MailProfileError("Tenant policy not found")
|
||||||
settings_payload = dict(tenant.settings or {})
|
return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="tenant", scope_id=tenant_id, policy=normalized)
|
||||||
settings_payload[MAIL_PROFILE_POLICY_SETTINGS_KEY] = normalized
|
|
||||||
tenant.settings = settings_payload
|
|
||||||
session.add(tenant)
|
|
||||||
return normalized
|
|
||||||
if not scope_id:
|
if not scope_id:
|
||||||
raise MailProfileError(f"{clean_scope.capitalize()} mail profile policy requires scope_id")
|
raise MailProfileError(f"{clean_scope.capitalize()} mail profile policy requires scope_id")
|
||||||
if clean_scope == "user":
|
if clean_scope == "user":
|
||||||
user = session.get(User, scope_id)
|
if not _user_exists(session, tenant_id=tenant_id, user_id=scope_id):
|
||||||
if user is None or user.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("User policy not found")
|
raise MailProfileError("User policy not found")
|
||||||
user.mail_profile_policy = normalized
|
return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="user", scope_id=scope_id, policy=normalized)
|
||||||
session.add(user)
|
|
||||||
return normalized
|
|
||||||
if clean_scope == "group":
|
if clean_scope == "group":
|
||||||
group = session.get(Group, scope_id)
|
if not _group_exists(session, tenant_id=tenant_id, group_id=scope_id):
|
||||||
if group is None or group.tenant_id != tenant_id:
|
|
||||||
raise MailProfileError("Group policy not found")
|
raise MailProfileError("Group policy not found")
|
||||||
group.mail_profile_policy = normalized
|
return _write_mail_profile_policy(session, tenant_id=tenant_id, scope_type="group", scope_id=scope_id, policy=normalized)
|
||||||
session.add(group)
|
provider = _campaign_policy_provider()
|
||||||
return normalized
|
if provider is None:
|
||||||
campaign = session.get(_campaign_model(), scope_id)
|
raise MailProfileError("Campaign module is not available")
|
||||||
if campaign is None or campaign.tenant_id != tenant_id:
|
try:
|
||||||
raise MailProfileError("Campaign policy not found")
|
provider.set_campaign_mail_profile_policy(session, tenant_id=tenant_id, campaign_id=scope_id, policy=normalized)
|
||||||
campaign.mail_profile_policy = normalized
|
except ValueError as exc:
|
||||||
session.add(campaign)
|
raise MailProfileError("Campaign policy not found") from exc
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
@@ -1354,10 +1491,11 @@ def mail_profile_id_from_campaign_json(raw_json: dict[str, Any] | None) -> str |
|
|||||||
|
|
||||||
|
|
||||||
def group_ids_for_user(session: Session, *, tenant_id: str, user_id: str) -> list[str]:
|
def group_ids_for_user(session: Session, *, tenant_id: str, user_id: str) -> list[str]:
|
||||||
return [
|
directory = _access_directory()
|
||||||
row.group_id
|
if directory is not None:
|
||||||
for row in session.query(UserGroupMembership).filter(
|
return [group.id for group in directory.groups_for_user(user_id, tenant_id=tenant_id)]
|
||||||
UserGroupMembership.tenant_id == tenant_id,
|
rows = session.execute(
|
||||||
UserGroupMembership.user_id == user_id,
|
text("select group_id from user_group_memberships where tenant_id = :tenant_id and user_id = :user_id"),
|
||||||
)
|
{"tenant_id": tenant_id, "user_id": user_id},
|
||||||
]
|
).all()
|
||||||
|
return [row[0] for row in rows]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
|
||||||
from govoplan_core.core.modules import FrontendModule, MigrationSpec, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate
|
from govoplan_core.core.modules import FrontendModule, MigrationSpec, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate
|
||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
from govoplan_mail.backend.db import models as mail_models # noqa: F401 - populate Mail ORM metadata
|
from govoplan_mail.backend.db import models as mail_models # noqa: F401 - populate Mail ORM metadata
|
||||||
@@ -57,15 +58,23 @@ def _mail_router(context: ModuleContext):
|
|||||||
from govoplan_mail.backend.runtime import configure_runtime
|
from govoplan_mail.backend.runtime import configure_runtime
|
||||||
|
|
||||||
configure_runtime(settings=context.settings)
|
configure_runtime(settings=context.settings)
|
||||||
|
from fastapi import APIRouter
|
||||||
from govoplan_mail.backend.router import router
|
from govoplan_mail.backend.router import router
|
||||||
|
|
||||||
return router
|
aggregate = APIRouter()
|
||||||
|
aggregate.include_router(router)
|
||||||
|
app_env = str(getattr(context.settings, "app_env", "dev")).lower()
|
||||||
|
if app_env == "dev" and bool(getattr(context.settings, "dev_mailbox_api_enabled", False)):
|
||||||
|
from govoplan_mail.backend.dev_router import router as dev_router
|
||||||
|
|
||||||
|
aggregate.include_router(dev_router)
|
||||||
|
return aggregate
|
||||||
|
|
||||||
|
|
||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="mail",
|
id="mail",
|
||||||
name="Mail",
|
name="Mail",
|
||||||
version="0.1.4",
|
version="0.1.5",
|
||||||
dependencies=("access",),
|
dependencies=("access",),
|
||||||
optional_dependencies=(),
|
optional_dependencies=(),
|
||||||
permissions=PERMISSIONS,
|
permissions=PERMISSIONS,
|
||||||
@@ -81,6 +90,20 @@ manifest = ModuleManifest(
|
|||||||
module_id="mail",
|
module_id="mail",
|
||||||
metadata=Base.metadata,
|
metadata=Base.metadata,
|
||||||
script_location=str(Path(__file__).with_name("migrations") / "versions"),
|
script_location=str(Path(__file__).with_name("migrations") / "versions"),
|
||||||
|
retirement_supported=True,
|
||||||
|
retirement_provider=drop_table_retirement_provider(
|
||||||
|
mail_models.MailServerProfile,
|
||||||
|
mail_models.MailProfilePolicy,
|
||||||
|
label="Mail",
|
||||||
|
),
|
||||||
|
retirement_notes="Destructive retirement drops mail-owned database tables after the installer captures a database snapshot.",
|
||||||
|
),
|
||||||
|
uninstall_guard_providers=(
|
||||||
|
persistent_table_uninstall_guard(
|
||||||
|
mail_models.MailServerProfile,
|
||||||
|
mail_models.MailProfilePolicy,
|
||||||
|
label="Mail",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
capability_factories={
|
capability_factories={
|
||||||
"mail.campaign_delivery": lambda context: __import__("govoplan_mail.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context),
|
"mail.campaign_delivery": lambda context: __import__("govoplan_mail.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context),
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""mail profile policies
|
||||||
|
|
||||||
|
Revision ID: 3d4e5f708192
|
||||||
|
Revises: 1b2c3d4e5f70
|
||||||
|
Create Date: 2026-07-06 00:00:00.000000
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "3d4e5f708192"
|
||||||
|
down_revision = "1b2c3d4e5f70"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
inspector = sa.inspect(op.get_bind())
|
||||||
|
if "mail_profile_policies" in inspector.get_table_names():
|
||||||
|
return
|
||||||
|
op.create_table(
|
||||||
|
"mail_profile_policies",
|
||||||
|
sa.Column("id", sa.String(length=36), nullable=False),
|
||||||
|
sa.Column("tenant_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("scope_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("scope_id", sa.String(length=36), nullable=True),
|
||||||
|
sa.Column("policy", sa.JSON(), nullable=False),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], name=op.f("fk_mail_profile_policies_tenant_id_tenants"), ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_mail_profile_policies")),
|
||||||
|
sa.UniqueConstraint("tenant_id", "scope_type", "scope_id", name="uq_mail_profile_policies_scope"),
|
||||||
|
)
|
||||||
|
op.create_index(op.f("ix_mail_profile_policies_tenant_id"), "mail_profile_policies", ["tenant_id"], unique=False)
|
||||||
|
op.create_index(op.f("ix_mail_profile_policies_scope_type"), "mail_profile_policies", ["scope_type"], unique=False)
|
||||||
|
op.create_index(op.f("ix_mail_profile_policies_scope_id"), "mail_profile_policies", ["scope_id"], unique=False)
|
||||||
|
op.create_index("ix_mail_profile_policies_scope", "mail_profile_policies", ["scope_type", "scope_id"], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
inspector = sa.inspect(op.get_bind())
|
||||||
|
if "mail_profile_policies" not in inspector.get_table_names():
|
||||||
|
return
|
||||||
|
op.drop_index("ix_mail_profile_policies_scope", table_name="mail_profile_policies")
|
||||||
|
op.drop_index(op.f("ix_mail_profile_policies_scope_id"), table_name="mail_profile_policies")
|
||||||
|
op.drop_index(op.f("ix_mail_profile_policies_scope_type"), table_name="mail_profile_policies")
|
||||||
|
op.drop_index(op.f("ix_mail_profile_policies_tenant_id"), table_name="mail_profile_policies")
|
||||||
|
op.drop_table("mail_profile_policies")
|
||||||
@@ -20,7 +20,7 @@ from govoplan_mail.backend.schemas import (
|
|||||||
MailServerProfileUpdateRequest,
|
MailServerProfileUpdateRequest,
|
||||||
MailSmtpTestRequest,
|
MailSmtpTestRequest,
|
||||||
)
|
)
|
||||||
from govoplan_core.auth.dependencies import ApiPrincipal, get_api_principal, has_scope
|
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal, has_scope
|
||||||
from govoplan_core.db.session import get_session
|
from govoplan_core.db.session import get_session
|
||||||
from govoplan_mail.backend.mail_profiles import (
|
from govoplan_mail.backend.mail_profiles import (
|
||||||
MailProfileError,
|
MailProfileError,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/mail-webui",
|
"name": "@govoplan/mail-webui",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"./styles/mail-profiles.css": "./src/styles/mail-profiles.css"
|
"./styles/mail-profiles.css": "./src/styles/mail-profiles.css"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.4",
|
"@govoplan/core-webui": "^0.1.5",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^0.555.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user