From cfd384752426d9a215352f5c74aeb6f234b63e2f Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 21 Aug 2026 16:03:29 +0200 Subject: [PATCH] feat(mail): return exact messages from Quick Access --- src/govoplan_mail/backend/manifest.py | 12 ++++- tests/test_manifest.py | 9 ++++ webui/package.json | 2 +- webui/src/features/mail/MailQuickAccess.tsx | 54 +++++++++++++++++---- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/govoplan_mail/backend/manifest.py b/src/govoplan_mail/backend/manifest.py index b437950..b8847c3 100644 --- a/src/govoplan_mail/backend/manifest.py +++ b/src/govoplan_mail/backend/manifest.py @@ -436,7 +436,9 @@ manifest = ModuleManifest( full_page_path="/mail", required_any=("mail:mailbox:read",), order=10, - modes=("browse", "compose"), + modes=("browse", "compose", "select"), + returned_reference_kinds=("mail.message",), + help_context_id="mail.quick_access.messages", ), ), ), @@ -621,6 +623,10 @@ manifest = ModuleManifest( "appears inside the shared Messages drawer alongside independent Postbox and future chat contributions. " "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, " + "folder, UID version, label, and owner route through the shared result contract. Cases discards the label and " + "does not receive headers, participants, preview, body, attachments, credentials, or an access decision; Mail " + "reauthorizes the mailbox list now and the exact message again whenever its owner route is opened. " "Compose deliberately launches the user's configured mail application because GovOPlaN Mail's mailbox is " "read-only; it neither selects a GovOPlaN transport profile nor claims a GovOPlaN delivery. The shared drawer " "does not merge channel state, credentials, custody, delivery semantics, or authorization." @@ -639,6 +645,10 @@ manifest = ModuleManifest( "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, " + "Bei einem aktiven Vorgang gibt die Auswahl einer aktuellen Nachricht nur den mandantengebundenen Profil-, " + "Ordner- und UID-Verweis sowie die Eigentümerroute über den gemeinsamen Ergebnisvertrag zurück. Vorgänge " + "übernehmen weder Kopfzeilen, Beteiligte, Vorschau, Inhalt, Anlagen, Zugangsdaten noch eine Zugriffsentscheidung; " + "Mail prüft den Zugriff beim Auflisten und beim späteren Öffnen erneut. " "da das GovOPlaN-Mail-Postfach nur lesend arbeitet; dabei wird weder ein GovOPlaN-Transportprofil gewählt " "noch eine GovOPlaN-Zustellung behauptet. Die gemeinsame Darstellung führt weder Kanalzustand noch " "Zugangsdaten, Verwahrung, Zustelllogik oder Berechtigungen zusammen." diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 1fff79c..494e9f1 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -9,6 +9,15 @@ from govoplan_mail.backend.manifest import _mail_retirement_provider, get_manife class MailManifestTests(unittest.TestCase): + def test_mail_quick_access_can_return_exact_message_references(self) -> None: + frontend = get_manifest().frontend + self.assertIsNotNone(frontend) + tool = next( + item for item in frontend.quick_access_tools if item.id == "mail.messages" # type: ignore[union-attr] + ) + self.assertIn("select", tool.modes) + self.assertEqual(("mail.message",), tool.returned_reference_kinds) + def test_manifest_declares_optional_addresses_lookup(self) -> None: manifest = get_manifest() diff --git a/webui/package.json b/webui/package.json index 30f4e94..a14433b 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/mail-webui", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/src/features/mail/MailQuickAccess.tsx b/webui/src/features/mail/MailQuickAccess.tsx index c9de076..36a8582 100644 --- a/webui/src/features/mail/MailQuickAccess.tsx +++ b/webui/src/features/mail/MailQuickAccess.tsx @@ -1,8 +1,9 @@ import { useCallback, useRef, useState } from "react"; -import { ExternalLink, FilePenLine, Mail, Pencil } from "lucide-react"; +import { ExternalLink, FilePenLine, Mail, Pencil, X } from "lucide-react"; import { Link } from "react-router"; import { DashboardWidgetList, + Button, DismissibleAlert, EmailAddressInput, LoadingFrame, @@ -34,10 +35,16 @@ type MailQuickAccessData = { type Props = Pick< QuickAccessToolRenderContext, - "settings" | "launchContext" | "close" + "settings" | "launchContext" | "complete" | "cancel" | "close" >; -export default function MailQuickAccess({ settings, launchContext, close }: Props) { +export default function MailQuickAccess({ + settings, + launchContext, + complete, + cancel, + close +}: Props) { const [composing, setComposing] = useState(false); const [recipients, setRecipients] = useState([]); const [suggestions, setSuggestions] = useState([]); @@ -60,6 +67,8 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop }; }, [settings]); const { data, loading, error } = useDashboardWidgetData(load, 0); + const selectingForCase = launchContext.activeObject?.ownerModule === "cases" + && launchContext.activeObject.kind === "case"; const lookupRecipients = useCallback(async (query: string) => { const request = ++lookupRequestRef.current; @@ -85,6 +94,12 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop return ( {error ? {error} : null} + {selectingForCase ? ( +

+ Select an authorized exact message for {launchContext.activeObject?.label}. + Mail content remains in Mail and access is checked again when opened. +

+ ) : null} ({ @@ -93,12 +108,27 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop detail: message.from_header || data?.profileName, meta: formatMessageDate(message.date), leading:
); }