Compare commits

...
4 Commits
Author SHA1 Message Date
zemion c62c7783d6 feat: inventory SMTP infrastructure dependencies
Module Package Release / publish-packages (push) Successful in 12s
2026-08-24 14:56:27 +02:00
zemion ecc1283de7 feat: contribute Messages product surface
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 12:53:44 +02:00
zemion 48205e6e46 fix(webui): bind reusable mail credentials to help
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 11:36:41 +02:00
zemion c31f9b69cb docs: complete German structured documentation
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 01:15:35 +02:00
12 changed files with 413 additions and 21 deletions
+8
View File
@@ -61,6 +61,14 @@ conflicting profile is preserved unless the reviewed fragment explicitly sets
`on_conflict` to `update`. Credential bindings are added idempotently and are
never removed merely because a package omits a credential reference.
Mail also registers a module-owned infrastructure dependency provider. Its
authorized Ops inventory lists every persisted SMTP endpoint and legacy SMTP
profile by stable non-secret reference, state and scope, together with numeric
credential-binding evidence. Before the host deployer changes or removes
`mail.smtp`, it requires a fresh, complete inventory from the same installation
and displays these dependencies in the plan. The inventory never contains
transport credentials or decrypted envelope data.
Preflight does not prove SMTP reachability. After apply, use the normal Mail
profile test and Ops health surfaces. If the receipt says SMTP is unavailable,
is invalid, or is not mounted, import is blocked with an operator-facing
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@govoplan/mail-webui",
"version": "0.1.22",
"version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@govoplan/mail-webui",
"version": "0.1.22",
"version": "0.1.26",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/mail-webui",
"version": "0.1.22",
"version": "0.1.26",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
+2 -2
View File
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-mail"
version = "0.1.22"
version = "0.1.26"
description = "GovOPlaN mail module with backend and WebUI integration."
readme = "README.md"
requires-python = ">=3.12"
license = { file = "LICENSE" }
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.18",
"govoplan-core>=0.1.42",
"pydantic>=2,<3",
"redis>=5,<6",
"SQLAlchemy>=2,<3",
@@ -5,7 +5,7 @@ from dataclasses import dataclass, replace
import re
from typing import Any
from sqlalchemy import or_, select
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from govoplan_core.core.configuration_packages import (
@@ -24,6 +24,8 @@ from govoplan_core.core.configuration_packages import (
from govoplan_core.core.infrastructure_capabilities import (
InfrastructureCapability,
InfrastructureCapabilityReceipt,
InfrastructureDependency,
InfrastructureDependencyProvider,
)
from govoplan_core.security.credential_envelopes import (
CredentialAccessContext,
@@ -51,6 +53,9 @@ from govoplan_mail.backend.server_hierarchy import (
MAIL_CONFIGURATION_CAPABILITY = "mail.configuration"
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY = (
"infrastructure.dependency_inventory.mail"
)
SMTP_PROFILE_FRAGMENT = "smtp_profile"
_SYSTEM_CONFIGURATION_SCOPES = frozenset(
{"system:settings:write", "system:governance:write"}
@@ -132,8 +137,12 @@ class _ProfileTarget:
on_conflict: str
class SqlMailConfigurationProvider(ConfigurationProvider):
class SqlMailConfigurationProvider(
ConfigurationProvider,
InfrastructureDependencyProvider,
):
module_id = "mail"
capability_ids = ("mail.smtp",)
def describe(self) -> ConfigurationProviderDescription:
return ConfigurationProviderDescription(
@@ -204,6 +213,84 @@ class SqlMailConfigurationProvider(ConfigurationProvider):
item for item in import_result.diagnostics if item.severity == "blocker"
)
def infrastructure_dependencies(self) -> tuple[InfrastructureDependency, ...]:
with get_database().session() as session:
return _smtp_infrastructure_dependencies(session)
def _smtp_infrastructure_dependencies(
session: Session,
) -> tuple[InfrastructureDependency, ...]:
endpoints = tuple(
session.execute(
select(MailServerEndpoint)
.where(MailServerEndpoint.protocol == "smtp")
.order_by(MailServerEndpoint.id)
).scalars()
)
binding_counts = {
str(server_id): int(count)
for server_id, count in session.execute(
select(
MailServerCredentialBinding.server_id,
func.count(MailServerCredentialBinding.id),
).group_by(MailServerCredentialBinding.server_id)
)
}
dependencies: list[InfrastructureDependency] = []
endpoint_profile_ids: set[str] = set()
for endpoint in endpoints:
endpoint_profile_ids.add(endpoint.profile_id)
dependencies.append(
InfrastructureDependency(
capability_id="mail.smtp",
module_id="mail",
dependency_type="smtp_endpoint",
dependency_ref=mail_server_ref(endpoint.id) or f"mail:{endpoint.id}",
state="active" if endpoint.is_active else "inactive",
scope=str(endpoint.scope_type or "tenant"),
summary=(
"A persisted Mail SMTP endpoint is bound to the deployment relay."
),
metrics={
"credential_binding_count": binding_counts.get(endpoint.id, 0),
"default_endpoint": int(bool(endpoint.is_default)),
},
required_action=(
"Rebind, migrate, or explicitly retire this SMTP endpoint and its credential-envelope references before changing the relay capability."
),
)
)
legacy_profiles = tuple(
session.execute(
select(MailServerProfile)
.where(MailServerProfile.smtp_config.is_not(None))
.order_by(MailServerProfile.id)
).scalars()
)
for profile in legacy_profiles:
if profile.id in endpoint_profile_ids or not dict(profile.smtp_config or {}):
continue
dependencies.append(
InfrastructureDependency(
capability_id="mail.smtp",
module_id="mail",
dependency_type="legacy_smtp_profile",
dependency_ref=f"mail-profile:{profile.id}",
state="active" if profile.is_active else "inactive",
scope=str(profile.scope_type or "tenant"),
summary=(
"A persisted legacy Mail profile still contains SMTP transport configuration."
),
metrics={"credential_binding_count": 0},
required_action=(
"Migrate or explicitly retire this legacy profile before changing the relay capability."
),
)
)
return tuple(dependencies)
def _preflight_smtp_profile(
session: Session,
@@ -0,0 +1,214 @@
"""German translations for public structured documentation metadata."""
from __future__ import annotations
from typing import Any
GERMAN_STRUCTURED_TRANSLATIONS: dict[str, dict[str, Any]] = {'mail.bounce-processing': {'verification': 'Senden Sie eine Nachricht mit einer eindeutigen '
'Message-ID, nehmen Sie zweimal einen DSN auf und '
'überprüfen Sie eine korrelierte Beobachtung, während '
'die SMTP-Akzeptanz intakt bleibt.'},
'mail.privacy.data-subject-requests': {'limitations': ['Verschlüsselte ausgehende Nutzlasten '
'können vom Empfänger nicht ohne eine '
'unabhängig bestätigte '
'Mail-Befehlsreferenz durchsucht werden.',
'Die externe Mailbox-Löschung befindet '
'sich außerhalb des DSAR-Anbieters und '
'muss mit dem konfigurierten Anbieter '
'koordiniert werden.'],
'steps': ['Führen Sie die Mail-Provider-Suche und '
'Überprüfung von Mailbox-Index, POP3-Import, '
'Profil, Lieferung, Abgleich und '
'Bounce-Dispositionen aus.',
'Bewahren Sie den unveränderlichen '
'Transportnachweis mit seinem Grund auf.',
'Koordinieren Sie die genehmigte '
'Mailbox-Inhaltelöschung mit der autoritativen '
'externen Mailbox und aktualisieren Sie dann den '
'abgeleiteten Index.',
'Verwenden Sie '
'E-Mail-Profillebenszykluskontrollen für '
'genehmigte Änderungen des persönlichen Profils; '
'Bearbeiten Sie nicht direkt verschlüsselte '
'Nutzlast- oder Beweiszeilen.']},
'mail.profile-standard-folder-mappings': {'steps': ['Öffnen Sie einen editierbaren IMAP-Server in '
'einem wiederverwendbaren Mail-Profil.',
'Wählen Sie Ordner erkennen, um die vom '
'Anbieter sichtbaren Ordnernamen zu laden, '
'oder geben Sie genaue Namen manuell ein.',
'Wenden Sie die erkannten Zuordnungen an, '
'überprüfen Sie jede Rolle und lassen Sie '
'unsichere Rollen für automatisches Verhalten '
'leer.',
'Speichern Sie das Profil und laden Sie es '
'neu, um zu überprüfen, ob die Zuordnungen '
'beibehalten wurden.'],
'verification': 'Bestätigen Sie, dass ältere '
'Sent-only-Profile das gleiche '
'effektive Sent-Mapping zeigen, und '
'bestätigen Sie, dass ein '
'kampagnenlokaler Sent-Override '
'unverändert bleibt.'},
'mail.reference.campaign-delivery-contract': {'verification': 'Nachweisen Sie, dass veraltete '
'Revisionen vor der Entschlüsselung '
'fehlschlagen, der Batch-Preflight '
'vor DATA fehlschlägt, zwei '
'Nachrichten eine gesunde '
'Verbindung wiederverwenden, eine '
'veraltete Verbindung vor der '
'nächsten Nachricht wieder '
'verbunden wird, die Trennung nach '
'der DATA nie wiedergegeben wird, '
'systemische Ausfälle verbleibende '
'Jobs anhalten, Anbieterdetails '
'werden gelöscht und das Interface '
'/ Versionsgate wird übergeben.'},
'mail.reference.credentials-egress-retirement': {'verification': 'Testen Sie DNS-Rebinding und '
'verweigerte Adressen, beweisen '
'Sie, dass der verbundene Peer '
'angeheftet ist, injizieren Sie '
'Credential-Scrub- und '
'Auditfehler, wiederholen Sie '
'die Löschung für Idempotenz und '
'führen Sie den '
'Ruhestands-Preflight gegen '
'einen Snapshot aus.'},
'mail.workflow.choose-and-test-profile': {'outcome': 'Die verbrauchende Aufgabe verweist auf ein '
'verfügbares Mail-eigenes Profil und enthält '
'keine kopierte Transportkonfiguration oder '
'Anmeldeinformationen.',
'prerequisites': ['Mail ist installiert und Sie können '
'Profile lesen, verwenden und testen, '
'die im aktuellen Kontext sichtbar '
'sind.',
'Ein Profiladministrator hat '
'Anmeldeinformationen und effektive '
'Richtlinien konfiguriert.'],
'steps': ['Öffnen Sie Mail-Profile und wählen Sie ein '
'sichtbares aktives Profil.',
'Überprüfen Sie den Sicherheitsumfang, die '
'Verfügbarkeit von SMTP/IMAP/JMAP und die '
'Absenderidentität, ohne Anmeldewerte zu '
'erwarten.',
'Führen Sie den entsprechenden SMTP-, IMAP- '
'oder '
'JMAP-Konnektivitäts-/Authentifizierungstest '
'zuerst gegen ein Nicht-Produktionsziel aus.',
'Kehren Sie zur verbrauchenden Aufgabe zurück '
'und wählen Sie dasselbe Profil über den '
'Picker aus.'],
'verification': 'Laden Sie beide Oberflächen neu, '
'bestätigen Sie, dass nur die stabile '
'Referenz vom Verbraucher beibehalten '
'wird, und führen Sie die eigene '
'kontextbezogene Richtlinienvalidierung '
'des Verbrauchers durch.'},
'mail.workflow.legacy-pop3-import': {'fields': [{'label': 'Verkehrssicherheit',
'user_description': 'Verwenden Sie TLS oder '
'STARTTLS; unverschlüsselter '
'Transport unterliegt '
'weiterhin der '
'Deployment-Egress-Richtlinie '
'und ist nicht der sichere '
'Standard.'},
{'label': 'Grenzwerte für Nachrichten und Chargen',
'user_description': 'Bound sowohl jede '
'heruntergeladene Nachricht '
'als auch die gesamte '
'importierte Charge, bevor '
'der Anbieterinhalt in den '
'lokalen verschlüsselten '
'Speicher gelangt.'},
{'label': 'Ausdrücklich Legacy Import ermöglichen',
'user_description': 'Hält die Quelle inaktiv, '
'bis die Endpunktmetadaten '
'und ihre verschlüsselten '
'Anmeldeinformationen beide '
'gespeichert wurden.'},
{'label': 'Anträge auf Löschung nach Einfuhr',
'user_description': 'Erlaubt, aber wählt niemals '
'die separat autorisierte '
'destruktive Anforderung '
'aus; jede Charge benötigt '
'noch eine explizite '
'Bestätigung.'}],
'limitations': ['Ein POP3-Server muss stabile '
'UIDL-Identifikatoren bereitstellen; '
'ansonsten ist eine sichere doppelte '
'Verhinderung nicht verfügbar und der Import '
'wird abgelehnt.',
'POP3 hat keine Ordner- oder Flag-Semantik '
'und ist nicht das empfohlene Protokoll für '
'den laufenden Mailbox-Zugriff.',
'Die Quelllöschung kann nicht zurückgesetzt '
'werden und hat möglicherweise ein '
'unbekanntes Ergebnis, wenn die Verbindung '
'fehlschlägt, während QUIT Löschungen '
'festlegt.'],
'operational_consequences': ['Durch das Ändern von Host-, '
'Port-, Sicherheits-, '
'Kontoidentitäts- oder '
'Importlimits wird die '
'Transportrevision geändert und '
'veraltete Previews ungültig '
'gemacht.',
'Durch das Ermöglichen des '
'Löschens von Quellen entsteht '
'eine irreversible Grenze für '
'externe Effekte; unbekannte '
'QUIT-Ergebnisse erfordern eine '
'Versöhnung statt eines blinden '
'Wiederholens.',
'Die Deaktivierung von '
'Legacy-Importen führt zu '
'regulierten lokalen Importen '
'und deren Nachweisen, die zur '
'Überprüfung zur Verfügung '
'stehen, während der Zugang '
'neuer Anbieter verhindert '
'wird.'],
'steps': ['Bitten Sie einen Mail-Administrator, eine '
'dedizierte POP3-Altquelle zu konfigurieren und '
'explizit zu aktivieren.',
'Testen Sie die Quelle und aktualisieren Sie eine '
'begrenzte Vorschau; Es werden keine '
'Nachrichtenflags oder Löschstatus geändert.',
'Wählen Sie Nachrichten aus und importieren Sie '
'sie in verschlüsselte Pending-Review-Datensätze.',
'Verwenden Sie delete-after-import nur, wenn '
'Richtlinien und eine separate destruktive '
'Berechtigung dies zulassen, und versöhnen Sie '
'dann fehlgeschlagene oder unbekannte Ergebnisse.'],
'verification': 'Nachweisen Sie deaktivierte Richtlinien, '
'TLS- und Authentifizierungsdiagnostik, '
'zerstörungsfreie Vorschau / Import, '
'UIDL-Duplikate-Verhinderung, verschlüsselte '
'Rohdatenspeicherung, separate '
'Löschberechtigung, '
'Import-Vor-Löschen-Auditbestellung und '
'fehlgeschlagene oder ergebnisunbekannte '
'Löschnachweise.'},
'mail.workflow.read-mailbox': {'outcome': 'Die erforderliche Nachricht wurde inspiziert, ohne den '
'Status der Provider-Mailbox zu ändern.',
'prerequisites': ['Ein aktives sichtbares Profil hat IMAP oder '
'JMAP konfiguriert.',
'Sie können beide dieses profil verwenden und '
'seine mailbox lesen.'],
'steps': ['Öffnen Sie Mail und wählen Sie ein autorisiertes IMAP- '
'oder JMAP-fähiges Profil.',
'Wählen Sie einen Ordner aus, überprüfen Sie das '
'Live/Cache-Synchronisationslabel und stellen Sie den '
'begrenzten Nachrichtenindex auf die Seite; JMAP-Suchen '
'werden beim Anbieter ausgeführt.',
'Verwenden Sie den vom Anbieter abgeleiteten '
'Read/Unread-Indikator und öffnen Sie dann nur die für '
'die Aufgabe benötigte Nachricht.',
'Wechseln Sie bei Bedarf zwischen sicheren Klartext- und '
'isolierten HTML-Ansichten und überprüfen Sie die '
'Anhang- oder Nichtverfügbarkeitsangaben, bevor Sie die '
'Vorschau schließen.'],
'verification': 'Aktualisieren Sie das Provider-Postfach '
'unabhängig und bestätigen Sie, dass keine Lese-, '
'Verschiebe-, Lösch-, Antwort- oder Flag-Mutation '
'durch GovOPlaN verursacht wurde.'}}
+61 -4
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
from govoplan_core.core.modules import with_documentation_structured_translations
from govoplan_mail.backend.german_structured_documentation import GERMAN_STRUCTURED_TRANSLATIONS
from dataclasses import replace
from pathlib import Path
@@ -29,7 +32,9 @@ from govoplan_core.core.modules import (
ModuleManifest,
NavItem,
PermissionDefinition,
ProductAvailabilityExplanation,
ProductAreaContribution,
ProductSurfaceContribution,
QuickAccessTool,
RoleTemplate,
)
@@ -43,7 +48,10 @@ from govoplan_core.core.provider_governance import (
from govoplan_core.core.search import SearchSourceProviderRegistration
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_mail.backend.configuration_provider import MAIL_CONFIGURATION_CAPABILITY
from govoplan_mail.backend.configuration_provider import (
MAIL_CONFIGURATION_CAPABILITY,
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
)
from govoplan_mail.backend.documentation import (
documentation_configuration_states,
documentation_topics,
@@ -442,7 +450,7 @@ POP3_PROVIDER = ExternalProviderDeclaration(
manifest = ModuleManifest(
id="mail",
name="Mail",
version="0.1.22",
version="0.1.26",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
optional_dependencies=("campaigns", "addresses", "calendar", "postbox", "search"),
provides_interfaces=(
@@ -559,6 +567,39 @@ manifest = ModuleManifest(
order=40,
),
),
product_surfaces=(
ProductSurfaceContribution(
id="communication.messages",
module_id="mail",
label="i18n:govoplan-core.product_surface.messages",
description="i18n:govoplan-core.product_surface.messages_description",
icon="mail",
entry_path="/messages",
route_path="/mail",
surface_ids=("mail.nav.mail", "mail.route.mail"),
presentations=("task", "reader"),
search_source_ids=("mail.mailbox_messages",),
help_context_ids=("mail.quick_access.messages",),
documentation_topic_ids=("mail.quick-access-and-product-area",),
required_any=("mail:mailbox:read",),
aliases=("/inbox",),
order=10,
unavailable=ProductAvailabilityExplanation(
reason="authorization",
title="i18n:govoplan-core.product_surface.messages_unavailable",
description="i18n:govoplan-core.product_surface.messages_unavailable_description",
resolution="i18n:govoplan-core.product_surface.messages_unavailable_resolution",
responsible_role="i18n:govoplan-core.access_administrator",
),
degraded=ProductAvailabilityExplanation(
reason="provider_degraded",
title="i18n:govoplan-core.product_surface.messages_degraded",
description="i18n:govoplan-core.product_surface.messages_degraded_description",
resolution="i18n:govoplan-core.product_surface.messages_degraded_resolution",
responsible_role="i18n:govoplan-core.integration_operator",
),
),
),
quick_access_tools=(
QuickAccessTool(
id="mail.messages",
@@ -604,6 +645,7 @@ manifest = ModuleManifest(
),
capability_factories={
MAIL_CONFIGURATION_CAPABILITY: _configuration_provider,
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY: _configuration_provider,
"mail.campaign_delivery": lambda context: __import__("govoplan_mail.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context),
CAPABILITY_MAIL_DELIVERY_OUTBOX: lambda context: __import__(
"govoplan_mail.backend.capabilities",
@@ -840,7 +882,9 @@ manifest = ModuleManifest(
"The Mail configuration provider reads the validated mail.smtp deployment capability and derives authoritative endpoint metadata. "
"Missing non-secret transport fields are collected by preflight; authentication is represented only by an existing credential-envelope id. "
"Tenant scope is the default, system scope requires system authority, conflicting profiles are preserved unless update is explicitly reviewed, "
"and an unchanged second apply is a no-op. SMTP reachability remains a separate Mail profile test."
"and an unchanged second apply is a no-op. Before the host deployer changes or removes mail.smtp, Mail inventories every persisted SMTP endpoint, "
"legacy SMTP profile, and credential-binding count through the non-secret Ops dependency report. Missing, stale, or incomplete inventory blocks apply; "
"SMTP reachability remains a separate Mail profile test."
),
layer="configured",
documentation_types=("admin",),
@@ -872,7 +916,9 @@ manifest = ModuleManifest(
"Der Mail-Konfigurationsprovider liest die validierte Bereitstellungsfähigkeit mail.smtp und übernimmt deren maßgebliche Endpunktdaten. "
"Fehlende nicht geheime Transportangaben werden im Preflight abgefragt; Authentifizierung wird ausschließlich durch die ID eines vorhandenen Zugangsdaten-Umschlags referenziert. "
"Mandantenbezug ist der Standard, Systembezug erfordert Systemberechtigung, abweichende vorhandene Profile bleiben ohne ausdrücklich geprüfte Aktualisierung unverändert, "
"und eine unveränderte zweite Anwendung bleibt wirkungslos. Die SMTP-Erreichbarkeit wird weiterhin separat im Mail-Profil getestet."
"und eine unveränderte zweite Anwendung bleibt wirkungslos. Bevor der Host-Deployer mail.smtp ändert oder entfernt, inventarisiert Mail alle gespeicherten SMTP-Endpunkte, "
"älteren SMTP-Profile und die Anzahl ihrer Zugangsdatenbindungen im nicht geheimen Ops-Abhängigkeitsbericht. Ein fehlendes, veraltetes oder unvollständiges Inventar blockiert die Anwendung; "
"die SMTP-Erreichbarkeit wird weiterhin separat im Mail-Profil getestet."
),
}
},
@@ -890,6 +936,9 @@ manifest = ModuleManifest(
body=(
"Mail contributes its full mailbox to Communication. When Quick Access is enabled, its compact provider surface "
"appears inside the shared Messages drawer alongside independent Postbox and future chat contributions. "
"Mail also contributes to the stable Messages product entry: `/messages` and its migration alias `/inbox` open "
"Mail only when its mailbox route is currently authorized and visible in the effective View; otherwise another "
"available Messages owner is selected or the shell explains how access can be restored. "
"Recent entries open the exact authorized profile, folder, and message; Drafts resolves the configured or "
"provider-detected Drafts folder, and the full Mail page keeps the versioned Quick Access return context. "
"When an active Case requests a selection, choosing a recent message returns only its tenant-bound profile, "
@@ -911,6 +960,9 @@ manifest = ModuleManifest(
"body": (
"Mail ordnet das vollständige Postfach Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die kompakte "
"Mail-Oberfläche gemeinsam mit unabhängigen Beiträgen aus Postbox und künftig Chat unter Nachrichten. "
"Mail trägt außerdem zum stabilen Produkteinstieg Nachrichten bei: `/messages` und der Migrationsalias `/inbox` "
"öffnen Mail nur, wenn die Postfachroute aktuell berechtigt und in der wirksamen View sichtbar ist. Andernfalls "
"wählt die Shell einen anderen verfügbaren Nachrichten-Eigentümer oder erklärt die Wiederherstellung des Zugriffs. "
"Aktuelle Einträge öffnen das genaue berechtigte Profil, den Ordner und die Nachricht; Entwürfe verwendet "
"den konfigurierten oder vom Anbieter erkannten Entwurfsordner. Die vollständige Mail-Seite erhält den "
"versionierten Rücksprungkontext. Verfassen öffnet bewusst die konfigurierte Mail-Anwendung des Benutzers, "
@@ -1484,5 +1536,10 @@ manifest = ModuleManifest(
)
manifest = with_documentation_structured_translations(
manifest, locale="de", translations=GERMAN_STRUCTURED_TRANSLATIONS
)
def get_manifest() -> ModuleManifest:
return manifest
+22
View File
@@ -22,6 +22,7 @@ from govoplan_core.db.session import configure_database, reset_database
from govoplan_core.security.credential_envelopes import CredentialEnvelope
from govoplan_mail.backend.configuration_provider import (
MAIL_CONFIGURATION_CAPABILITY,
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
SqlMailConfigurationProvider,
)
from govoplan_mail.backend.db.models import (
@@ -168,9 +169,30 @@ class MailConfigurationProviderTests(unittest.TestCase):
def test_provider_is_registered_and_describes_receipt_bound_fragment(self) -> None:
self.assertIn(MAIL_CONFIGURATION_CAPABILITY, manifest.capability_factories)
self.assertIn(
MAIL_INFRASTRUCTURE_DEPENDENCY_CAPABILITY,
manifest.capability_factories,
)
description = self.provider.describe()
self.assertEqual(("smtp_profile",), description.fragment_types)
def test_inventory_reports_actual_smtp_endpoint_and_credential_binding(self) -> None:
fragment = ConfigurationPackageFragment(
module_id="mail",
fragment_type="smtp_profile",
fragment_id="inventory-smtp",
payload={"credential_envelope_id": "credential-1"},
)
self.provider.apply(fragment, {}, self.context)
dependencies = self.provider.infrastructure_dependencies()
self.assertEqual(1, len(dependencies))
self.assertEqual("mail.smtp", dependencies[0].capability_id)
self.assertEqual("smtp_endpoint", dependencies[0].dependency_type)
self.assertEqual(1, dependencies[0].metrics["credential_binding_count"])
self.assertNotIn("test-mail", str(dependencies[0].to_dict()))
def test_apply_is_idempotent_and_binds_existing_credential_envelope(self) -> None:
fragment = ConfigurationPackageFragment(
module_id="mail",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@govoplan/mail-webui",
"version": "0.1.22",
"version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@govoplan/mail-webui",
"version": "0.1.22",
"version": "0.1.26",
"devDependencies": {
"typescript": "^5.7.2"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/mail-webui",
"version": "0.1.22",
"version": "0.1.26",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -1495,7 +1495,7 @@ function ProfileForm({
{editTarget.kind === "credentials" &&
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
{creatingCredential &&
<FormField label="Reusable credential" documentation={MAIL_PROFILE_WORKFLOW_DOCUMENTATION}>
<FormField label="Reusable credential" documentation={MAIL_PROFILE_WORKFLOW_DOCUMENTATION} helpContextId="mail.profiles" helpModuleId="mail">
<select value={reuseCredentialId} disabled={credentialDisabled} onChange={(event) => setReuseCredentialId(event.target.value)}>
<option value="">Create a new credential</option>
{availableCredentials.map((credential) =>
@@ -1509,13 +1509,13 @@ function ProfileForm({
{creatingCredential && <div />}
{!reuseCredentialId &&
<>
<FormField label="Credential name" documentation={MAIL_PROFILE_DOCUMENTATION}>
<FormField label="Credential name" documentation={MAIL_PROFILE_DOCUMENTATION} helpContextId="mail.profiles" helpModuleId="mail">
<input value={draft.credentialName} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialName: event.target.value })} />
</FormField>
<FormField label="Description">
<input value={draft.credentialDescription} disabled={credentialDisabled} onChange={(event) => setDraft({ ...draft, credentialDescription: event.target.value })} />
</FormField>
<FormField label="Available to modules" help="Choose installed modules. Keep Mail selected to use this credential here.">
<FormField label="Available to modules" help="Choose installed modules. Keep Mail selected to use this credential here." helpContextId="mail.profiles" helpModuleId="mail">
<ReferenceMultiSelect
values={commaValues(draft.credentialAllowedModules)}
onChange={(values) => setDraft({ ...draft, credentialAllowedModules: values.join(", ") })}
@@ -1527,7 +1527,9 @@ function ProfileForm({
</FormField>
<FormField
label="Limited to servers"
help="Choose an announced server or enter a module-qualified reference. Leave blank for every permitted server.">
help="Choose an announced server or enter a module-qualified reference. Leave blank for every permitted server."
helpContextId="mail.profiles"
helpModuleId="mail">
<ReferenceMultiSelect
values={commaValues(draft.credentialAllowedServerRefs)}
onChange={(values) => setDraft({ ...draft, credentialAllowedServerRefs: values.join(", ") })}
@@ -1540,6 +1542,8 @@ function ProfileForm({
</FormField>
<ToggleSwitch
checked={draft.credentialIsDefault}
helpContextId="mail.profiles"
helpModuleId="mail"
disabled={credentialDisabled}
onChange={(checked) => setDraft({ ...draft, credentialIsDefault: checked })}
label={`Default credential for this ${editTarget.protocol.toUpperCase()} server`} />
+3 -3
View File
@@ -1,5 +1,5 @@
import { createElement, lazy } from "react";
import type { MailDevMailboxUiCapability, MailProfilesUiCapability, PlatformWebModule, QuickAccessToolsUiCapability } from "@govoplan/core-webui";
import { messagesProductSurfaceTranslations, type MailDevMailboxUiCapability, type MailProfilesUiCapability, type PlatformWebModule, type QuickAccessToolsUiCapability } from "@govoplan/core-webui";
import { MailProfilePolicyEditor, MailProfileScopeManager } from "./features/mail/MailProfileManagement";
import { validateMailPolicy } from "./features/mail/mailPolicyValidation";
import { mailCredentialReferenceSelectors } from "./features/mail/mailReferenceProviders";
@@ -14,8 +14,8 @@ const mailboxRead = ["mail:mailbox:read"];
const bounceRead = ["mail:bounce:read", "mail:bounce:manage"];
const legacyImportAccess = ["mail:pop3:import", "mail:pop3:manage"];
const translations = {
en: generatedTranslations.en,
de: generatedTranslations.de
en: { ...generatedTranslations.en, ...messagesProductSurfaceTranslations.en },
de: { ...generatedTranslations.de, ...messagesProductSurfaceTranslations.de }
};
const mailQuickAccessTools: QuickAccessToolsUiCapability = {
tools: [