feat(mail): dedicated Mail reader in the CRM

A Mail surface in the Communication group, distinct from Messenger (chat) and the
work-item Inbox: read app-to-app + email messages (subject + rendered body), reply,
and compose (in-app to a person, or external to an email).

- mail-api.ts: crm.mail.list/history/reply + compose (crm.mail.internal/send), reusing
  the messenger directory for the people picker. Live via the AppShell SDK; mock in demo mode.
- mail.tsx: thread list + reader + reply + New-mail composer. HTML bodies render inside a
  SANDBOXED iframe (no scripts) — safe against untrusted email HTML.
- Wired into the sidebar (Communication) + dashboard switch.

The existing work-item Inbox stays as the notifier (IIOS's projector already flags new
mail there); this is where you read it. tsc + next build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 16:38:16 +05:30
parent 4b50d682e6
commit 46aa7a767c
4 changed files with 337 additions and 1 deletions
+2 -1
View File
@@ -33,6 +33,7 @@ export const NAV_GROUPS: NavGroup[] = [
title: "Communication",
items: [
{ key: "messenger", label: "Messenger", icon: "send", subtitle: "Chat with your team and clients" },
{ key: "mail", label: "Mail", icon: "chat", subtitle: "Read and send app messages and email" },
{ key: "inbox", label: "Inbox", icon: "bell", subtitle: "Mentions, replies and updates for you" },
],
},
@@ -79,7 +80,7 @@ export const NAV_ITEMS: NavItem[] = NAV_GROUPS.flatMap((g) => g.items);
// brand-new user with no membership); every other item requires membership, and the
// items mapped here additionally require the given permission. Unmapped items are
// shown to any member. This is UX only — be-crm still enforces every action.
const ALWAYS_VISIBLE = new Set(["dashboard", "profile", "messenger", "inbox"]);
const ALWAYS_VISIBLE = new Set(["dashboard", "profile", "messenger", "mail", "inbox"]);
const NAV_PERMISSION: Record<string, string | undefined> = {
team: "team.manage",
people: "team.manage",