feat(mail): complete Quick Access message launches

This commit is contained in:
2026-08-20 02:39:41 +02:00
parent 9ac8847559
commit 2ad122056e
10 changed files with 219 additions and 12 deletions
+6
View File
@@ -497,6 +497,12 @@ Before claiming a Mail composition is production-ready:
is stable. is stable.
- POP3 except for a future explicit legacy download/import requirement. - POP3 except for a future explicit legacy download/import requirement.
- A full mail client with compose/reply/move/delete/read-state mutation. - A full mail client with compose/reply/move/delete/read-state mutation.
- Quick Access may launch the operating environment's configured composer via
`mailto:`. That explicit handoff is not a GovOPlaN Mail delivery: it selects
no Mail profile or credential, bypasses no Mail policy, and reports no
GovOPlaN delivery result. Recent-message and Drafts links remain read-only
deep links into the authorized Mail profile and preserve their Quick Access
return context.
- Recovery-ledger adoption for future provider-side move, delete, and flag - Recovery-ledger adoption for future provider-side move, delete, and flag
mutations; no such production path exists in the current read-only mailbox. mutations; no such production path exists in the current read-only mailbox.
- Proof that process-local throttling coordinates multiple workers when Redis - Proof that process-local throttling coordinates multiple workers when Redis
+11 -2
View File
@@ -554,7 +554,11 @@ manifest = ModuleManifest(
body=( body=(
"Mail contributes its full mailbox to Communication. When Quick Access is enabled, its compact provider surface " "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. " "appears inside the shared Messages drawer alongside independent Postbox and future chat contributions. "
"The shared drawer does not merge channel state, credentials, custody, delivery semantics, or authorization." "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. "
"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."
), ),
layer="configured", layer="configured",
documentation_types=("user", "admin"), documentation_types=("user", "admin"),
@@ -567,7 +571,12 @@ manifest = ModuleManifest(
"body": ( "body": (
"Mail ordnet das vollständige Postfach Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die kompakte " "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-Oberfläche gemeinsam mit unabhängigen Beiträgen aus Postbox und künftig Chat unter Nachrichten. "
"Die gemeinsame Darstellung führt weder Kanalzustand noch Zugangsdaten, Verwahrung oder Berechtigungen zusammen." "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, "
"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."
), ),
} }
}, },
+1 -1
View File
@@ -26,7 +26,7 @@
} }
}, },
"scripts": { "scripts": {
"test:mail-ui": "rm -rf .mail-test-build && mkdir -p .mail-test-build && printf '{\"type\":\"commonjs\"}\\n' > .mail-test-build/package.json && tsc -p tsconfig.mail-tests.json && node .mail-test-build/tests/mailbox-display.test.js && node .mail-test-build/tests/mailbox-folders.test.js && node .mail-test-build/tests/mail-profile-editor-model.test.js && node .mail-test-build/tests/mail-policy-validation.test.js && node scripts/test-mailbox-icon-button-structure.mjs && node scripts/test-interface-pattern-language.mjs" "test:mail-ui": "rm -rf .mail-test-build && mkdir -p .mail-test-build && printf '{\"type\":\"commonjs\"}\\n' > .mail-test-build/package.json && tsc -p tsconfig.mail-tests.json && node .mail-test-build/tests/mailbox-display.test.js && node .mail-test-build/tests/mailbox-folders.test.js && node .mail-test-build/tests/mailbox-launch.test.js && node .mail-test-build/tests/mail-profile-editor-model.test.js && node .mail-test-build/tests/mail-policy-validation.test.js && node scripts/test-mailbox-icon-button-structure.mjs && node scripts/test-interface-pattern-language.mjs"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.7.2" "typescript": "^5.7.2"
+43 -5
View File
@@ -1,27 +1,39 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { Mail } from "lucide-react"; import { ExternalLink, FilePenLine, Mail, Pencil } from "lucide-react";
import { Link } from "react-router"; import { Link } from "react-router";
import { import {
DashboardWidgetList, DashboardWidgetList,
DismissibleAlert, DismissibleAlert,
LoadingFrame, LoadingFrame,
quickAccessLaunchState,
useDashboardWidgetData, useDashboardWidgetData,
type ApiSettings type QuickAccessToolRenderContext
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
import { import {
bootstrapMailbox, bootstrapMailbox,
listMailServerProfiles, listMailServerProfiles,
type MailMailboxMessageSummary type MailMailboxMessageSummary
} from "../../api/mail"; } from "../../api/mail";
import {
mailboxDraftsLaunchPath,
mailboxMessageLaunchPath
} from "./mailboxLaunch";
type MailQuickAccessData = { type MailQuickAccessData = {
profileName?: string; profileName?: string;
profileId?: string;
draftsFolder?: string | null;
messages: MailMailboxMessageSummary[]; messages: MailMailboxMessageSummary[];
available: boolean; available: boolean;
}; };
export default function MailQuickAccess({ settings }: { settings: ApiSettings }) { type Props = Pick<
QuickAccessToolRenderContext,
"settings" | "launchContext" | "close"
>;
export default function MailQuickAccess({ settings, launchContext, close }: Props) {
const load = useCallback(async (): Promise<MailQuickAccessData> => { const load = useCallback(async (): Promise<MailQuickAccessData> => {
const profiles = await listMailServerProfiles(settings); const profiles = await listMailServerProfiles(settings);
const profile = profiles.find((item) => item.is_active && item.imap); const profile = profiles.find((item) => item.is_active && item.imap);
@@ -29,6 +41,10 @@ export default function MailQuickAccess({ settings }: { settings: ApiSettings })
const response = await bootstrapMailbox(settings, profile.id, "INBOX", 7, 0, false); const response = await bootstrapMailbox(settings, profile.id, "INBOX", 7, 0, false);
return { return {
profileName: profile.name, profileName: profile.name,
profileId: profile.id,
draftsFolder: profile.imap?.folder_mappings?.drafts
|| response.folders.detected_folder_mappings?.drafts
|| null,
messages: response.messages.messages ?? [], messages: response.messages.messages ?? [],
available: true available: true
}; };
@@ -46,11 +62,33 @@ export default function MailQuickAccess({ settings }: { settings: ApiSettings })
detail: message.from_header || data?.profileName, detail: message.from_header || data?.profileName,
meta: formatMessageDate(message.date), meta: formatMessageDate(message.date),
leading: <Mail size={17} aria-hidden="true" />, leading: <Mail size={17} aria-hidden="true" />,
to: "/mail" to: mailboxMessageLaunchPath(data!.profileId!, message),
state: quickAccessLaunchState(launchContext),
onClick: close
}))} }))}
/> />
<div className="dashboard-contribution-footer"> <div className="dashboard-contribution-footer">
<Link className="btn btn-secondary" to="/mail">i18n:govoplan-mail.mail.92379cbb</Link> <a className="btn btn-secondary" href="mailto:" onClick={close}>
<Pencil size={15} aria-hidden="true" /> i18n:govoplan-mail.compose
</a>
{data?.profileId && data.draftsFolder ? (
<Link
className="btn btn-secondary"
to={mailboxDraftsLaunchPath(data.profileId, data.draftsFolder)}
state={quickAccessLaunchState(launchContext)}
onClick={close}
>
<FilePenLine size={15} aria-hidden="true" /> i18n:govoplan-mail.drafts.22a31d86
</Link>
) : null}
<Link
className="btn btn-secondary"
to="/mail"
state={quickAccessLaunchState(launchContext)}
onClick={close}
>
<ExternalLink size={15} aria-hidden="true" /> i18n:govoplan-mail.open_mail
</Link>
</div> </div>
</LoadingFrame> </LoadingFrame>
); );
+46 -3
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Activity, ChevronRight, Database, Home, Mail, MailOpen, Paperclip, RefreshCw, Search, X } from "lucide-react"; import { Activity, ChevronRight, Database, Home, Mail, MailOpen, Paperclip, RefreshCw, Search, X } from "lucide-react";
import { useLocation } from "react-router";
import { ToolbarGroup, ActionToolbar, import { ToolbarGroup, ActionToolbar,
ActionBlockerHint, ActionBlockerHint,
Button, Button,
@@ -30,6 +31,7 @@ import {
"../../api/mail"; "../../api/mail";
import { buildMailboxFolderTree, findFolderNodeId, folderAncestorIds, type MailFolderNode } from "./mailboxFolders"; import { buildMailboxFolderTree, findFolderNodeId, folderAncestorIds, type MailFolderNode } from "./mailboxFolders";
import { isMailboxMessageRead, mailboxSyncState, type MailboxSyncProvenance } from "./mailboxDisplay"; import { isMailboxMessageRead, mailboxSyncState, type MailboxSyncProvenance } from "./mailboxDisplay";
import { mailboxLaunchFolder, parseMailboxLaunch, type MailboxLaunch } from "./mailboxLaunch";
const MAILBOX_DOCUMENTATION = { const MAILBOX_DOCUMENTATION = {
topicId: "mail.workflow.read-mailbox", topicId: "mail.workflow.read-mailbox",
@@ -38,6 +40,7 @@ const MAILBOX_DOCUMENTATION = {
export default function MailboxPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) { export default function MailboxPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
const navigate = useGuardedNavigate(); const navigate = useGuardedNavigate();
const location = useLocation();
const [profiles, setProfiles] = useState<MailServerProfile[]>([]); const [profiles, setProfiles] = useState<MailServerProfile[]>([]);
const [selectedProfileId, setSelectedProfileId] = useState(""); const [selectedProfileId, setSelectedProfileId] = useState("");
const [folders, setFolders] = useState<MailImapFolderResponse[]>([]); const [folders, setFolders] = useState<MailImapFolderResponse[]>([]);
@@ -67,6 +70,7 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
const messageDetailRequestRef = useRef(0); const messageDetailRequestRef = useRef(0);
const mailboxPageCursorsRef = useRef<Record<string, string | null>>({}); const mailboxPageCursorsRef = useRef<Record<string, string | null>>({});
const skipNextMessageLoadRef = useRef(false); const skipNextMessageLoadRef = useRef(false);
const launchRequestRef = useRef<MailboxLaunch | null>(parseMailboxLaunch(location.search));
const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null; const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null;
const imapProfiles = useMemo(() => profiles.filter((profile) => profile.is_active && profile.imap), [profiles]); const imapProfiles = useMemo(() => profiles.filter((profile) => profile.is_active && profile.imap), [profiles]);
@@ -99,6 +103,21 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
: ""; : "";
useEffect(() => {void loadProfiles();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); useEffect(() => {void loadProfiles();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
useEffect(() => {
const request = parseMailboxLaunch(location.search);
launchRequestRef.current = request;
if (!profiles.length || (!request.profileId && !request.folder && !request.folderRole && !request.messageUid)) return;
const usable = profiles.filter((profile) => profile.is_active && profile.imap);
const targetProfileId = request.profileId && usable.some((profile) => profile.id === request.profileId)
? request.profileId
: selectedProfileId || usable[0]?.id || "";
if (!targetProfileId) return;
if (targetProfileId !== selectedProfileId) {
selectProfile(targetProfileId);
return;
}
void loadMailboxBootstrap(targetProfileId);
}, [location.search]);
useEffect(() => {selectedMessageKeyRef.current = selectedMessageKeyState;}, [selectedMessageKeyState]); useEffect(() => {selectedMessageKeyRef.current = selectedMessageKeyState;}, [selectedMessageKeyState]);
useEffect(() => {if (messagePage > messagePageCount) setMessagePage(messagePageCount);}, [messagePage, messagePageCount]); useEffect(() => {if (messagePage > messagePageCount) setMessagePage(messagePageCount);}, [messagePage, messagePageCount]);
useEffect(() => {if (selectedProfileId) void loadMailboxBootstrap(selectedProfileId);}, [selectedProfileId]); useEffect(() => {if (selectedProfileId) void loadMailboxBootstrap(selectedProfileId);}, [selectedProfileId]);
@@ -111,6 +130,19 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize); void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize);
}, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]); }, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]);
useEffect(() => {
const request = launchRequestRef.current;
if (!request || !foldersReady || loadingMessages) return;
const targetProfileMatches = !request.profileId || request.profileId === selectedProfileId;
const targetFolder = mailboxLaunchFolder(request, selectedProfile);
if (!targetProfileMatches || (targetFolder && targetFolder !== selectedFolder)) return;
const target = request.messageUid
? messages.find((message) => message.uid === request.messageUid)
: null;
launchRequestRef.current = null;
if (target) void openMessage(target);
}, [foldersReady, loadingMessages, messages, selectedFolder, selectedProfile, selectedProfileId]);
useEffect(() => { useEffect(() => {
const handlePreviewShortcut = (event: KeyboardEvent) => { const handlePreviewShortcut = (event: KeyboardEvent) => {
if (isEditableTarget(event.target)) return; if (isEditableTarget(event.target)) return;
@@ -158,8 +190,15 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
try { try {
const loaded = await listMailServerProfiles(settings); const loaded = await listMailServerProfiles(settings);
const usable = loaded.filter((profile) => profile.is_active && profile.imap); const usable = loaded.filter((profile) => profile.is_active && profile.imap);
const requestedProfileId = parseMailboxLaunch(location.search).profileId;
setProfiles(loaded); setProfiles(loaded);
setSelectedProfileId((current) => current && usable.some((profile) => profile.id === current) ? current : usable[0]?.id ?? ""); setSelectedProfileId((current) =>
requestedProfileId && usable.some((profile) => profile.id === requestedProfileId)
? requestedProfileId
: current && usable.some((profile) => profile.id === current)
? current
: usable[0]?.id ?? ""
);
if (usable.length === 0) { if (usable.length === 0) {
setFolders([]); setFolders([]);
setFoldersLoadedForProfile(""); setFoldersLoadedForProfile("");
@@ -182,9 +221,13 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
async function loadMailboxBootstrap(profileId = selectedProfileId, refresh = false) { async function loadMailboxBootstrap(profileId = selectedProfileId, refresh = false) {
if (!profileId) return; if (!profileId) return;
const profileInbox = profiles.find((profile) => profile.id === profileId)?.imap?.folder_mappings?.inbox || ""; const profileInbox = profiles.find((profile) => profile.id === profileId)?.imap?.folder_mappings?.inbox || "";
const requestedFolder = foldersLoadedForProfile === profileId && selectedFolder const requestedLaunch = launchRequestRef.current;
const requestedLaunchFolder = requestedLaunch && (!requestedLaunch.profileId || requestedLaunch.profileId === profileId)
? mailboxLaunchFolder(requestedLaunch, profiles.find((profile) => profile.id === profileId) ?? null)
: null;
const requestedFolder = requestedLaunchFolder || (foldersLoadedForProfile === profileId && selectedFolder
? selectedFolder ? selectedFolder
: profileInbox || "INBOX"; : profileInbox || "INBOX");
const folderRequestId = ++folderRequestRef.current; const folderRequestId = ++folderRequestRef.current;
const messageRequestId = ++messageListRequestRef.current; const messageRequestId = ++messageListRequestRef.current;
messageDetailRequestRef.current += 1; messageDetailRequestRef.current += 1;
+59
View File
@@ -0,0 +1,59 @@
type MessageReference = { folder: string; uid: string };
type ProfileFolderMapping = {
imap?: { folder_mappings?: { drafts?: string | null } | null } | null;
};
export type MailboxLaunch = {
profileId: string | null;
folder: string | null;
folderRole: "drafts" | null;
messageUid: string | null;
};
export function parseMailboxLaunch(search: string): MailboxLaunch {
const params = new URLSearchParams(search);
return {
profileId: boundedValue(params.get("profile")),
folder: boundedValue(params.get("folder")),
folderRole: params.get("folderRole") === "drafts" ? "drafts" : null,
messageUid: boundedValue(params.get("message"))
};
}
export function mailboxMessageLaunchPath(
profileId: string,
message: MessageReference
): string {
return mailPath({
profile: profileId,
folder: message.folder,
message: message.uid
});
}
export function mailboxDraftsLaunchPath(profileId: string, folder?: string | null): string {
return mailPath(folder
? { profile: profileId, folder }
: { profile: profileId, folderRole: "drafts" });
}
export function mailboxLaunchFolder(
launch: MailboxLaunch,
profile: ProfileFolderMapping | null
): string | null {
if (launch.folder) return launch.folder;
if (launch.folderRole === "drafts") {
return boundedValue(profile?.imap?.folder_mappings?.drafts ?? null);
}
return null;
}
function mailPath(values: Record<string, string>): string {
const params = new URLSearchParams(values);
return `/mail?${params.toString()}`;
}
function boundedValue(value: string | null): string | null {
const clean = value?.trim() ?? "";
return clean && clean.length <= 500 ? clean : null;
}
+4
View File
@@ -95,6 +95,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy", "i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy",
"i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles", "i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles",
"i18n:govoplan-mail.mail.92379cbb": "Mail", "i18n:govoplan-mail.mail.92379cbb": "Mail",
"i18n:govoplan-mail.compose": "Compose",
"i18n:govoplan-mail.open_mail": "Open Mail",
"i18n:govoplan-mail.quick_access_description": "Recent mailbox messages and mail actions.", "i18n:govoplan-mail.quick_access_description": "Recent mailbox messages and mail actions.",
"i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.", "i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.",
"i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders", "i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders",
@@ -293,6 +295,8 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy", "i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy",
"i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles", "i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles",
"i18n:govoplan-mail.mail.92379cbb": "Mail", "i18n:govoplan-mail.mail.92379cbb": "Mail",
"i18n:govoplan-mail.compose": "Verfassen",
"i18n:govoplan-mail.open_mail": "Mail öffnen",
"i18n:govoplan-mail.quick_access_description": "Aktuelle Posteingangsnachrichten und Mail-Aktionen.", "i18n:govoplan-mail.quick_access_description": "Aktuelle Posteingangsnachrichten und Mail-Aktionen.",
"i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.", "i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.",
"i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders", "i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders",
+1 -1
View File
@@ -19,7 +19,7 @@ const mailQuickAccessTools: QuickAccessToolsUiCapability = {
tools: [ tools: [
{ {
id: "mail.messages", id: "mail.messages",
render: ({ settings }) => createElement(MailQuickAccess, { settings }) render: (context) => createElement(MailQuickAccess, context)
} }
] ]
}; };
+46
View File
@@ -0,0 +1,46 @@
function assertEqual(actual: unknown, expected: unknown): void {
if (actual !== expected) throw new Error(`expected ${String(expected)}, got ${String(actual)}`);
}
function assertDeepEqual(actual: unknown, expected: unknown): void {
const actualJson = JSON.stringify(actual);
const expectedJson = JSON.stringify(expected);
if (actualJson !== expectedJson) throw new Error(`expected ${expectedJson}, got ${actualJson}`);
}
import {
mailboxDraftsLaunchPath,
mailboxLaunchFolder,
mailboxMessageLaunchPath,
parseMailboxLaunch
} from "../src/features/mail/mailboxLaunch";
assertDeepEqual(
parseMailboxLaunch("?profile=profile-1&folder=INBOX%2FTeam&message=42"),
{
profileId: "profile-1",
folder: "INBOX/Team",
folderRole: null,
messageUid: "42"
}
);
assertEqual(
mailboxMessageLaunchPath("profile 1", { folder: "INBOX/Team", uid: "42" }),
"/mail?profile=profile+1&folder=INBOX%2FTeam&message=42"
);
assertEqual(
mailboxDraftsLaunchPath("profile-1"),
"/mail?profile=profile-1&folderRole=drafts"
);
assertEqual(
mailboxDraftsLaunchPath("profile-1", "Entwürfe"),
"/mail?profile=profile-1&folder=Entw%C3%BCrfe"
);
assertEqual(
mailboxLaunchFolder(parseMailboxLaunch("?folderRole=drafts"), {
imap: { folder_mappings: { drafts: "Entwürfe" } }
}),
"Entwürfe"
);
console.log("mailbox launch tests passed");
+2
View File
@@ -19,10 +19,12 @@
"include": [ "include": [
"tests/mailbox-display.test.ts", "tests/mailbox-display.test.ts",
"tests/mailbox-folders.test.ts", "tests/mailbox-folders.test.ts",
"tests/mailbox-launch.test.ts",
"tests/mail-profile-editor-model.test.ts", "tests/mail-profile-editor-model.test.ts",
"tests/mail-policy-validation.test.ts", "tests/mail-policy-validation.test.ts",
"src/features/mail/mailboxDisplay.ts", "src/features/mail/mailboxDisplay.ts",
"src/features/mail/mailboxFolders.ts", "src/features/mail/mailboxFolders.ts",
"src/features/mail/mailboxLaunch.ts",
"src/features/mail/mailProfileEditorModel.ts", "src/features/mail/mailProfileEditorModel.ts",
"src/features/mail/mailPolicyValidation.ts" "src/features/mail/mailPolicyValidation.ts"
] ]