feat(unified-mailbox): account-bounded Unified Mailbox with opt-in cross-account
Rework the sidebar "All accounts" section into a "Unified Mailbox" that, by default, stays within the active login account and its shared/group folders. Merging across multiple logged-in accounts becomes an opt-in sub-option instead of the default, and the standalone per-account "All Mail" virtual folder is folded into the unified All mail / Unread / Starred entries (its folder selection now narrows those lists). Scope: - lib/unified-mailbox.ts: UnifiedAccountClient.crossIncludedMailboxIds; the cross views honor the per-account folder selection (union across accounts = the sum of each account's selection), falling back to inbox+custom when unset. - stores/email-store.ts: buildUnifiedAccountClients gains scopeToClientAccountId (the account boundary) and populates crossIncludedMailboxIds from allMailFolderIds; remove the standalone __all_mail__ fetch/search/load-more branches. - page.tsx: scope to the active account unless cross-account is active (per-user opt-in AND admin gate); the per-role unified mailboxes obey the same scope. Folding: - Drop ALL_MAIL_MAILBOX_ID (lib/jmap/types.ts); thread-list source-folder column now keys on isUnifiedView only; settings folder picker moves under the unified group and shows once any unified entry is enabled. Config: - User: new unifiedCrossAccount (default false); includeGroupInUnified default flips to true; enableAllMailView retired; the three cross-view toggles now gate the unified Unread/Starred/All mail entries. - Admin: new unifiedCrossAccountEnabled gate, default FALSE (cross-account is an admin opt-in; when off the per-user toggle is hidden and the scope is forced account-bounded at runtime). allMailViewEnabled deprecated and normalized forward into crossAllViewEnabled on policy load; cross-view gate labels reworded to "Unified Mailbox: ...". Header: the sidebar section shows "All accounts" when cross-account is active (opt-in AND admin gate AND >1 connected account), else "Unified Mailbox". Migration: - Settings persist v5 -> v6 (exported migrateSettings) - cross-active users keep cross-account; All-Mail-only users get the account-bounded unified All mail entry with folder ids preserved; includeGroupInUnified enabled for every migrated config; fresh installs are account-bounded. - Admin policy: one-shot, marker-guarded migratePolicyUnifiedMailbox (run before configManager.load) enables unifiedCrossAccountEnabled when a cross view was active, so existing cross-account installs keep the behaviour despite the default-false gate. Skipped on read-only config dirs. Locales: sidebar all_accounts (original label) + unified_mailbox (translated, per locale) keys; dead standalone all_mail strings removed across all 20 locales. Docs: FEATURES.md updated to the account-bounded model, the cross-account gate, and the folder-narrowed aggregate entries. Verification: tsc clean, eslint clean, full vitest suite green (incl. translations completeness, cross-view/migration coverage, and the admin policy migration test).
This commit is contained in:
+4
-5
@@ -4,9 +4,8 @@
|
|||||||
|
|
||||||
- Read, compose, reply, reply-all, and forward with a Tiptap rich text editor (inline images, drag-and-drop embedding, tables)
|
- Read, compose, reply, reply-all, and forward with a Tiptap rich text editor (inline images, drag-and-drop embedding, tables)
|
||||||
- Gmail-style threading with inline expansion and an optional conversation toggle
|
- Gmail-style threading with inline expansion and an optional conversation toggle
|
||||||
- Unified mailbox view across all connected accounts – combined Inbox, Sent, Drafts, Junk, Archive, and Trash, with group/shared accounts optionally merged in
|
- Unified Mailbox – combined Inbox, Sent, Drafts, Junk, Archive, and Trash, scoped by default to the active account and its shared/group folders, with an optional admin-gated cross-account mode that spans every connected account
|
||||||
- Cross-account "All accounts" views – All unread, All starred, and All mail spanning every account (including shared/group folders); each aggregate list labels the source folder of every message
|
- Aggregated All mail / Unread / Starred entries in the Unified Mailbox – scoped by the same account boundary (or all accounts in cross-account mode) and narrowed by a per-account folder selection; each list labels the source folder of every message
|
||||||
- "All Mail" view that merges an account's folders (with a configurable folder selection) into a single list
|
|
||||||
- Three selectable mail layouts: split (three-pane), focused list, and reading pane at bottom
|
- Three selectable mail layouts: split (three-pane), focused list, and reading pane at bottom
|
||||||
- Draft auto-save with identity preservation, persisted HTML body, and proper `In-Reply-To` / `References` headers on replies
|
- Draft auto-save with identity preservation, persisted HTML body, and proper `In-Reply-To` / `References` headers on replies
|
||||||
- Attachment upload, download, drag-out to local file system, and inline preview – images, inline PDF on desktop and mobile, composer attachments (click to open), and `.eml` (`message/rfc822`) attachments rendered like an email; image thumbnails and forgotten-attachment warning
|
- Attachment upload, download, drag-out to local file system, and inline preview – images, inline PDF on desktop and mobile, composer attachments (click to open), and `.eml` (`message/rfc822`) attachments rendered like an email; image thumbnails and forgotten-attachment warning
|
||||||
@@ -117,7 +116,7 @@ Automatic browser detection with persistent preference. Configurable locale URL
|
|||||||
- Configurable signature position (above or below quoted text)
|
- Configurable signature position (above or below quoted text)
|
||||||
- Sub-addressing (`user+tag@domain.com`) with configurable delimiter and contextual tag suggestions
|
- Sub-addressing (`user+tag@domain.com`) with configurable delimiter and contextual tag suggestions
|
||||||
- Shared folders across accounts
|
- Shared folders across accounts
|
||||||
- Shared / group (delegated) accounts: their folders appear alongside your own and can be merged into the unified and "All accounts" views ("Include group inboxes"); their messages are fully actionable there – open, mark read, spam / not-spam, move, delete, and archive – with folder unread counts kept in sync
|
- Shared / group (delegated) accounts: their folders appear alongside your own and can be merged into the Unified Mailbox ("Include group inboxes"); their messages are fully actionable there – open, mark read, spam / not-spam, move, delete, and archive – with folder unread counts kept in sync
|
||||||
- Multiple JMAP servers per deployment with optional auto-pick by email domain
|
- Multiple JMAP servers per deployment with optional auto-pick by email domain
|
||||||
- Optional custom JMAP endpoints on the login form (`ALLOW_CUSTOM_JMAP_ENDPOINT`)
|
- Optional custom JMAP endpoints on the login form (`ALLOW_CUSTOM_JMAP_ENDPOINT`)
|
||||||
|
|
||||||
@@ -125,7 +124,7 @@ Automatic browser detection with persistent preference. Configurable locale URL
|
|||||||
|
|
||||||
- Web setup wizard for first launch – guides through JMAP server(s), OAuth/OIDC, session secret, logging, branding (with file upload), and admin password; persists to the admin config dir, no `.env.local` editing required
|
- Web setup wizard for first launch – guides through JMAP server(s), OAuth/OIDC, session secret, logging, branding (with file upload), and admin password; persists to the admin config dir, no `.env.local` editing required
|
||||||
- Stalwart admin dashboard with dedicated policy sections, collapsed into a single tabbed page
|
- Stalwart admin dashboard with dedicated policy sections, collapsed into a single tabbed page
|
||||||
- Admin policy gates for the aggregate mail views – enable or disable the "All Mail" and the cross-account "All unread / starred / all" entries org-wide; each gated view still respects the user's own toggle
|
- Admin policy gates for the Unified Mailbox – enable or disable the All mail / Unread / Starred entries org-wide, plus a cross-account capability gate (off by default; auto-enabled on upgrade for instances that already used the cross-account views); each gated view still respects the user's own toggle
|
||||||
- Split admin storage: `ADMIN_CONFIG_DIR` (operator-authored, mountable read-only after setup) and `ADMIN_STATE_DIR` (runtime audit log and login timestamps)
|
- Split admin storage: `ADMIN_CONFIG_DIR` (operator-authored, mountable read-only after setup) and `ADMIN_STATE_DIR` (runtime audit log and login timestamps)
|
||||||
- File-based secrets for JSON config: `passwordHashFile` (admin password), `sessionSecretFile`, and `oauthClientSecretFile` for Docker/Kubernetes secret mounts
|
- File-based secrets for JSON config: `passwordHashFile` (admin password), `sessionSecretFile`, and `oauthClientSecretFile` for Docker/Kubernetes secret mounts
|
||||||
- Admin toggle for search-engine indexing (`robots.txt` / `noindex`)
|
- Admin toggle for search-engine indexing (`robots.txt` / `noindex`)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { ComposerDraftData } from "@/components/email/email-composer";
|
|||||||
import { ProtocolAccountPicker } from "@/components/protocol/protocol-account-picker";
|
import { ProtocolAccountPicker } from "@/components/protocol/protocol-account-picker";
|
||||||
import { ThreadConversationView } from "@/components/email/thread-conversation-view";
|
import { ThreadConversationView } from "@/components/email/thread-conversation-view";
|
||||||
import { MobileHeader } from "@/components/layout/mobile-header";
|
import { MobileHeader } from "@/components/layout/mobile-header";
|
||||||
import { ThreadGroup, Email, Mailbox, isUnifiedMailboxId, UNIFIED_ROLE_BY_ID, ALL_MAIL_MAILBOX_ID, CROSS_VIEW_BY_ID, isCrossViewId } from "@/lib/jmap/types";
|
import { ThreadGroup, Email, Mailbox, isUnifiedMailboxId, UNIFIED_ROLE_BY_ID, CROSS_VIEW_BY_ID, isCrossViewId } from "@/lib/jmap/types";
|
||||||
import { useAccountStore } from "@/stores/account-store";
|
import { useAccountStore } from "@/stores/account-store";
|
||||||
import { usePolicyStore } from "@/stores/policy-store";
|
import { usePolicyStore } from "@/stores/policy-store";
|
||||||
import type { UnifiedAccountClient } from "@/lib/unified-mailbox";
|
import type { UnifiedAccountClient } from "@/lib/unified-mailbox";
|
||||||
@@ -356,10 +356,7 @@ export default function Home() {
|
|||||||
useProMultiAccountMailboxes();
|
useProMultiAccountMailboxes();
|
||||||
|
|
||||||
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
|
const enableUnifiedMailbox = useSettingsStore((s) => s.enableUnifiedMailbox);
|
||||||
const enableAllMailView = useSettingsStore((s) => s.enableAllMailView);
|
|
||||||
const delayedSendSupported = client?.hasDelayedSend() ?? true;
|
const delayedSendSupported = client?.hasDelayedSend() ?? true;
|
||||||
const allMailViewEnabled = usePolicyStore((s) => s.isFeatureEnabled('allMailViewEnabled'));
|
|
||||||
const showAllMailMailbox = allMailViewEnabled && enableAllMailView;
|
|
||||||
|
|
||||||
// Cross-account "All accounts" views: a sub-feature of the unified mailbox, so
|
// Cross-account "All accounts" views: a sub-feature of the unified mailbox, so
|
||||||
// they require Unified Mailbox to be enabled, plus the admin gate and the
|
// they require Unified Mailbox to be enabled, plus the admin gate and the
|
||||||
@@ -377,18 +374,36 @@ export default function Home() {
|
|||||||
const activeHasMore = isScheduledView ? scheduledHasMore : hasMoreEmails;
|
const activeHasMore = isScheduledView ? scheduledHasMore : hasMoreEmails;
|
||||||
const activeIsLoading = isScheduledView ? isLoadingScheduled : isLoading;
|
const activeIsLoading = isScheduledView ? isLoadingScheduled : isLoading;
|
||||||
const includeGroupInUnified = useSettingsStore((s) => s.includeGroupInUnified);
|
const includeGroupInUnified = useSettingsStore((s) => s.includeGroupInUnified);
|
||||||
|
const unifiedCrossAccount = useSettingsStore((s) => s.unifiedCrossAccount);
|
||||||
|
const unifiedCrossAccountGate = usePolicyStore((s) => s.isFeatureEnabled('unifiedCrossAccountEnabled'));
|
||||||
const accounts = useAccountStore((s) => s.accounts);
|
const accounts = useAccountStore((s) => s.accounts);
|
||||||
const connectedAccountsSignature = useMemo(
|
const connectedAccountsSignature = useMemo(
|
||||||
() => accounts.filter((a) => a.isConnected).map((a) => a.id).sort().join(","),
|
() => accounts.filter((a) => a.isConnected).map((a) => a.id).sort().join(","),
|
||||||
[accounts],
|
[accounts],
|
||||||
);
|
);
|
||||||
|
// Cross-account is "active" when the user opted in, the admin allows it, and
|
||||||
|
// more than one account is connected. Drives the sidebar header label: the
|
||||||
|
// old "All accounts" when spanning accounts, else "Unified Mailbox".
|
||||||
|
const crossAccountActive =
|
||||||
|
unifiedCrossAccount &&
|
||||||
|
unifiedCrossAccountGate &&
|
||||||
|
accounts.filter((a) => a.isConnected).length > 1;
|
||||||
|
|
||||||
// Builds the populated UnifiedAccountClient[] used by the unified-view
|
// Builds the populated UnifiedAccountClient[] used by the unified-view
|
||||||
// effects and one-shot actions in this page. Reads the includeGroup
|
// effects and one-shot actions in this page. Reads the settings at call time
|
||||||
// setting at call time so the latest toggle value is always honored.
|
// so the latest toggle values are always honored. When the cross-account
|
||||||
|
// sub-option is off, the unified mailbox stays within the active account
|
||||||
|
// boundary (its own + shared folders); when on, it spans every login account.
|
||||||
const buildPopulatedUnifiedAccounts = useCallback(async (): Promise<UnifiedAccountClient[]> => {
|
const buildPopulatedUnifiedAccounts = useCallback(async (): Promise<UnifiedAccountClient[]> => {
|
||||||
|
// Cross-account scope requires both the per-user opt-in and the admin
|
||||||
|
// capability gate; otherwise stay within the active account boundary.
|
||||||
|
const crossAccount = useSettingsStore.getState().unifiedCrossAccount
|
||||||
|
&& usePolicyStore.getState().isFeatureEnabled('unifiedCrossAccountEnabled');
|
||||||
return buildUnifiedAccountClients({
|
return buildUnifiedAccountClients({
|
||||||
includeGroup: useSettingsStore.getState().includeGroupInUnified,
|
includeGroup: useSettingsStore.getState().includeGroupInUnified,
|
||||||
|
scopeToClientAccountId: crossAccount
|
||||||
|
? undefined
|
||||||
|
: (useAccountStore.getState().activeAccountId ?? undefined),
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -1025,7 +1040,7 @@ export default function Home() {
|
|||||||
if (built.length < 2 && !hasGroupEntry && !isEmbedded) return;
|
if (built.length < 2 && !hasGroupEntry && !isEmbedded) return;
|
||||||
refreshUnifiedCounts(built);
|
refreshUnifiedCounts(built);
|
||||||
});
|
});
|
||||||
}, [enableUnifiedMailbox, includeGroupInUnified, isEmbedded, isAuthenticated, client, mailboxes, connectedAccountsSignature, buildPopulatedUnifiedAccounts, refreshUnifiedCounts, refreshCrossCounts, showCrossUnread, showCrossStarred, showCrossAll]);
|
}, [enableUnifiedMailbox, includeGroupInUnified, unifiedCrossAccount, activeAccountId, isEmbedded, isAuthenticated, client, mailboxes, connectedAccountsSignature, buildPopulatedUnifiedAccounts, refreshUnifiedCounts, refreshCrossCounts, showCrossUnread, showCrossStarred, showCrossAll]);
|
||||||
|
|
||||||
// System-notification click handler. The push SW navigates the user back
|
// System-notification click handler. The push SW navigates the user back
|
||||||
// here with `?email=<id>` (specific email it built the toast from) or
|
// here with `?email=<id>` (specific email it built the toast from) or
|
||||||
@@ -2413,14 +2428,12 @@ export default function Home() {
|
|||||||
// Get current mailbox name for mobile header
|
// Get current mailbox name for mobile header
|
||||||
const currentMailboxName = isScheduledView
|
const currentMailboxName = isScheduledView
|
||||||
? t('sidebar.scheduled')
|
? t('sidebar.scheduled')
|
||||||
: selectedMailbox === ALL_MAIL_MAILBOX_ID
|
: (() => {
|
||||||
? t('sidebar.mailboxes.all_mail')
|
const mb = mailboxes.find(m => m.id === selectedMailbox);
|
||||||
: (() => {
|
return mb
|
||||||
const mb = mailboxes.find(m => m.id === selectedMailbox);
|
? localizeMailboxName(mb.role, mb.name, (k) => t(`sidebar.mailboxes.${k}`))
|
||||||
return mb
|
: "Inbox";
|
||||||
? localizeMailboxName(mb.role, mb.name, (k) => t(`sidebar.mailboxes.${k}`))
|
})();
|
||||||
: "Inbox";
|
|
||||||
})();
|
|
||||||
const isFocusedMailLayout = mailLayout === 'focus';
|
const isFocusedMailLayout = mailLayout === 'focus';
|
||||||
const isHorizontalMailLayout = mailLayout === 'horizontal' && !isMobile && !isTablet;
|
const isHorizontalMailLayout = mailLayout === 'horizontal' && !isMobile && !isTablet;
|
||||||
const hasViewerContent = showComposer || Boolean(conversationThread) || Boolean(selectedEmail);
|
const hasViewerContent = showComposer || Boolean(conversationThread) || Boolean(selectedEmail);
|
||||||
@@ -2708,7 +2721,7 @@ export default function Home() {
|
|||||||
selectedKeyword={selectedKeyword}
|
selectedKeyword={selectedKeyword}
|
||||||
scheduledTotal={scheduledTotal}
|
scheduledTotal={scheduledTotal}
|
||||||
showScheduledMailbox={delayedSendSupported}
|
showScheduledMailbox={delayedSendSupported}
|
||||||
showAllMailMailbox={showAllMailMailbox}
|
crossAccountActive={crossAccountActive}
|
||||||
showCrossUnread={showCrossUnread}
|
showCrossUnread={showCrossUnread}
|
||||||
showCrossStarred={showCrossStarred}
|
showCrossStarred={showCrossStarred}
|
||||||
showCrossAll={showCrossAll}
|
showCrossAll={showCrossAll}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import type { SettingsPolicy, FeatureGates } from '@/lib/admin/types';
|
|||||||
import { DEFAULT_FEATURE_GATES, DEFAULT_POLICY } from '@/lib/admin/types';
|
import { DEFAULT_FEATURE_GATES, DEFAULT_POLICY } from '@/lib/admin/types';
|
||||||
import { apiFetch } from '@/lib/browser-navigation';
|
import { apiFetch } from '@/lib/browser-navigation';
|
||||||
|
|
||||||
const EXCLUDED_FEATURE_GATES: (keyof FeatureGates)[] = ['pluginsEnabled', 'pluginsUploadEnabled', 'themesEnabled', 'userThemesEnabled'];
|
// `allMailViewEnabled` is deprecated (folded into `crossAllViewEnabled`, normalized
|
||||||
|
// forward on policy load), so it is hidden from the admin UI.
|
||||||
|
const EXCLUDED_FEATURE_GATES: (keyof FeatureGates)[] = ['pluginsEnabled', 'pluginsUploadEnabled', 'themesEnabled', 'userThemesEnabled', 'allMailViewEnabled'];
|
||||||
|
|
||||||
const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; description: string }>> = {
|
const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; description: string }>> = {
|
||||||
sidebarAppsEnabled: { label: 'Sidebar Apps', description: 'Allow custom web apps in navigation rail' },
|
sidebarAppsEnabled: { label: 'Sidebar Apps', description: 'Allow custom web apps in navigation rail' },
|
||||||
@@ -22,10 +24,10 @@ const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; d
|
|||||||
folderIconsEnabled: { label: 'Folder Icons', description: 'Allow custom folder icon picker' },
|
folderIconsEnabled: { label: 'Folder Icons', description: 'Allow custom folder icon picker' },
|
||||||
hoverActionsConfigEnabled: { label: 'Hover Actions Config', description: 'Allow users to customize email hover actions' },
|
hoverActionsConfigEnabled: { label: 'Hover Actions Config', description: 'Allow users to customize email hover actions' },
|
||||||
filesEnabled: { label: 'Files (WebDAV)', description: 'Enable file storage via WebDAV. WARNING: Large uploads can cause Stalwart/RocksDB instability. Not recommended for production.' },
|
filesEnabled: { label: 'Files (WebDAV)', description: 'Enable file storage via WebDAV. WARNING: Large uploads can cause Stalwart/RocksDB instability. Not recommended for production.' },
|
||||||
allMailViewEnabled: { label: 'All Mail View', description: 'Show a virtual "All Mail" folder that merges messages from across an account’s folders into one list. Users choose which folders are included. Requires the per-user toggle in Settings → Appearance.' },
|
crossUnreadViewEnabled: { label: 'Unified Mailbox: Unread', description: 'Allow an "Unread" entry in the Unified Mailbox section that lists unread mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' },
|
||||||
crossUnreadViewEnabled: { label: 'All Accounts: Unread', description: 'Allow an "All unread" entry in the All accounts section that lists unread mail across every account (incl. shared folders), spanning all folders except junk, sent, archive, trash and drafts. Requires the matching per-user toggle in Settings → Appearance.' },
|
crossStarredViewEnabled: { label: 'Unified Mailbox: Starred', description: 'Allow a "Starred" entry in the Unified Mailbox section that lists flagged/starred mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' },
|
||||||
crossStarredViewEnabled: { label: 'All Accounts: Starred', description: 'Allow an "All starred" entry in the All accounts section that lists flagged/starred mail across every account (incl. shared folders), spanning all folders except junk, sent, archive, trash and drafts. Requires the matching per-user toggle in Settings → Appearance.' },
|
crossAllViewEnabled: { label: 'Unified Mailbox: All Mail', description: 'Allow an "All mail" entry in the Unified Mailbox section that lists all mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' },
|
||||||
crossAllViewEnabled: { label: 'All Accounts: All Mail', description: 'Allow an "All mail" entry in the All accounts section that lists all mail across every account (incl. shared folders), spanning all folders except junk, sent, archive, trash and drafts. Requires the matching per-user toggle in Settings → Appearance.' },
|
unifiedCrossAccountEnabled: { label: 'Unified Mailbox: Cross-account', description: 'Allow users to expand the Unified Mailbox beyond the active account boundary so its lists merge across every logged-in account. When off, the Unified Mailbox stays within the active account and its shared folders.' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const RESTRICTABLE_SETTINGS = [
|
const RESTRICTABLE_SETTINGS = [
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { formatDate, formatDateTime, stripInvisibleLeading } from "@/lib/utils";
|
import { formatDate, formatDateTime, stripInvisibleLeading } from "@/lib/utils";
|
||||||
import { Email, ThreadGroup, ALL_MAIL_MAILBOX_ID } from "@/lib/jmap/types";
|
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SelectableAvatar } from "@/components/email/selectable-avatar";
|
import { SelectableAvatar } from "@/components/email/selectable-avatar";
|
||||||
import { Paperclip, Star, Pin, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square, Reply, Forward, CalendarClock, Folder } from "lucide-react";
|
import { Paperclip, Star, Pin, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square, Reply, Forward, CalendarClock, Folder } from "lucide-react";
|
||||||
@@ -97,7 +97,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
const showAvatarsInJunk = useSettingsStore((state) => state.showAvatarsInJunk);
|
const showAvatarsInJunk = useSettingsStore((state) => state.showAvatarsInJunk);
|
||||||
const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk;
|
const hideJunkAvatarImages = currentMailboxRole === 'junk' && !showAvatarsInJunk;
|
||||||
// Show the originating folder in the aggregate "All …" views.
|
// Show the originating folder in the aggregate "All …" views.
|
||||||
const showSourceFolder = (isUnifiedView || selectedMailbox === ALL_MAIL_MAILBOX_ID) && !!email.sourceFolder;
|
const showSourceFolder = isUnifiedView && !!email.sourceFolder;
|
||||||
const getAccountById = useAccountStore((state) => state.getAccountById);
|
const getAccountById = useAccountStore((state) => state.getAccountById);
|
||||||
const accountColor = email.accountId ? getAccountById(email.accountId)?.avatarColor : undefined;
|
const accountColor = email.accountId ? getAccountById(email.accountId)?.avatarColor : undefined;
|
||||||
const isChecked = selectedEmailIds.has(email.id);
|
const isChecked = selectedEmailIds.has(email.id);
|
||||||
@@ -459,7 +459,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const { selectedMailbox, mailboxes, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection, isUnifiedView, unifiedRole } = useEmailStore();
|
const { selectedMailbox, mailboxes, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection, isUnifiedView, unifiedRole } = useEmailStore();
|
||||||
const showSourceFolder = (isUnifiedView || selectedMailbox === ALL_MAIL_MAILBOX_ID) && !!latestEmail.sourceFolder;
|
const showSourceFolder = isUnifiedView && !!latestEmail.sourceFolder;
|
||||||
const getAccountById = useAccountStore((state) => state.getAccountById);
|
const getAccountById = useAccountStore((state) => state.getAccountById);
|
||||||
const threadAccountColor = latestEmail.accountId ? getAccountById(latestEmail.accountId)?.avatarColor : undefined;
|
const threadAccountColor = latestEmail.accountId ? getAccountById(latestEmail.accountId)?.avatarColor : undefined;
|
||||||
// In Sent/Drafts folders, show recipient instead of sender (which is always
|
// In Sent/Drafts folders, show recipient instead of sender (which is always
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ interface SidebarProps {
|
|||||||
onRefreshMailboxes?: () => void;
|
onRefreshMailboxes?: () => void;
|
||||||
scheduledTotal?: number;
|
scheduledTotal?: number;
|
||||||
showScheduledMailbox?: boolean;
|
showScheduledMailbox?: boolean;
|
||||||
/** Gated "All Mail" virtual folder that merges all of the account's folders. */
|
/** True when the unified view spans multiple login accounts (cross-account).
|
||||||
showAllMailMailbox?: boolean;
|
* Drives the section header: "All accounts" when true, else "Unified Mailbox". */
|
||||||
/** Gated cross-account views in the "All accounts" section. */
|
crossAccountActive?: boolean;
|
||||||
|
/** Gated All mail / Unread / Starred entries in the "Unified Mailbox" section. */
|
||||||
showCrossUnread?: boolean;
|
showCrossUnread?: boolean;
|
||||||
showCrossStarred?: boolean;
|
showCrossStarred?: boolean;
|
||||||
showCrossAll?: boolean;
|
showCrossAll?: boolean;
|
||||||
@@ -692,7 +693,7 @@ export function Sidebar({
|
|||||||
onRefreshMailboxes,
|
onRefreshMailboxes,
|
||||||
scheduledTotal = 0,
|
scheduledTotal = 0,
|
||||||
showScheduledMailbox = false,
|
showScheduledMailbox = false,
|
||||||
showAllMailMailbox = false,
|
crossAccountActive = false,
|
||||||
showCrossUnread = false,
|
showCrossUnread = false,
|
||||||
showCrossStarred = false,
|
showCrossStarred = false,
|
||||||
showCrossAll = false,
|
showCrossAll = false,
|
||||||
@@ -1017,20 +1018,10 @@ export function Sidebar({
|
|||||||
|
|
||||||
{/* Mailbox List */}
|
{/* Mailbox List */}
|
||||||
<div className="flex-1 overflow-y-auto" data-tour="sidebar">
|
<div className="flex-1 overflow-y-auto" data-tour="sidebar">
|
||||||
{showAllMailMailbox && (
|
|
||||||
<SidebarRow
|
|
||||||
icon={<Mails className={cn("w-4 h-4 flex-shrink-0", selectedMailbox === '__all_mail__' ? "text-foreground" : "text-muted-foreground")} />}
|
|
||||||
label={t('mailboxes.all_mail')}
|
|
||||||
depth={0}
|
|
||||||
isSelected={!selectedKeyword && selectedMailbox === '__all_mail__'}
|
|
||||||
onClick={() => onMailboxSelect?.('__all_mail__')}
|
|
||||||
isCollapsed={isCollapsed}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(showUnified || showCrossUnread || showCrossStarred || showCrossAll) && (
|
{(showUnified || showCrossUnread || showCrossStarred || showCrossAll) && (
|
||||||
<div>
|
<div>
|
||||||
<SidebarSectionHeader
|
<SidebarSectionHeader
|
||||||
label={t("all_accounts")}
|
label={t(crossAccountActive ? "all_accounts" : "unified_mailbox")}
|
||||||
expanded={unifiedExpanded}
|
expanded={unifiedExpanded}
|
||||||
onToggle={toggleUnified}
|
onToggle={toggleUnified}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
|
|||||||
@@ -118,19 +118,26 @@ function MailLayoutPreview({
|
|||||||
export function LayoutSettings() {
|
export function LayoutSettings() {
|
||||||
const t = useTranslations('settings.appearance');
|
const t = useTranslations('settings.appearance');
|
||||||
const tEmail = useTranslations('settings.email_behavior');
|
const tEmail = useTranslations('settings.email_behavior');
|
||||||
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, enableAllMailView, allMailFolderIds, enableCrossUnreadView, enableCrossStarredView, enableCrossAllView, colorfulSidebarIcons, tintListRowsByTag, showFolderTotalCount, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, includeGroupInUnified, unifiedCrossAccount, allMailFolderIds, enableCrossUnreadView, enableCrossStarredView, enableCrossAllView, colorfulSidebarIcons, tintListRowsByTag, showFolderTotalCount, mailLayout, proInterface, updateSetting } = useSettingsStore();
|
||||||
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
||||||
const accounts = useAccountStore(s => s.accounts);
|
const accounts = useAccountStore(s => s.accounts);
|
||||||
const activeAccountId = useAccountStore(s => s.activeAccountId);
|
const activeAccountId = useAccountStore(s => s.activeAccountId);
|
||||||
const mailboxes = useEmailStore(s => s.mailboxes);
|
const mailboxes = useEmailStore(s => s.mailboxes);
|
||||||
const hasGroupInboxes = useMemo(() => mailboxes.some(m => m.isShared), [mailboxes]);
|
const hasGroupInboxes = useMemo(() => mailboxes.some(m => m.isShared), [mailboxes]);
|
||||||
const allMailViewAllowed = isFeatureEnabled('allMailViewEnabled');
|
const connectedAccountCount = useMemo(() => accounts.filter(a => a.isConnected).length, [accounts]);
|
||||||
// Cross-account "All accounts" views, each gated independently by the admin.
|
const unifiedCrossAccountAllowed = isFeatureEnabled('unifiedCrossAccountEnabled');
|
||||||
|
// Unified Mailbox entries (All mail / Unread / Starred), each gated independently
|
||||||
|
// by the admin. Scope (single account vs. cross-account) is governed by
|
||||||
|
// `unifiedCrossAccount`; the folder picker below narrows which own folders feed them.
|
||||||
const crossViews = [
|
const crossViews = [
|
||||||
{ setting: 'enableCrossUnreadView', value: enableCrossUnreadView, allowed: isFeatureEnabled('crossUnreadViewEnabled'), labelKey: 'cross_unread.label', descKey: 'cross_unread.description' },
|
{ setting: 'enableCrossUnreadView', value: enableCrossUnreadView, allowed: isFeatureEnabled('crossUnreadViewEnabled'), labelKey: 'cross_unread.label', descKey: 'cross_unread.description' },
|
||||||
{ setting: 'enableCrossStarredView', value: enableCrossStarredView, allowed: isFeatureEnabled('crossStarredViewEnabled'), labelKey: 'cross_starred.label', descKey: 'cross_starred.description' },
|
{ setting: 'enableCrossStarredView', value: enableCrossStarredView, allowed: isFeatureEnabled('crossStarredViewEnabled'), labelKey: 'cross_starred.label', descKey: 'cross_starred.description' },
|
||||||
{ setting: 'enableCrossAllView', value: enableCrossAllView, allowed: isFeatureEnabled('crossAllViewEnabled'), labelKey: 'cross_all.label', descKey: 'cross_all.description' },
|
{ setting: 'enableCrossAllView', value: enableCrossAllView, allowed: isFeatureEnabled('crossAllViewEnabled'), labelKey: 'cross_all.label', descKey: 'cross_all.description' },
|
||||||
] as const;
|
] as const;
|
||||||
|
// The folder picker narrows the own folders included in the entries above; show
|
||||||
|
// it once the user has enabled at least one of them.
|
||||||
|
const anyCrossEnabled = enableCrossUnreadView || enableCrossStarredView || enableCrossAllView;
|
||||||
|
const anyCrossAllowed = crossViews.some(c => c.allowed);
|
||||||
|
|
||||||
// Own (non-shared) folders and the active account's All Mail selection. The
|
// Own (non-shared) folders and the active account's All Mail selection. The
|
||||||
// selection is per account: a missing entry = never configured, which
|
// selection is per account: a missing entry = never configured, which
|
||||||
@@ -244,6 +251,21 @@ export function LayoutSettings() {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{enableUnifiedMailbox && connectedAccountCount > 1 && unifiedCrossAccountAllowed && !isSettingHidden('unifiedCrossAccount') && (
|
||||||
|
<div className="ml-4 border-l-2 border-border pl-4 -mt-2">
|
||||||
|
<SettingItem
|
||||||
|
label={t('unified_mailbox.cross_account.label')}
|
||||||
|
description={t('unified_mailbox.cross_account.description')}
|
||||||
|
locked={isSettingLocked('unifiedCrossAccount')}
|
||||||
|
>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={unifiedCrossAccount}
|
||||||
|
onChange={(v) => updateSetting('unifiedCrossAccount', v)}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{enableUnifiedMailbox && hasGroupInboxes && !isSettingHidden('includeGroupInUnified') && (
|
{enableUnifiedMailbox && hasGroupInboxes && !isSettingHidden('includeGroupInUnified') && (
|
||||||
<div className="ms-4 border-s-2 border-border ps-4 -mt-2">
|
<div className="ms-4 border-s-2 border-border ps-4 -mt-2">
|
||||||
<SettingItem
|
<SettingItem
|
||||||
@@ -259,8 +281,9 @@ export function LayoutSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{enableUnifiedMailbox && crossViews.some(c => c.allowed) && (
|
{enableUnifiedMailbox && anyCrossAllowed && (
|
||||||
<div className="ms-4 border-s-2 border-border ps-4 -mt-2 space-y-2">
|
<div className="ms-4 border-s-2 border-border ps-4 -mt-2 space-y-2">
|
||||||
|
|
||||||
{crossViews.map(({ setting, value, allowed, labelKey, descKey }) => (
|
{crossViews.map(({ setting, value, allowed, labelKey, descKey }) => (
|
||||||
allowed && !isSettingHidden(setting) && (
|
allowed && !isSettingHidden(setting) && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
@@ -279,21 +302,9 @@ export function LayoutSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allMailViewAllowed && !isSettingHidden('enableAllMailView') && (
|
{enableUnifiedMailbox && anyCrossAllowed && anyCrossEnabled && (
|
||||||
<SettingItem
|
|
||||||
label={t('all_mail.label')}
|
|
||||||
description={t('all_mail.description')}
|
|
||||||
locked={isSettingLocked('enableAllMailView')}
|
|
||||||
>
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={enableAllMailView}
|
|
||||||
onChange={(v) => updateSetting('enableAllMailView', v)}
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allMailViewAllowed && enableAllMailView && (
|
|
||||||
<div className="ms-4 border-s-2 border-border ps-4 -mt-2 space-y-2">
|
<div className="ms-4 border-s-2 border-border ps-4 -mt-2 space-y-2">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-foreground">{t('all_mail.folders_label')}</div>
|
<div className="text-sm font-medium text-foreground">{t('all_mail.folders_label')}</div>
|
||||||
<div className="text-xs text-muted-foreground">{t('all_mail.folders_description')}</div>
|
<div className="text-xs text-muted-foreground">{t('all_mail.folders_description')}</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { configManager } from "./lib/admin/config-manager";
|
import { configManager } from "./lib/admin/config-manager";
|
||||||
import { initAdminPassword } from "./lib/admin/password";
|
import { initAdminPassword } from "./lib/admin/password";
|
||||||
import { migrateLegacyAdminLayout } from "./lib/admin/migrate";
|
import { migrateLegacyAdminLayout, migratePolicyUnifiedMailbox } from "./lib/admin/migrate";
|
||||||
import { detectSetupState } from "./lib/setup/state";
|
import { detectSetupState } from "./lib/setup/state";
|
||||||
import { ensureSetupToken } from "./lib/setup/token";
|
import { ensureSetupToken } from "./lib/setup/token";
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ console.info(`Bulwark Webmail v${current}`);
|
|||||||
// Initialize admin config and password bootstrap. Migration runs first so
|
// Initialize admin config and password bootstrap. Migration runs first so
|
||||||
// existing v1 layouts are split before anything reads admin.json.
|
// existing v1 layouts are split before anything reads admin.json.
|
||||||
migrateLegacyAdminLayout()
|
migrateLegacyAdminLayout()
|
||||||
|
.then(() => migratePolicyUnifiedMailbox())
|
||||||
.then(() => configManager.load())
|
.then(() => configManager.load())
|
||||||
.then(() => initAdminPassword())
|
.then(() => initAdminPassword())
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
|||||||
@@ -42,6 +42,26 @@ describe('getCrossIncludedMailboxes', () => {
|
|||||||
const ids = getCrossIncludedMailboxes(account).map((m) => m.id);
|
const ids = getCrossIncludedMailboxes(account).map((m) => m.id);
|
||||||
expect(ids).toEqual(['inbox', 'projects']);
|
expect(ids).toEqual(['inbox', 'projects']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('honors an explicit crossIncludedMailboxIds selection (folder picker)', () => {
|
||||||
|
const account = makeAccount({
|
||||||
|
accountId: 'a',
|
||||||
|
mailboxes: [mb('inbox', 'inbox'), mb('projects', undefined), mb('archive', 'archive')],
|
||||||
|
// user picked inbox + archive, excluded projects - overrides role exclusion
|
||||||
|
crossIncludedMailboxIds: ['inbox', 'archive'],
|
||||||
|
});
|
||||||
|
const ids = getCrossIncludedMailboxes(account).map((m) => m.id);
|
||||||
|
expect(ids).toEqual(['inbox', 'archive']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('an empty selection yields no folders', () => {
|
||||||
|
const account = makeAccount({
|
||||||
|
accountId: 'a',
|
||||||
|
mailboxes: [mb('inbox', 'inbox'), mb('projects', undefined)],
|
||||||
|
crossIncludedMailboxIds: [],
|
||||||
|
});
|
||||||
|
expect(getCrossIncludedMailboxes(account)).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildCrossFilter', () => {
|
describe('buildCrossFilter', () => {
|
||||||
@@ -86,6 +106,22 @@ describe('getCrossUnreadTotal', () => {
|
|||||||
});
|
});
|
||||||
expect(getCrossUnreadTotal([a, b])).toBe(10);
|
expect(getCrossUnreadTotal([a, b])).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('counts only the selected folders when crossIncludedMailboxIds is set; shared accounts stay unrestricted', () => {
|
||||||
|
// personal account narrowed to inbox only (projects excluded by the picker)
|
||||||
|
const personal = makeAccount({
|
||||||
|
accountId: 'a',
|
||||||
|
mailboxes: [mb('inbox', 'inbox', 3), mb('projects', undefined, 4)],
|
||||||
|
crossIncludedMailboxIds: ['inbox'],
|
||||||
|
});
|
||||||
|
// shared account unrestricted -> role-exclusion default (inbox + custom)
|
||||||
|
const shared = makeAccount({
|
||||||
|
accountId: 'owner',
|
||||||
|
isShared: true,
|
||||||
|
mailboxes: [mb('ns:inbox', 'inbox', 5, 'orig-inbox'), mb('ns:team', undefined, 2, 'orig-team'), mb('ns:junk', 'junk', 9, 'orig-junk')],
|
||||||
|
});
|
||||||
|
expect(getCrossUnreadTotal([personal, shared])).toBe(3 + 5 + 2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resolveSourceFolderName', () => {
|
describe('resolveSourceFolderName', () => {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { mkdtemp, rm, readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { migratePolicyUnifiedMailbox } from '../migrate';
|
||||||
|
|
||||||
|
// migratePolicyUnifiedMailbox reads ADMIN_CONFIG_DIR at call time (see paths.ts),
|
||||||
|
// so each test points it at a fresh temp dir.
|
||||||
|
let dir: string;
|
||||||
|
const policyPath = () => path.join(dir, 'policy.json');
|
||||||
|
const markerPath = () => path.join(dir, '.migrated-unified-mailbox');
|
||||||
|
|
||||||
|
const writePolicy = (features: Record<string, unknown>) =>
|
||||||
|
writeFile(policyPath(), JSON.stringify({ features, restrictions: {} }, null, 2), 'utf-8');
|
||||||
|
const readFeatures = async () =>
|
||||||
|
JSON.parse(await readFile(policyPath(), 'utf-8')).features as Record<string, unknown>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
dir = await mkdtemp(path.join(tmpdir(), 'bw-policy-'));
|
||||||
|
process.env.ADMIN_CONFIG_DIR = dir;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
delete process.env.ADMIN_CONFIG_DIR;
|
||||||
|
await rm(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('migratePolicyUnifiedMailbox', () => {
|
||||||
|
it('enables unifiedCrossAccountEnabled when a cross view was active', async () => {
|
||||||
|
await writePolicy({ crossUnreadViewEnabled: true });
|
||||||
|
await migratePolicyUnifiedMailbox();
|
||||||
|
expect((await readFeatures()).unifiedCrossAccountEnabled).toBe(true);
|
||||||
|
expect(existsSync(markerPath())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not enable it for a standalone All-Mail-only policy', async () => {
|
||||||
|
await writePolicy({ allMailViewEnabled: true, crossUnreadViewEnabled: false, crossStarredViewEnabled: false, crossAllViewEnabled: false });
|
||||||
|
await migratePolicyUnifiedMailbox();
|
||||||
|
expect((await readFeatures()).unifiedCrossAccountEnabled).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is a one-shot: a later admin disable survives a re-run', async () => {
|
||||||
|
await writePolicy({ crossAllViewEnabled: true });
|
||||||
|
await migratePolicyUnifiedMailbox();
|
||||||
|
expect((await readFeatures()).unifiedCrossAccountEnabled).toBe(true);
|
||||||
|
|
||||||
|
// Admin turns it back off; the marker is present, so re-running is a no-op.
|
||||||
|
await writePolicy({ crossAllViewEnabled: true, unifiedCrossAccountEnabled: false });
|
||||||
|
await migratePolicyUnifiedMailbox();
|
||||||
|
expect((await readFeatures()).unifiedCrossAccountEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no policy.json: writes the marker and does not throw', async () => {
|
||||||
|
await migratePolicyUnifiedMailbox();
|
||||||
|
expect(existsSync(markerPath())).toBe(true);
|
||||||
|
expect(existsSync(policyPath())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -33,12 +33,12 @@ class ConfigManager {
|
|||||||
this.adminConfig = await this.readJsonFile('config.json') || {};
|
this.adminConfig = await this.readJsonFile('config.json') || {};
|
||||||
const policy = await this.readJsonFile('policy.json');
|
const policy = await this.readJsonFile('policy.json');
|
||||||
if (policy) {
|
if (policy) {
|
||||||
this.policyCache = {
|
this.policyCache = ConfigManager.normalizePolicy({
|
||||||
...DEFAULT_POLICY,
|
...DEFAULT_POLICY,
|
||||||
...policy,
|
...policy,
|
||||||
features: { ...DEFAULT_FEATURE_GATES, ...(policy.features || {}) },
|
features: { ...DEFAULT_FEATURE_GATES, ...(policy.features || {}) },
|
||||||
themePolicy: { ...DEFAULT_THEME_POLICY, ...(policy.themePolicy || {}) },
|
themePolicy: { ...DEFAULT_THEME_POLICY, ...(policy.themePolicy || {}) },
|
||||||
};
|
});
|
||||||
} else {
|
} else {
|
||||||
this.policyCache = { ...DEFAULT_POLICY };
|
this.policyCache = { ...DEFAULT_POLICY };
|
||||||
}
|
}
|
||||||
@@ -166,15 +166,28 @@ class ConfigManager {
|
|||||||
*/
|
*/
|
||||||
async setPolicy(policy: SettingsPolicy): Promise<void> {
|
async setPolicy(policy: SettingsPolicy): Promise<void> {
|
||||||
assertWritable('update settings policy');
|
assertWritable('update settings policy');
|
||||||
this.policyCache = {
|
this.policyCache = ConfigManager.normalizePolicy({
|
||||||
...DEFAULT_POLICY,
|
...DEFAULT_POLICY,
|
||||||
...policy,
|
...policy,
|
||||||
features: { ...DEFAULT_FEATURE_GATES, ...(policy.features || {}) },
|
features: { ...DEFAULT_FEATURE_GATES, ...(policy.features || {}) },
|
||||||
themePolicy: { ...DEFAULT_THEME_POLICY, ...(policy.themePolicy || {}) },
|
themePolicy: { ...DEFAULT_THEME_POLICY, ...(policy.themePolicy || {}) },
|
||||||
};
|
});
|
||||||
await this.writeJsonFile('policy.json', this.policyCache as unknown as Record<string, unknown>);
|
await this.writeJsonFile('policy.json', this.policyCache as unknown as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates deprecated feature gates forward. The standalone "All Mail" view
|
||||||
|
* (`allMailViewEnabled`) was folded into the unified "All mail" entry, so an
|
||||||
|
* admin who enabled it keeps that entry available via `crossAllViewEnabled`.
|
||||||
|
* Idempotent - safe to run on every load.
|
||||||
|
*/
|
||||||
|
private static normalizePolicy(policy: SettingsPolicy): SettingsPolicy {
|
||||||
|
if (policy.features.allMailViewEnabled) {
|
||||||
|
policy.features.crossAllViewEnabled = true;
|
||||||
|
}
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reload config from disk (for manual file edits or multi-instance).
|
* Reload config from disk (for manual file edits or multi-instance).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import type { AdminConfigData, AdminStateData } from './types';
|
import type { AdminConfigData, AdminStateData } from './types';
|
||||||
|
|
||||||
const MIGRATION_MARKER = '.migrated-v2';
|
const MIGRATION_MARKER = '.migrated-v2';
|
||||||
|
const POLICY_UNIFIED_MARKER = '.migrated-unified-mailbox';
|
||||||
|
|
||||||
interface LegacyAdminData {
|
interface LegacyAdminData {
|
||||||
passwordHash: string;
|
passwordHash: string;
|
||||||
@@ -59,6 +60,65 @@ export async function migrateLegacyAdminLayout(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-shot policy migration for the Unified Mailbox rework. Before it, the
|
||||||
|
* cross views (crossUnread/crossStarred/crossAll) merged across every logged-in
|
||||||
|
* account, so an admin who had any of them enabled was already permitting
|
||||||
|
* cross-account aggregation. The new `unifiedCrossAccountEnabled` gate (default
|
||||||
|
* false) controls that capability, so enable it whenever a cross view was active
|
||||||
|
* - otherwise existing cross-account installs would silently lose the behaviour
|
||||||
|
* on upgrade (the per-user `unifiedCrossAccount` is AND-ed with this gate).
|
||||||
|
*
|
||||||
|
* Persisted + marker-guarded (not a per-load normalization) so a later admin
|
||||||
|
* decision to disable the gate survives restarts. Skipped on read-only config
|
||||||
|
* dirs - operators who locked their config must migrate manually (mirrors
|
||||||
|
* migrateLegacyAdminLayout). The deprecated `allMailViewEnabled` (a single-account
|
||||||
|
* view, never cross-account) deliberately does NOT trigger this.
|
||||||
|
*/
|
||||||
|
export async function migratePolicyUnifiedMailbox(): Promise<void> {
|
||||||
|
if (isConfigReadOnly()) return;
|
||||||
|
|
||||||
|
const markerPath = getConfigPath(POLICY_UNIFIED_MARKER);
|
||||||
|
if (existsSync(markerPath)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const policyPath = getConfigPath('policy.json');
|
||||||
|
if (existsSync(policyPath)) {
|
||||||
|
let parsed: Record<string, unknown> | null = null;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(await readFile(policyPath, 'utf-8')) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
logger.warn('policy.json is not valid JSON; skipping Unified Mailbox policy migration');
|
||||||
|
}
|
||||||
|
const features =
|
||||||
|
parsed && typeof parsed.features === 'object' && parsed.features
|
||||||
|
? (parsed.features as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
if (features) {
|
||||||
|
const hadCrossAccount = !!(
|
||||||
|
features.crossUnreadViewEnabled ||
|
||||||
|
features.crossStarredViewEnabled ||
|
||||||
|
features.crossAllViewEnabled
|
||||||
|
);
|
||||||
|
if (hadCrossAccount && features.unifiedCrossAccountEnabled !== true) {
|
||||||
|
features.unifiedCrossAccountEnabled = true;
|
||||||
|
const tmp = policyPath + '.tmp';
|
||||||
|
await writeFile(tmp, JSON.stringify(parsed, null, 2), 'utf-8');
|
||||||
|
await rename(tmp, policyPath);
|
||||||
|
logger.info('Migrated policy: enabled unifiedCrossAccountEnabled (cross-account views were active)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureConfigDir();
|
||||||
|
await writeFile(markerPath, new Date().toISOString(), 'utf-8');
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Unified Mailbox policy migration failed; will retry on next boot', {
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the existing admin.json carries timestamp fields (legacy mixed layout),
|
* If the existing admin.json carries timestamp fields (legacy mixed layout),
|
||||||
* split them into admin-state.json and rewrite admin.json without them.
|
* split them into admin-state.json and rewrite admin.json without them.
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ export interface FeatureGates {
|
|||||||
hoverActionsConfigEnabled: boolean;
|
hoverActionsConfigEnabled: boolean;
|
||||||
filesEnabled: boolean;
|
filesEnabled: boolean;
|
||||||
contactsEnabled: boolean;
|
contactsEnabled: boolean;
|
||||||
|
/** @deprecated Folded into `crossAllViewEnabled`; normalized forward on policy load. */
|
||||||
allMailViewEnabled: boolean;
|
allMailViewEnabled: boolean;
|
||||||
crossUnreadViewEnabled: boolean;
|
crossUnreadViewEnabled: boolean;
|
||||||
crossStarredViewEnabled: boolean;
|
crossStarredViewEnabled: boolean;
|
||||||
crossAllViewEnabled: boolean;
|
crossAllViewEnabled: boolean;
|
||||||
|
unifiedCrossAccountEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
||||||
@@ -89,6 +91,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
|||||||
crossUnreadViewEnabled: false,
|
crossUnreadViewEnabled: false,
|
||||||
crossStarredViewEnabled: false,
|
crossStarredViewEnabled: false,
|
||||||
crossAllViewEnabled: false,
|
crossAllViewEnabled: false,
|
||||||
|
unifiedCrossAccountEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ThemePolicy {
|
export interface ThemePolicy {
|
||||||
|
|||||||
+6
-13
@@ -892,19 +892,12 @@ export function isUnifiedMailboxId(id: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Virtual mailbox id for the gated "All Mail" view: every folder of a single
|
* Cross views shown in the unified ("Unified Mailbox") section: All mail /
|
||||||
* account merged into one date-sorted list. Distinct from the unified mailbox
|
* Unread / Starred. Each merges messages across the account boundary (the active
|
||||||
* ids above, which merge one role across multiple accounts. Which folders are
|
* account + its shared folders by default, or every logged-in account when the
|
||||||
* included is a per-user setting (see `allMailFolderIds`).
|
* cross-account sub-option is on), narrowed by the user's folder selection (see
|
||||||
*/
|
* `allMailFolderIds`). Distinct from the per-role unified ids (one role across
|
||||||
export const ALL_MAIL_MAILBOX_ID = '__all_mail__';
|
* accounts).
|
||||||
|
|
||||||
/**
|
|
||||||
* Cross-account "All …" views shown in the unified ("All accounts") section.
|
|
||||||
* Each merges messages across EVERY account (including shared/group folders),
|
|
||||||
* spanning all folders except junk/spam, sent, archive, trash and drafts, in
|
|
||||||
* one date-sorted list. Distinct from the per-role unified ids (one role across
|
|
||||||
* accounts) and from ALL_MAIL_MAILBOX_ID (all folders of a single account).
|
|
||||||
*/
|
*/
|
||||||
export const CROSS_UNREAD = '__cross_unread__';
|
export const CROSS_UNREAD = '__cross_unread__';
|
||||||
export const CROSS_STARRED = '__cross_starred__';
|
export const CROSS_STARRED = '__cross_starred__';
|
||||||
|
|||||||
+22
-2
@@ -22,6 +22,18 @@ export interface UnifiedAccountClient {
|
|||||||
// must use the mailbox's `originalId` and explicitly target this accountId
|
// must use the mailbox's `originalId` and explicitly target this accountId
|
||||||
// so the server routes to the owner's data.
|
// so the server routes to the owner's data.
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
|
// Store-side mailbox ids that make up THIS account's contribution to the
|
||||||
|
// cross views (All mail / Unread / Starred). It is intentionally per-account,
|
||||||
|
// not a global list: mailbox ids are account-scoped, so an id from one account
|
||||||
|
// is meaningless in another. The effective folder set of a cross view is the
|
||||||
|
// UNION across every account's entry (one UnifiedAccountClient per account),
|
||||||
|
// i.e. the sum of the respective per-account selections.
|
||||||
|
//
|
||||||
|
// For personal accounts this is the user's folder selection
|
||||||
|
// (`allMailFolderIds[accountId]`); shared/group accounts are not individually
|
||||||
|
// configurable and leave this undefined. When undefined, getCrossIncludedMailboxes
|
||||||
|
// falls back to the role-exclusion default (inbox + custom folders).
|
||||||
|
crossIncludedMailboxIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnifiedFetchResult {
|
export interface UnifiedFetchResult {
|
||||||
@@ -313,10 +325,18 @@ export function fetchUnifiedMailboxCounts(
|
|||||||
// filter is built from each account's included-mailbox ids.
|
// filter is built from each account's included-mailbox ids.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mailboxes of an account included in the cross-account views: everything whose
|
* Mailboxes of an account included in the cross views (All mail / Unread /
|
||||||
* role is not excluded (inbox + custom/no-role folders).
|
* Starred). When the account carries an explicit `crossIncludedMailboxIds`
|
||||||
|
* selection (personal accounts honor the user's folder picker, shared accounts
|
||||||
|
* include everything), only those mailboxes are used. Otherwise it falls back
|
||||||
|
* to the role-exclusion default: everything whose role is not excluded (inbox +
|
||||||
|
* custom/no-role folders).
|
||||||
*/
|
*/
|
||||||
export function getCrossIncludedMailboxes(account: UnifiedAccountClient): Mailbox[] {
|
export function getCrossIncludedMailboxes(account: UnifiedAccountClient): Mailbox[] {
|
||||||
|
if (account.crossIncludedMailboxIds) {
|
||||||
|
const selected = new Set(account.crossIncludedMailboxIds);
|
||||||
|
return account.mailboxes.filter((m) => selected.has(m.id));
|
||||||
|
}
|
||||||
return account.mailboxes.filter((m) => !CROSS_EXCLUDED_ROLES.has(m.role ?? ''));
|
return account.mailboxes.filter((m) => !CROSS_EXCLUDED_ROLES.has(m.role ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Všechny archivy",
|
"unified_archive": "Všechny archivy",
|
||||||
"unified_junk": "Všechen spam",
|
"unified_junk": "Všechen spam",
|
||||||
"all_accounts": "Všechny účty",
|
"all_accounts": "Všechny účty",
|
||||||
|
"unified_mailbox": "Sjednocená schránka",
|
||||||
"expand": "Rozbalit",
|
"expand": "Rozbalit",
|
||||||
"collapse": "Sbalit",
|
"collapse": "Sbalit",
|
||||||
"expand_tooltip": "Rozbalit",
|
"expand_tooltip": "Rozbalit",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Sjednocená schránka",
|
"label": "Sjednocená schránka",
|
||||||
"description": "Zobrazovat sloučené složky (Doručené, Odeslané atd.) ze všech připojených účtů",
|
"description": "Zobrazovat sloučené složky (Doručené, Odeslané atd.) ze všech připojených účtů",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Zahrnout skupinové schránky",
|
"label": "Zahrnout skupinové schránky",
|
||||||
"description": "Zahrnout do sjednoceného zobrazení také sdílené/skupinové schránky."
|
"description": "Zahrnout do sjednoceného zobrazení také sdílené/skupinové schránky."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Alle arkiver",
|
"unified_archive": "Alle arkiver",
|
||||||
"unified_junk": "Alt spam",
|
"unified_junk": "Alt spam",
|
||||||
"all_accounts": "Alle konti",
|
"all_accounts": "Alle konti",
|
||||||
|
"unified_mailbox": "Samlet postkasse",
|
||||||
"expand": "Udvid",
|
"expand": "Udvid",
|
||||||
"collapse": "Skjul",
|
"collapse": "Skjul",
|
||||||
"expand_tooltip": "Udvid",
|
"expand_tooltip": "Udvid",
|
||||||
@@ -927,14 +928,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Samlet postkasse",
|
"label": "Samlet postkasse",
|
||||||
"description": "Vis samlede mapper (Indbakke, Sendt osv.) på tværs af alle tilknyttede konti",
|
"description": "Vis samlede mapper (Indbakke, Sendt osv.) på tværs af alle tilknyttede konti",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Inkluder gruppepostkasser",
|
"label": "Inkluder gruppepostkasser",
|
||||||
"description": "Inkluder også delte/gruppepostkasser i den samlede visning."
|
"description": "Inkluder også delte/gruppepostkasser i den samlede visning."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Alle Archive",
|
"unified_archive": "Alle Archive",
|
||||||
"unified_junk": "Alle Spam",
|
"unified_junk": "Alle Spam",
|
||||||
"all_accounts": "Alle Konten",
|
"all_accounts": "Alle Konten",
|
||||||
|
"unified_mailbox": "Gemeinsames Postfach",
|
||||||
"expand": "Erweitern",
|
"expand": "Erweitern",
|
||||||
"collapse": "Einklappen",
|
"collapse": "Einklappen",
|
||||||
"expand_tooltip": "Erweitern",
|
"expand_tooltip": "Erweitern",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Gemeinsames Postfach",
|
"label": "Gemeinsames Postfach",
|
||||||
"description": "Kombinierte Ordner (Posteingang, Gesendet usw.) für alle verbundenen Konten anzeigen",
|
"description": "Kombinierte Ordner (Posteingang, Gesendet usw.) für alle verbundenen Konten anzeigen",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Über alle Konten",
|
||||||
|
"description": "Den vereinheitlichten Posteingang über alle verbundenen Konten zusammenführen, statt beim aktiven Konto zu bleiben."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Gruppenpostfächer einbeziehen",
|
"label": "Gruppenpostfächer einbeziehen",
|
||||||
"description": "Gemeinsam genutzte/Gruppenpostfächer ebenfalls in die vereinheitlichte Ansicht aufnehmen."
|
"description": "Gemeinsam genutzte/Gruppenpostfächer ebenfalls in die vereinheitlichte Ansicht aufnehmen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Gilt für {account}.",
|
"account_hint": "Gilt für {account}.",
|
||||||
|
|||||||
+14
-11
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "All Archive",
|
"unified_archive": "All Archive",
|
||||||
"unified_junk": "All Junk",
|
"unified_junk": "All Junk",
|
||||||
"all_accounts": "All Accounts",
|
"all_accounts": "All Accounts",
|
||||||
|
"unified_mailbox": "Unified Mailbox",
|
||||||
"expand": "Expand",
|
"expand": "Expand",
|
||||||
"collapse": "Collapse",
|
"collapse": "Collapse",
|
||||||
"expand_tooltip": "Expand",
|
"expand_tooltip": "Expand",
|
||||||
@@ -926,17 +927,19 @@
|
|||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Unified Mailbox",
|
"label": "Unified Mailbox",
|
||||||
"description": "Show combined folders (Inbox, Sent, etc.) across all connected accounts",
|
"description": "Show combined folders (Inbox, Sent, etc.) for the active account and its shared folders.",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Include group inboxes",
|
"label": "Include group inboxes",
|
||||||
"description": "Also merge shared/group inboxes into the unified view."
|
"description": "Also merge shared/group inboxes into the unified view."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
"folders_label": "Folders in the unified lists",
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
"folders_description": "Choose which of this account's folders are merged into the All mail / Unread / Starred lists.",
|
||||||
"folders_label": "Folders in All Mail",
|
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
"no_folders": "No folders available."
|
"no_folders": "No folders available."
|
||||||
},
|
},
|
||||||
@@ -959,16 +962,16 @@
|
|||||||
"back_to_standard": "Back to standard"
|
"back_to_standard": "Back to standard"
|
||||||
},
|
},
|
||||||
"cross_unread": {
|
"cross_unread": {
|
||||||
"label": "All accounts: Unread",
|
"label": "Unread",
|
||||||
"description": "Show an entry in the All accounts section listing unread mail across every account, spanning all folders except junk, sent, archive, trash and drafts."
|
"description": "Show an Unread entry in the Unified Mailbox listing unread mail across the selected folders."
|
||||||
},
|
},
|
||||||
"cross_starred": {
|
"cross_starred": {
|
||||||
"label": "All accounts: Starred",
|
"label": "Starred",
|
||||||
"description": "Show an entry in the All accounts section listing starred mail across every account, spanning all folders except junk, sent, archive, trash and drafts."
|
"description": "Show a Starred entry in the Unified Mailbox listing flagged/starred mail across the selected folders."
|
||||||
},
|
},
|
||||||
"cross_all": {
|
"cross_all": {
|
||||||
"label": "All accounts: All mail",
|
"label": "All mail",
|
||||||
"description": "Show an entry in the All accounts section listing all mail across every account, spanning all folders except junk, sent, archive, trash and drafts."
|
"description": "Show an All mail entry in the Unified Mailbox listing all mail across the selected folders."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keywords": {
|
"keywords": {
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Todos los archivos",
|
"unified_archive": "Todos los archivos",
|
||||||
"unified_junk": "Todo el spam",
|
"unified_junk": "Todo el spam",
|
||||||
"all_accounts": "Todas las cuentas",
|
"all_accounts": "Todas las cuentas",
|
||||||
|
"unified_mailbox": "Buzón unificado",
|
||||||
"expand": "Expandir",
|
"expand": "Expandir",
|
||||||
"collapse": "Contraer",
|
"collapse": "Contraer",
|
||||||
"expand_tooltip": "Expandir",
|
"expand_tooltip": "Expandir",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Buzón unificado",
|
"label": "Buzón unificado",
|
||||||
"description": "Mostrar carpetas combinadas (Entrada, Enviados, etc.) de todas las cuentas conectadas",
|
"description": "Mostrar carpetas combinadas (Entrada, Enviados, etc.) de todas las cuentas conectadas",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Incluir buzones de grupo",
|
"label": "Incluir buzones de grupo",
|
||||||
"description": "Incluir también los buzones compartidos o de grupo en la vista unificada."
|
"description": "Incluir también los buzones compartidos o de grupo en la vista unificada."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "همه بایگانیها",
|
"unified_archive": "همه بایگانیها",
|
||||||
"unified_junk": "همه هرزنامهها",
|
"unified_junk": "همه هرزنامهها",
|
||||||
"all_accounts": "همه حسابها",
|
"all_accounts": "همه حسابها",
|
||||||
|
"unified_mailbox": "صندوق پستی یکپارچه",
|
||||||
"expand": "باز کردن",
|
"expand": "باز کردن",
|
||||||
"collapse": "جمع کردن",
|
"collapse": "جمع کردن",
|
||||||
"expand_tooltip": "باز کردن",
|
"expand_tooltip": "باز کردن",
|
||||||
@@ -927,14 +928,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "صندوق پستی یکپارچه",
|
"label": "صندوق پستی یکپارچه",
|
||||||
"description": "نمایش پوشههای ترکیبی در همه حسابهای متصل",
|
"description": "نمایش پوشههای ترکیبی در همه حسابهای متصل",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "شامل صندوق ورودی گروهها",
|
"label": "شامل صندوق ورودی گروهها",
|
||||||
"description": "صندوق ورودی گروههای اشتراکی را هم ادغام کن"
|
"description": "صندوق ورودی گروههای اشتراکی را هم ادغام کن"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "همه ایمیلها",
|
|
||||||
"description": "نمایش گزینه \"همه ایمیلها\" بالای پوشهها",
|
|
||||||
"folders_label": "پوشهها در همه ایمیلها",
|
"folders_label": "پوشهها در همه ایمیلها",
|
||||||
"folders_description": "انتخاب کنید کدام پوشهها ادغام شوند",
|
"folders_description": "انتخاب کنید کدام پوشهها ادغام شوند",
|
||||||
"no_folders": "پوشهای موجود نیست",
|
"no_folders": "پوشهای موجود نیست",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Toutes les archives",
|
"unified_archive": "Toutes les archives",
|
||||||
"unified_junk": "Tous les indésirables",
|
"unified_junk": "Tous les indésirables",
|
||||||
"all_accounts": "Tous les comptes",
|
"all_accounts": "Tous les comptes",
|
||||||
|
"unified_mailbox": "Boîte aux lettres unifiée",
|
||||||
"expand": "Développer",
|
"expand": "Développer",
|
||||||
"collapse": "Réduire",
|
"collapse": "Réduire",
|
||||||
"expand_tooltip": "Développer",
|
"expand_tooltip": "Développer",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Boîte aux lettres unifiée",
|
"label": "Boîte aux lettres unifiée",
|
||||||
"description": "Afficher les dossiers combinés (Réception, Envoyés, etc.) de tous les comptes connectés",
|
"description": "Afficher les dossiers combinés (Réception, Envoyés, etc.) de tous les comptes connectés",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Inclure les boîtes de groupe",
|
"label": "Inclure les boîtes de groupe",
|
||||||
"description": "Inclure également les boîtes partagées ou de groupe dans la vue unifiée."
|
"description": "Inclure également les boîtes partagées ou de groupe dans la vue unifiée."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Összes archívum",
|
"unified_archive": "Összes archívum",
|
||||||
"unified_junk": "Összes spam",
|
"unified_junk": "Összes spam",
|
||||||
"all_accounts": "Minden fiók",
|
"all_accounts": "Minden fiók",
|
||||||
|
"unified_mailbox": "Egységes postafiók",
|
||||||
"expand": "Kibontás",
|
"expand": "Kibontás",
|
||||||
"collapse": "Összecsukás",
|
"collapse": "Összecsukás",
|
||||||
"expand_tooltip": "Kibontás",
|
"expand_tooltip": "Kibontás",
|
||||||
@@ -927,14 +928,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Egységes postafiók",
|
"label": "Egységes postafiók",
|
||||||
"description": "Összevont mappák (Beérkező, Elküldött, stb.) megjelenítése az összes csatlakoztatott fiók között",
|
"description": "Összevont mappák (Beérkező, Elküldött, stb.) megjelenítése az összes csatlakoztatott fiók között",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Csoportos postafiókok belefoglalása",
|
"label": "Csoportos postafiókok belefoglalása",
|
||||||
"description": "Megosztott/csoportos postafiókok egyesítése az egységes nézetbe."
|
"description": "Megosztott/csoportos postafiókok egyesítése az egységes nézetbe."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Tutti gli archivi",
|
"unified_archive": "Tutti gli archivi",
|
||||||
"unified_junk": "Tutto lo spam",
|
"unified_junk": "Tutto lo spam",
|
||||||
"all_accounts": "Tutti gli account",
|
"all_accounts": "Tutti gli account",
|
||||||
|
"unified_mailbox": "Casella di posta unificata",
|
||||||
"expand": "Espandi",
|
"expand": "Espandi",
|
||||||
"collapse": "Comprimi",
|
"collapse": "Comprimi",
|
||||||
"expand_tooltip": "Espandi",
|
"expand_tooltip": "Espandi",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Casella di posta unificata",
|
"label": "Casella di posta unificata",
|
||||||
"description": "Mostra le cartelle combinate (Posta in arrivo, Inviati, ecc.) di tutti gli account collegati",
|
"description": "Mostra le cartelle combinate (Posta in arrivo, Inviati, ecc.) di tutti gli account collegati",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Includi le caselle di gruppo",
|
"label": "Includi le caselle di gruppo",
|
||||||
"description": "Includi anche le caselle condivise o di gruppo nella vista unificata."
|
"description": "Includi anche le caselle condivise o di gruppo nella vista unificata."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "すべてのアーカイブ",
|
"unified_archive": "すべてのアーカイブ",
|
||||||
"unified_junk": "すべての迷惑メール",
|
"unified_junk": "すべての迷惑メール",
|
||||||
"all_accounts": "すべてのアカウント",
|
"all_accounts": "すべてのアカウント",
|
||||||
|
"unified_mailbox": "統合メールボックス",
|
||||||
"expand": "展開",
|
"expand": "展開",
|
||||||
"collapse": "折りたたむ",
|
"collapse": "折りたたむ",
|
||||||
"expand_tooltip": "展開",
|
"expand_tooltip": "展開",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "統合メールボックス",
|
"label": "統合メールボックス",
|
||||||
"description": "接続されたすべてのアカウントの統合フォルダ(受信トレイ、送信済みなど)を表示",
|
"description": "接続されたすべてのアカウントの統合フォルダ(受信トレイ、送信済みなど)を表示",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "グループ受信トレイを含める",
|
"label": "グループ受信トレイを含める",
|
||||||
"description": "共有/グループ受信トレイも統合ビューに含めます。"
|
"description": "共有/グループ受信トレイも統合ビューに含めます。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "모든 보관함",
|
"unified_archive": "모든 보관함",
|
||||||
"unified_junk": "모든 스팸함",
|
"unified_junk": "모든 스팸함",
|
||||||
"all_accounts": "모든 계정",
|
"all_accounts": "모든 계정",
|
||||||
|
"unified_mailbox": "통합 메일함",
|
||||||
"expand": "펼치기",
|
"expand": "펼치기",
|
||||||
"collapse": "접기",
|
"collapse": "접기",
|
||||||
"expand_tooltip": "펼치기",
|
"expand_tooltip": "펼치기",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "통합 메일함",
|
"label": "통합 메일함",
|
||||||
"description": "연결된 모든 계정의 통합 폴더(받은편지함, 보낸편지함 등)를 표시합니다",
|
"description": "연결된 모든 계정의 통합 폴더(받은편지함, 보낸편지함 등)를 표시합니다",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "그룹 받은편지함 포함",
|
"label": "그룹 받은편지함 포함",
|
||||||
"description": "공유/그룹 받은편지함도 통합 보기에 포함합니다."
|
"description": "공유/그룹 받은편지함도 통합 보기에 포함합니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Visi arhīvi",
|
"unified_archive": "Visi arhīvi",
|
||||||
"unified_junk": "Viss mēstules",
|
"unified_junk": "Viss mēstules",
|
||||||
"all_accounts": "Visi konti",
|
"all_accounts": "Visi konti",
|
||||||
|
"unified_mailbox": "Apvienotā pastkaste",
|
||||||
"expand": "Izvērst",
|
"expand": "Izvērst",
|
||||||
"collapse": "Sairt",
|
"collapse": "Sairt",
|
||||||
"expand_tooltip": "Izvērst",
|
"expand_tooltip": "Izvērst",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Apvienotā pastkaste",
|
"label": "Apvienotā pastkaste",
|
||||||
"description": "Rādīt apvienotās mapes (Iesūtne, Nosūtītie u.c.) no visiem pievienotajiem kontiem",
|
"description": "Rādīt apvienotās mapes (Iesūtne, Nosūtītie u.c.) no visiem pievienotajiem kontiem",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Iekļaut grupas pastkastes",
|
"label": "Iekļaut grupas pastkastes",
|
||||||
"description": "Iekļaut apvienotajā skatā arī koplietotās/grupas pastkastes."
|
"description": "Iekļaut apvienotajā skatā arī koplietotās/grupas pastkastes."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Alle archieven",
|
"unified_archive": "Alle archieven",
|
||||||
"unified_junk": "Alle spam",
|
"unified_junk": "Alle spam",
|
||||||
"all_accounts": "Alle accounts",
|
"all_accounts": "Alle accounts",
|
||||||
|
"unified_mailbox": "Gecombineerd postvak",
|
||||||
"expand": "Uitklappen",
|
"expand": "Uitklappen",
|
||||||
"collapse": "Inklappen",
|
"collapse": "Inklappen",
|
||||||
"expand_tooltip": "Uitklappen",
|
"expand_tooltip": "Uitklappen",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Gecombineerd postvak",
|
"label": "Gecombineerd postvak",
|
||||||
"description": "Gecombineerde mappen (Postvak IN, Verzonden, enz.) van alle verbonden accounts weergeven",
|
"description": "Gecombineerde mappen (Postvak IN, Verzonden, enz.) van alle verbonden accounts weergeven",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Groepspostvakken meenemen",
|
"label": "Groepspostvakken meenemen",
|
||||||
"description": "Gedeelde/groepspostvakken ook in de gecombineerde weergave opnemen."
|
"description": "Gedeelde/groepspostvakken ook in de gecombineerde weergave opnemen."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Wszystkie archiwa",
|
"unified_archive": "Wszystkie archiwa",
|
||||||
"unified_junk": "Wszystkie spam",
|
"unified_junk": "Wszystkie spam",
|
||||||
"all_accounts": "Wszystkie konta",
|
"all_accounts": "Wszystkie konta",
|
||||||
|
"unified_mailbox": "Wspólna skrzynka",
|
||||||
"expand": "Rozwiń",
|
"expand": "Rozwiń",
|
||||||
"collapse": "Zwiń",
|
"collapse": "Zwiń",
|
||||||
"expand_tooltip": "Rozwiń",
|
"expand_tooltip": "Rozwiń",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Wspólna skrzynka",
|
"label": "Wspólna skrzynka",
|
||||||
"description": "Wyświetlaj połączone foldery (Odebrane, Wysłane itp.) ze wszystkich połączonych kont",
|
"description": "Wyświetlaj połączone foldery (Odebrane, Wysłane itp.) ze wszystkich połączonych kont",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Uwzględnij skrzynki grupowe",
|
"label": "Uwzględnij skrzynki grupowe",
|
||||||
"description": "Dodaj również udostępnione/grupowe skrzynki do widoku wspólnego."
|
"description": "Dodaj również udostępnione/grupowe skrzynki do widoku wspólnego."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Todos os arquivos",
|
"unified_archive": "Todos os arquivos",
|
||||||
"unified_junk": "Todo o spam",
|
"unified_junk": "Todo o spam",
|
||||||
"all_accounts": "Todas as contas",
|
"all_accounts": "Todas as contas",
|
||||||
|
"unified_mailbox": "Caixa de correio unificada",
|
||||||
"expand": "Expandir",
|
"expand": "Expandir",
|
||||||
"collapse": "Recolher",
|
"collapse": "Recolher",
|
||||||
"expand_tooltip": "Expandir",
|
"expand_tooltip": "Expandir",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Caixa de correio unificada",
|
"label": "Caixa de correio unificada",
|
||||||
"description": "Mostrar pastas combinadas (Entrada, Enviados, etc.) de todas as contas conectadas",
|
"description": "Mostrar pastas combinadas (Entrada, Enviados, etc.) de todas as contas conectadas",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Incluir caixas de grupo",
|
"label": "Incluir caixas de grupo",
|
||||||
"description": "Incluir também as caixas partilhadas ou de grupo na vista unificada."
|
"description": "Incluir também as caixas partilhadas ou de grupo na vista unificada."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Arhivă completă",
|
"unified_archive": "Arhivă completă",
|
||||||
"unified_junk": "Toate mesajele nedorite",
|
"unified_junk": "Toate mesajele nedorite",
|
||||||
"all_accounts": "Toate conturile",
|
"all_accounts": "Toate conturile",
|
||||||
|
"unified_mailbox": "Căsuță poștală unificată",
|
||||||
"expand": "Extindeți",
|
"expand": "Extindeți",
|
||||||
"collapse": "Reduceți",
|
"collapse": "Reduceți",
|
||||||
"expand_tooltip": "Extindeți",
|
"expand_tooltip": "Extindeți",
|
||||||
@@ -927,14 +928,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Căsuță poștală unificată",
|
"label": "Căsuță poștală unificată",
|
||||||
"description": "Afișați folderele combinate (Mesaje primite, Mesaje trimise etc.) pentru toate conturile conectate",
|
"description": "Afișați folderele combinate (Mesaje primite, Mesaje trimise etc.) pentru toate conturile conectate",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Includeți căsuțele de e-mail de grup",
|
"label": "Includeți căsuțele de e-mail de grup",
|
||||||
"description": "De asemenea, integrați căsuțele de e-mail partajate/de grup în vizualizarea unificată."
|
"description": "De asemenea, integrați căsuțele de e-mail partajate/de grup în vizualizarea unificată."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "Toate mesajele",
|
|
||||||
"description": "Afișați o intrare „Toate mesajele” deasupra folderelor, care reunește mesajele din toate folderele acestui cont într-o singură listă.",
|
|
||||||
"folders_label": "Dosare în „Toate mesajele”",
|
"folders_label": "Dosare în „Toate mesajele”",
|
||||||
"folders_description": "Alegeți ce dosare să fie incluse în vizualizarea „Toate mesajele”.",
|
"folders_description": "Alegeți ce dosare să fie incluse în vizualizarea „Toate mesajele”.",
|
||||||
"account_hint": "Se aplică pentru {account}.",
|
"account_hint": "Se aplică pentru {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Все архивы",
|
"unified_archive": "Все архивы",
|
||||||
"unified_junk": "Весь спам",
|
"unified_junk": "Весь спам",
|
||||||
"all_accounts": "Все аккаунты",
|
"all_accounts": "Все аккаунты",
|
||||||
|
"unified_mailbox": "Общий почтовый ящик",
|
||||||
"expand": "Развернуть",
|
"expand": "Развернуть",
|
||||||
"collapse": "Свернуть",
|
"collapse": "Свернуть",
|
||||||
"expand_tooltip": "Развернуть",
|
"expand_tooltip": "Развернуть",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Общий почтовый ящик",
|
"label": "Общий почтовый ящик",
|
||||||
"description": "Показывать объединённые папки (Входящие, Отправленные и др.) для всех подключённых аккаунтов",
|
"description": "Показывать объединённые папки (Входящие, Отправленные и др.) для всех подключённых аккаунтов",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Включать групповые ящики",
|
"label": "Включать групповые ящики",
|
||||||
"description": "Также объединять общие/групповые ящики в едином представлении."
|
"description": "Также объединять общие/групповые ящики в едином представлении."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Tüm Arşiv",
|
"unified_archive": "Tüm Arşiv",
|
||||||
"unified_junk": "Tüm Önemsiz",
|
"unified_junk": "Tüm Önemsiz",
|
||||||
"all_accounts": "Tüm Hesaplar",
|
"all_accounts": "Tüm Hesaplar",
|
||||||
|
"unified_mailbox": "Birleşik Posta Kutusu",
|
||||||
"expand": "Genişlet",
|
"expand": "Genişlet",
|
||||||
"collapse": "Daralt",
|
"collapse": "Daralt",
|
||||||
"expand_tooltip": "Genişlet",
|
"expand_tooltip": "Genişlet",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Birleşik Posta Kutusu",
|
"label": "Birleşik Posta Kutusu",
|
||||||
"description": "Bağlı tüm hesaplardaki birleşik klasörleri (Gelen Kutusu, Gönderilenler vb.) göster",
|
"description": "Bağlı tüm hesaplardaki birleşik klasörleri (Gelen Kutusu, Gönderilenler vb.) göster",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Grup gelen kutularını dahil et",
|
"label": "Grup gelen kutularını dahil et",
|
||||||
"description": "Paylaşılan/grup gelen kutularını da birleşik görünüme dahil et."
|
"description": "Paylaşılan/grup gelen kutularını da birleşik görünüme dahil et."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "Усі архіви",
|
"unified_archive": "Усі архіви",
|
||||||
"unified_junk": "Весь спам",
|
"unified_junk": "Весь спам",
|
||||||
"all_accounts": "Усі облікові записи",
|
"all_accounts": "Усі облікові записи",
|
||||||
|
"unified_mailbox": "Спільна поштова скринька",
|
||||||
"expand": "Розгорнути",
|
"expand": "Розгорнути",
|
||||||
"collapse": "Згорнути",
|
"collapse": "Згорнути",
|
||||||
"expand_tooltip": "Розгорнути",
|
"expand_tooltip": "Розгорнути",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "Спільна поштова скринька",
|
"label": "Спільна поштова скринька",
|
||||||
"description": "Показувати об'єднані папки (Вхідні, Надіслані тощо) для всіх підключених облікових записів",
|
"description": "Показувати об'єднані папки (Вхідні, Надіслані тощо) для всіх підключених облікових записів",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "Включати групові скриньки",
|
"label": "Включати групові скриньки",
|
||||||
"description": "Також об'єднувати спільні/групові скриньки у спільному перегляді."
|
"description": "Також об'єднувати спільні/групові скриньки у спільному перегляді."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
"unified_archive": "所有归档",
|
"unified_archive": "所有归档",
|
||||||
"unified_junk": "所有垃圾邮件",
|
"unified_junk": "所有垃圾邮件",
|
||||||
"all_accounts": "所有账户",
|
"all_accounts": "所有账户",
|
||||||
|
"unified_mailbox": "统一邮箱",
|
||||||
"expand": "展开",
|
"expand": "展开",
|
||||||
"collapse": "收起",
|
"collapse": "收起",
|
||||||
"expand_tooltip": "展开",
|
"expand_tooltip": "展开",
|
||||||
@@ -924,14 +925,16 @@
|
|||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"label": "统一邮箱",
|
"label": "统一邮箱",
|
||||||
"description": "显示所有已连接账户的合并文件夹(收件箱、已发送等)",
|
"description": "显示所有已连接账户的合并文件夹(收件箱、已发送等)",
|
||||||
|
"cross_account": {
|
||||||
|
"label": "Across all accounts",
|
||||||
|
"description": "Merge the unified mailbox across every connected account instead of staying within the active account."
|
||||||
|
},
|
||||||
"include_group": {
|
"include_group": {
|
||||||
"label": "包含群组收件箱",
|
"label": "包含群组收件箱",
|
||||||
"description": "在统一视图中也合并共享/群组收件箱。"
|
"description": "在统一视图中也合并共享/群组收件箱。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"all_mail": {
|
"all_mail": {
|
||||||
"label": "All Mail",
|
|
||||||
"description": "Show an \"All Mail\" entry above your folders that merges messages from across this account's folders into one list.",
|
|
||||||
"folders_label": "Folders in All Mail",
|
"folders_label": "Folders in All Mail",
|
||||||
"folders_description": "Choose which folders are merged into the All Mail view.",
|
"folders_description": "Choose which folders are merged into the All Mail view.",
|
||||||
"account_hint": "Applies to {account}.",
|
"account_hint": "Applies to {account}.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { useSettingsStore } from '../settings-store';
|
import { useSettingsStore, migrateSettings } from '../settings-store';
|
||||||
|
|
||||||
describe('settings-store per-account allMailFolderIds', () => {
|
describe('settings-store per-account allMailFolderIds', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -53,3 +53,43 @@ describe('settings-store per-account allMailFolderIds', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('migrateSettings v5 -> v6 (Unified Mailbox rework)', () => {
|
||||||
|
it('keeps cross-account users cross-account when any cross view was on, and enables shared', () => {
|
||||||
|
const out = migrateSettings(
|
||||||
|
{ allMailFolderIds: {}, enableCrossUnreadView: true, enableAllMailView: false, includeGroupInUnified: false },
|
||||||
|
5,
|
||||||
|
) as unknown as Record<string, unknown>;
|
||||||
|
expect(out.unifiedCrossAccount).toBe(true);
|
||||||
|
// shared inclusion is enabled for every migrated config, even if it was off
|
||||||
|
expect(out.includeGroupInUnified).toBe(true);
|
||||||
|
expect(out.enableAllMailView).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('folds a standalone All-Mail user into the account-bounded unified "All mail" entry', () => {
|
||||||
|
const out = migrateSettings(
|
||||||
|
{
|
||||||
|
allMailFolderIds: { 'acct-1': ['inbox', 'projects'] },
|
||||||
|
enableAllMailView: true,
|
||||||
|
enableUnifiedMailbox: false,
|
||||||
|
enableCrossUnreadView: false,
|
||||||
|
enableCrossStarredView: false,
|
||||||
|
enableCrossAllView: false,
|
||||||
|
},
|
||||||
|
5,
|
||||||
|
) as unknown as Record<string, unknown>;
|
||||||
|
expect(out.enableUnifiedMailbox).toBe(true);
|
||||||
|
expect(out.enableCrossAllView).toBe(true);
|
||||||
|
expect(out.unifiedCrossAccount).toBe(false); // new account-bounded default
|
||||||
|
expect(out.includeGroupInUnified).toBe(true);
|
||||||
|
// folder selection carries over unchanged -> narrows the unified lists
|
||||||
|
expect(out.allMailFolderIds).toEqual({ 'acct-1': ['inbox', 'projects'] });
|
||||||
|
expect(out.enableAllMailView).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a fresh user gets account-bounded defaults', () => {
|
||||||
|
const out = migrateSettings({ allMailFolderIds: {} }, 5) as unknown as Record<string, unknown>;
|
||||||
|
expect(out.unifiedCrossAccount).toBe(false);
|
||||||
|
expect(out.includeGroupInUnified).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+36
-86
@@ -1,5 +1,5 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { Email, Mailbox, StateChange, ScheduledEmail, SendEmailResult, ALL_MAIL_MAILBOX_ID, isUnifiedMailboxId, isCrossViewId } from "@/lib/jmap/types";
|
import { Email, Mailbox, StateChange, ScheduledEmail, SendEmailResult, isUnifiedMailboxId, isCrossViewId } from "@/lib/jmap/types";
|
||||||
import type { UnifiedMailboxRole, CrossView } from "@/lib/jmap/types";
|
import type { UnifiedMailboxRole, CrossView } from "@/lib/jmap/types";
|
||||||
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
@@ -7,7 +7,7 @@ import { useCalendarStore } from "@/stores/calendar-store";
|
|||||||
import { SearchFilters, DEFAULT_SEARCH_FILTERS, buildJMAPFilter, isFilterEmpty } from "@/lib/jmap/search-utils";
|
import { SearchFilters, DEFAULT_SEARCH_FILTERS, buildJMAPFilter, isFilterEmpty } from "@/lib/jmap/search-utils";
|
||||||
import { emailHooks } from "@/lib/plugin-hooks";
|
import { emailHooks } from "@/lib/plugin-hooks";
|
||||||
import type { ExternalSearchResult } from "@/lib/plugin-types";
|
import type { ExternalSearchResult } from "@/lib/plugin-types";
|
||||||
import { fetchUnifiedEmails, fetchUnifiedMailboxCounts, searchUnifiedEmails, advancedSearchUnifiedEmails, fetchCrossViewEmails, searchCrossViewEmails, getCrossUnreadTotal, resolveSourceFolderName, type UnifiedAccountClient, type UnifiedMailboxCounts } from "@/lib/unified-mailbox";
|
import { fetchUnifiedEmails, fetchUnifiedMailboxCounts, searchUnifiedEmails, advancedSearchUnifiedEmails, fetchCrossViewEmails, searchCrossViewEmails, getCrossUnreadTotal, type UnifiedAccountClient, type UnifiedMailboxCounts } from "@/lib/unified-mailbox";
|
||||||
import { useAuthStore } from "@/stores/auth-store";
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
import { useAccountStore } from "@/stores/account-store";
|
import { useAccountStore } from "@/stores/account-store";
|
||||||
|
|
||||||
@@ -330,34 +330,19 @@ function resolveActionMailboxes(): Mailbox[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the JMAP mailbox ids that make up the gated "All Mail" view for the
|
* Resolves the store-side mailbox ids that make up a personal account's
|
||||||
* active/viewing account. Honors that account's `allMailFolderIds` entry; when
|
* contribution to the unified cross views (All mail / Unread / Starred), honoring
|
||||||
* not configured it defaults to every non-special (no-role) folder. Shared
|
* that account's `allMailFolderIds` folder selection. Returns `undefined` when the
|
||||||
* folders are excluded - All Mail is scoped to a single account. Returns
|
* account has no explicit selection, so the cross views fall back to their
|
||||||
* JMAP-side ids (originalId for namespaced mailboxes).
|
* role-exclusion default (inbox + custom folders). An explicit `[]` selection
|
||||||
|
* yields an empty list (no own folders). `ownMailboxes` must already exclude
|
||||||
|
* shared folders - the picker only ever scopes the user's own folders.
|
||||||
*/
|
*/
|
||||||
function resolveAllMailJmapIds(): string[] {
|
function resolveCrossIncludedMailboxIds(accountId: string, ownMailboxes: Mailbox[]): string[] | undefined {
|
||||||
const mailboxes = resolveActionMailboxes().filter((mb) => !mb.isShared);
|
const configured = useSettingsStore.getState().allMailFolderIds[accountId];
|
||||||
// Per-account selection: read the entry for the account the view is scoped to
|
if (configured === undefined) return undefined;
|
||||||
// (the Pro viewing override, else the global active account). A missing entry
|
const selected = new Set(configured);
|
||||||
// = "not configured" -> all no-role folders; an explicit [] = no folders.
|
return ownMailboxes.filter((mb) => selected.has(mb.id)).map((mb) => mb.id);
|
||||||
const accountId = useEmailStore.getState().viewingAccountId ?? useAuthStore.getState().activeAccountId;
|
|
||||||
const configured = accountId ? useSettingsStore.getState().allMailFolderIds[accountId] : undefined;
|
|
||||||
const selected = configured === undefined
|
|
||||||
? mailboxes.filter((mb) => !mb.role)
|
|
||||||
: mailboxes.filter((mb) => configured.includes(mb.id));
|
|
||||||
return selected.map((mb) => mb.originalId || mb.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the JMAP Email/query filter for the All Mail view from a set of
|
|
||||||
* mailbox ids - an OR of `inMailbox` conditions (or a single condition).
|
|
||||||
*/
|
|
||||||
function buildAllMailFilter(jmapMailboxIds: string[]): Record<string, unknown> {
|
|
||||||
if (jmapMailboxIds.length === 1) {
|
|
||||||
return { inMailbox: jmapMailboxIds[0] };
|
|
||||||
}
|
|
||||||
return { operator: 'OR', conditions: jmapMailboxIds.map((id) => ({ inMailbox: id })) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -418,12 +403,24 @@ function resolveEmailActionContext(
|
|||||||
* owner account reachable through each logged-in client. The shared entries
|
* owner account reachable through each logged-in client. The shared entries
|
||||||
* are flagged with `isShared: true` so `lib/unified-mailbox.ts` routes JMAP
|
* are flagged with `isShared: true` so `lib/unified-mailbox.ts` routes JMAP
|
||||||
* requests via `originalId` + owner accountId.
|
* requests via `originalId` + owner accountId.
|
||||||
|
*
|
||||||
|
* When `scopeToClientAccountId` is set, only the matching logged-in account
|
||||||
|
* (and the shared owners reachable through its client) is built - this keeps
|
||||||
|
* the unified mailbox within a single account boundary. Omitting it spans every
|
||||||
|
* logged-in account (the cross-account sub-option).
|
||||||
|
*
|
||||||
|
* Personal entries carry `crossIncludedMailboxIds` derived from the account's
|
||||||
|
* `allMailFolderIds` folder selection, restricting the All mail / Unread /
|
||||||
|
* Starred cross views to the chosen own folders (shared entries are left
|
||||||
|
* unrestricted so all their folders are included).
|
||||||
*/
|
*/
|
||||||
export async function buildUnifiedAccountClients(
|
export async function buildUnifiedAccountClients(
|
||||||
opts: { includeGroup?: boolean } = {},
|
opts: { includeGroup?: boolean; scopeToClientAccountId?: string } = {},
|
||||||
): Promise<UnifiedAccountClient[]> {
|
): Promise<UnifiedAccountClient[]> {
|
||||||
const { includeGroup = false } = opts;
|
const { includeGroup = false, scopeToClientAccountId } = opts;
|
||||||
const authAccounts = useAccountStore.getState().accounts.filter((a) => a.isConnected);
|
const authAccounts = useAccountStore.getState().accounts.filter(
|
||||||
|
(a) => a.isConnected && (!scopeToClientAccountId || a.id === scopeToClientAccountId),
|
||||||
|
);
|
||||||
const allClients = useAuthStore.getState().getAllConnectedClients();
|
const allClients = useAuthStore.getState().getAllConnectedClients();
|
||||||
const built: UnifiedAccountClient[] = [];
|
const built: UnifiedAccountClient[] = [];
|
||||||
// Per-account mailbox lists gathered here are cached into `accountMailboxes`
|
// Per-account mailbox lists gathered here are cached into `accountMailboxes`
|
||||||
@@ -443,7 +440,7 @@ export async function buildUnifiedAccountClients(
|
|||||||
// no-op (no namespacing) — keeps personal behavior identical while making
|
// no-op (no namespacing) — keeps personal behavior identical while making
|
||||||
// resolution branch-free against shared sources.
|
// resolution branch-free against shared sources.
|
||||||
const primaryJmapId = c.getAccountId();
|
const primaryJmapId = c.getAccountId();
|
||||||
built.push({ accountId: a.id, accountLabel: a.label || a.email, client: c, mailboxes: ownMailboxes, clientAccountId: a.id, jmapAccountId: primaryJmapId, isShared: false });
|
built.push({ accountId: a.id, accountLabel: a.label || a.email, client: c, mailboxes: ownMailboxes, clientAccountId: a.id, jmapAccountId: primaryJmapId, isShared: false, crossIncludedMailboxIds: resolveCrossIncludedMailboxIds(a.id, ownMailboxes) });
|
||||||
fetchedMailboxes[a.id] = ownMailboxes;
|
fetchedMailboxes[a.id] = ownMailboxes;
|
||||||
// Also cache under the JMAP id so `accountMailboxes[email.sourceAccountId]`
|
// Also cache under the JMAP id so `accountMailboxes[email.sourceAccountId]`
|
||||||
// resolves uniformly for personal and shared sources alike.
|
// resolves uniformly for personal and shared sources alike.
|
||||||
@@ -840,7 +837,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
// doesn't exist in the fetched list (e.g. after an account switch)
|
// doesn't exist in the fetched list (e.g. after an account switch)
|
||||||
const currentSelectedMailbox = get().selectedMailbox;
|
const currentSelectedMailbox = get().selectedMailbox;
|
||||||
const selectionValid = currentSelectedMailbox === VIRTUAL_SCHEDULED_MAILBOX_ID
|
const selectionValid = currentSelectedMailbox === VIRTUAL_SCHEDULED_MAILBOX_ID
|
||||||
|| currentSelectedMailbox === ALL_MAIL_MAILBOX_ID
|
|
||||||
// Unified per-role views (All Inbox/Drafts/Junk/…) and cross-account views
|
// Unified per-role views (All Inbox/Drafts/Junk/…) and cross-account views
|
||||||
// (All unread/starred/all) use a virtual id not present in the fetched
|
// (All unread/starred/all) use a virtual id not present in the fetched
|
||||||
// list. A background refresh after a delete must not clobber it and jump
|
// list. A background refresh after a delete must not clobber it and jump
|
||||||
@@ -904,28 +900,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
await get().fetchScheduledEmails(client);
|
await get().fetchScheduledEmails(client);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (targetMailboxId === ALL_MAIL_MAILBOX_ID) {
|
|
||||||
const jmapIds = resolveAllMailJmapIds();
|
|
||||||
if (jmapIds.length === 0) {
|
|
||||||
set({ emails: [], hasMoreEmails: false, totalEmails: 0, isLoading: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
|
||||||
const result = await resolveActionClient(client).advancedSearchEmails(
|
|
||||||
buildAllMailFilter(jmapIds), undefined, emailsPerPage, 0,
|
|
||||||
);
|
|
||||||
const allMailMailboxes = resolveActionMailboxes();
|
|
||||||
for (const email of result.emails) {
|
|
||||||
email.sourceFolder = resolveSourceFolderName(email, allMailMailboxes);
|
|
||||||
}
|
|
||||||
set({
|
|
||||||
emails: annotateScheduledEmails(result.emails, get().scheduledSubmissionByEmailId),
|
|
||||||
hasMoreEmails: result.hasMore,
|
|
||||||
totalEmails: result.total,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const effectiveClient = resolveActionClient(client);
|
const effectiveClient = resolveActionClient(client);
|
||||||
|
|
||||||
// Find the mailbox to get its accountId (for shared folder support)
|
// Find the mailbox to get its accountId (for shared folder support)
|
||||||
@@ -1071,21 +1045,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const { searchFilters } = get();
|
const { searchFilters } = get();
|
||||||
const hasFilters = !isFilterEmpty(searchFilters);
|
const hasFilters = !isFilterEmpty(searchFilters);
|
||||||
|
|
||||||
if (selectedMailbox === ALL_MAIL_MAILBOX_ID) {
|
if (searchQuery || hasFilters) {
|
||||||
if (searchQuery || hasFilters) {
|
|
||||||
// Search within All Mail spans the whole account (no inMailbox).
|
|
||||||
result = hasFilters
|
|
||||||
? await effectiveClient.advancedSearchEmails(buildJMAPFilter(searchQuery, searchFilters, undefined), undefined, emailsPerPage, position)
|
|
||||||
: await effectiveClient.searchEmails(searchQuery, undefined, undefined, emailsPerPage, position);
|
|
||||||
} else {
|
|
||||||
const jmapIds = resolveAllMailJmapIds();
|
|
||||||
if (jmapIds.length === 0) {
|
|
||||||
set({ hasMoreEmails: false, isLoadingMore: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result = await effectiveClient.advancedSearchEmails(buildAllMailFilter(jmapIds), undefined, emailsPerPage, position);
|
|
||||||
}
|
|
||||||
} else if (searchQuery || hasFilters) {
|
|
||||||
const mailboxes = resolveActionMailboxes();
|
const mailboxes = resolveActionMailboxes();
|
||||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||||
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||||
@@ -1111,13 +1071,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
result = await effectiveClient.getEmails(selectedKeyword ? undefined : jmapMailboxId, accountId, emailsPerPage, position, selectedKeyword ? `$label:${selectedKeyword}` : undefined, true);
|
result = await effectiveClient.getEmails(selectedKeyword ? undefined : jmapMailboxId, accountId, emailsPerPage, position, selectedKeyword ? `$label:${selectedKeyword}` : undefined, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMailbox === ALL_MAIL_MAILBOX_ID) {
|
|
||||||
const allMailMailboxes = resolveActionMailboxes();
|
|
||||||
for (const email of result.emails) {
|
|
||||||
email.sourceFolder = resolveSourceFolderName(email, allMailMailboxes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use fresh state when merging to avoid overwriting concurrent updates
|
// Use fresh state when merging to avoid overwriting concurrent updates
|
||||||
// (e.g. refreshCurrentMailbox running during the load)
|
// (e.g. refreshCurrentMailbox running during the load)
|
||||||
const currentEmails = get().emails;
|
const currentEmails = get().emails;
|
||||||
@@ -1789,16 +1742,14 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current mailbox to scope the search. In the All Mail view the
|
// Get the current mailbox to scope the search.
|
||||||
// search spans every folder of the account (no inMailbox constraint).
|
|
||||||
const selectedMailbox = get().selectedMailbox;
|
const selectedMailbox = get().selectedMailbox;
|
||||||
const isAllMail = selectedMailbox === ALL_MAIL_MAILBOX_ID;
|
|
||||||
const mailboxes = resolveActionMailboxes();
|
const mailboxes = resolveActionMailboxes();
|
||||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||||
// Use originalId for shared mailboxes
|
// Use originalId for shared mailboxes
|
||||||
const jmapMailboxId = isAllMail ? undefined : (mailbox?.originalId || selectedMailbox);
|
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||||
// Only pass accountId for shared mailboxes, not for primary account
|
// Only pass accountId for shared mailboxes, not for primary account
|
||||||
const accountId = isAllMail ? undefined : (mailbox?.isShared ? mailbox.accountId : undefined);
|
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||||
|
|
||||||
const result = await resolveActionClient(client).searchEmails(query, jmapMailboxId, accountId, emailsPerPage, 0);
|
const result = await resolveActionClient(client).searchEmails(query, jmapMailboxId, accountId, emailsPerPage, 0);
|
||||||
const externals = await emailHooks.onProvideSearchResults.transform([] as ExternalSearchResult[], { query, filters: get().searchFilters });
|
const externals = await emailHooks.onProvideSearchResults.transform([] as ExternalSearchResult[], { query, filters: get().searchFilters });
|
||||||
@@ -1883,10 +1834,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAllMail = selectedMailbox === ALL_MAIL_MAILBOX_ID;
|
|
||||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||||
const jmapMailboxId = isAllMail ? undefined : (mailbox?.originalId || selectedMailbox);
|
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||||
const accountId = isAllMail ? undefined : (mailbox?.isShared ? mailbox.accountId : undefined);
|
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||||
|
|
||||||
const filter = buildJMAPFilter(searchQuery, searchFilters, jmapMailboxId);
|
const filter = buildJMAPFilter(searchQuery, searchFilters, jmapMailboxId);
|
||||||
const result = await resolveActionClient(client).advancedSearchEmails(filter, accountId, emailsPerPage, 0);
|
const result = await resolveActionClient(client).advancedSearchEmails(filter, accountId, emailsPerPage, 0);
|
||||||
|
|||||||
+72
-39
@@ -240,22 +240,26 @@ interface SettingsState {
|
|||||||
|
|
||||||
// Unified Mailbox
|
// Unified Mailbox
|
||||||
enableUnifiedMailbox: boolean;
|
enableUnifiedMailbox: boolean;
|
||||||
|
// Include shared/delegated folders in the unified mailbox. Default true: the
|
||||||
|
// account-bounded unified view is defined by spanning the account's own folders
|
||||||
|
// plus every shared folder it can access.
|
||||||
includeGroupInUnified: boolean;
|
includeGroupInUnified: boolean;
|
||||||
|
// When true, the unified mailbox merges across every logged-in account
|
||||||
|
// (cross-account). When false (default for new installs) it stays within the
|
||||||
|
// active account boundary (own + shared folders). Gated by the admin
|
||||||
|
// `unifiedCrossAccountEnabled` feature.
|
||||||
|
unifiedCrossAccount: boolean;
|
||||||
|
|
||||||
// All Mail view (gated): user toggle (like the unified mailbox) plus the set
|
// Unified Mailbox entries, each gated per-view by the admin policy. These show
|
||||||
// of folder ids merged into the virtual "All Mail" mailbox. `null` = never
|
// the All mail / Unread / Starred lists, scoped by `unifiedCrossAccount` and
|
||||||
// configured, in which case the view defaults to all non-special (no-role)
|
// narrowed by the `allMailFolderIds` folder selection.
|
||||||
// folders of the active account.
|
|
||||||
enableAllMailView: boolean;
|
|
||||||
|
|
||||||
// Cross-account "All accounts" views (gated per-view by the admin policy)
|
|
||||||
enableCrossUnreadView: boolean;
|
enableCrossUnreadView: boolean;
|
||||||
enableCrossStarredView: boolean;
|
enableCrossStarredView: boolean;
|
||||||
enableCrossAllView: boolean;
|
enableCrossAllView: boolean;
|
||||||
|
|
||||||
// Per-account "All Mail" folder selection, keyed by AccountEntry.id. A
|
// Per-account folder selection narrowing the unified All mail / Unread /
|
||||||
// missing entry = "not configured" -> defaults to every no-role folder; an
|
// Starred lists, keyed by AccountEntry.id. A missing entry = "not configured"
|
||||||
// explicit [] = "no folders". (Replaced the legacy global string[] | null.)
|
// -> defaults to inbox + custom folders; an explicit [] = "no own folders".
|
||||||
allMailFolderIds: Record<string, string[]>;
|
allMailFolderIds: Record<string, string[]>;
|
||||||
|
|
||||||
// Email Display
|
// Email Display
|
||||||
@@ -447,10 +451,9 @@ const DEFAULT_SETTINGS = {
|
|||||||
|
|
||||||
// Unified Mailbox
|
// Unified Mailbox
|
||||||
enableUnifiedMailbox: false,
|
enableUnifiedMailbox: false,
|
||||||
includeGroupInUnified: false,
|
includeGroupInUnified: true,
|
||||||
|
unifiedCrossAccount: false,
|
||||||
|
|
||||||
// All Mail view (gated)
|
|
||||||
enableAllMailView: false,
|
|
||||||
allMailFolderIds: {} as Record<string, string[]>,
|
allMailFolderIds: {} as Record<string, string[]>,
|
||||||
|
|
||||||
enableCrossUnreadView: false,
|
enableCrossUnreadView: false,
|
||||||
@@ -635,7 +638,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
// (see DEVICE_LOCAL_SETTING_KEYS) and must not be synced.
|
// (see DEVICE_LOCAL_SETTING_KEYS) and must not be synced.
|
||||||
enableUnifiedMailbox: state.enableUnifiedMailbox,
|
enableUnifiedMailbox: state.enableUnifiedMailbox,
|
||||||
includeGroupInUnified: state.includeGroupInUnified,
|
includeGroupInUnified: state.includeGroupInUnified,
|
||||||
enableAllMailView: state.enableAllMailView,
|
unifiedCrossAccount: state.unifiedCrossAccount,
|
||||||
allMailFolderIds: state.allMailFolderIds,
|
allMailFolderIds: state.allMailFolderIds,
|
||||||
enableCrossUnreadView: state.enableCrossUnreadView,
|
enableCrossUnreadView: state.enableCrossUnreadView,
|
||||||
enableCrossStarredView: state.enableCrossStarredView,
|
enableCrossStarredView: state.enableCrossStarredView,
|
||||||
@@ -897,9 +900,36 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
version: 5,
|
version: 6,
|
||||||
migrate: (persisted, version) => {
|
migrate: migrateSettings,
|
||||||
const state = persisted as Record<string, unknown>;
|
onRehydrateStorage: () => {
|
||||||
|
return (state) => {
|
||||||
|
if (state) {
|
||||||
|
// Defensive: a legacy global array or any non-record value (e.g.
|
||||||
|
// synced from an older client) is coerced to an empty map so
|
||||||
|
// per-account consumers never see a non-record.
|
||||||
|
if (!isPlainRecord(state.allMailFolderIds)) {
|
||||||
|
state.allMailFolderIds = {};
|
||||||
|
}
|
||||||
|
if (!isPlainRecord(state.preferredIdentityIds)) {
|
||||||
|
state.preferredIdentityIds = {};
|
||||||
|
}
|
||||||
|
applyFontSize(state.fontSize);
|
||||||
|
applyDensity(state.density);
|
||||||
|
applyAnimations(state.animationsEnabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versioned migration for persisted settings. Exported for tests. Mutates and
|
||||||
|
* returns the persisted record so each bump only needs to handle its own delta.
|
||||||
|
*/
|
||||||
|
export function migrateSettings(persisted: unknown, version: number): SettingsState {
|
||||||
|
const state = persisted as Record<string, unknown>;
|
||||||
if (version < 2 && state.listDensity) {
|
if (version < 2 && state.listDensity) {
|
||||||
state.density = state.listDensity;
|
state.density = state.listDensity;
|
||||||
delete state.listDensity;
|
delete state.listDensity;
|
||||||
@@ -925,29 +955,32 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
if (version < 5 || !isPlainRecord(state.allMailFolderIds)) {
|
if (version < 5 || !isPlainRecord(state.allMailFolderIds)) {
|
||||||
state.allMailFolderIds = {};
|
state.allMailFolderIds = {};
|
||||||
}
|
}
|
||||||
return state as unknown as SettingsState;
|
// v6: "All accounts" was reworked into the account-bounded "Unified
|
||||||
},
|
// Mailbox". The standalone __all_mail__ view (`enableAllMailView`) was
|
||||||
onRehydrateStorage: () => {
|
// folded into the unified "All mail" entry (`enableCrossAllView`), and a
|
||||||
return (state) => {
|
// `unifiedCrossAccount` toggle now governs whether the views span every
|
||||||
if (state) {
|
// logged-in account. Existing users keep their current behaviour:
|
||||||
// Defensive: a legacy global array or any non-record value (e.g.
|
// - if any cross view was on, they were already cross-account -> keep it on
|
||||||
// synced from an older client) is coerced to an empty map so
|
// - else if only standalone All Mail was on, enable the account-bounded
|
||||||
// per-account consumers never see a non-record.
|
// unified "All mail" entry (folder selection carries over via allMailFolderIds)
|
||||||
if (!isPlainRecord(state.allMailFolderIds)) {
|
if (version < 6) {
|
||||||
state.allMailFolderIds = {};
|
const hadCross = !!(state.enableCrossUnreadView || state.enableCrossStarredView || state.enableCrossAllView);
|
||||||
}
|
if (hadCross) {
|
||||||
if (!isPlainRecord(state.preferredIdentityIds)) {
|
state.unifiedCrossAccount = true;
|
||||||
state.preferredIdentityIds = {};
|
} else if (state.enableAllMailView) {
|
||||||
}
|
state.enableUnifiedMailbox = true;
|
||||||
applyFontSize(state.fontSize);
|
state.enableCrossAllView = true;
|
||||||
applyDensity(state.density);
|
state.unifiedCrossAccount = false;
|
||||||
applyAnimations(state.animationsEnabled);
|
|
||||||
}
|
}
|
||||||
};
|
delete state.enableAllMailView;
|
||||||
},
|
if (typeof state.unifiedCrossAccount !== 'boolean') state.unifiedCrossAccount = false;
|
||||||
}
|
// The reworked unified mailbox spans the account's own folders plus its
|
||||||
)
|
// shared/group folders, so enable shared inclusion for every migrated
|
||||||
);
|
// configuration (matches the new-install default).
|
||||||
|
state.includeGroupInUnified = true;
|
||||||
|
}
|
||||||
|
return state as unknown as SettingsState;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper functions to apply settings to DOM
|
// Helper functions to apply settings to DOM
|
||||||
function applyFontSize(size: FontSize) {
|
function applyFontSize(size: FontSize) {
|
||||||
|
|||||||
Reference in New Issue
Block a user