diff --git a/components/email/__tests__/email-context-menu-shared-tags.test.tsx b/components/email/__tests__/email-context-menu-shared-tags.test.tsx
new file mode 100644
index 000000000..9f9e340a3
--- /dev/null
+++ b/components/email/__tests__/email-context-menu-shared-tags.test.tsx
@@ -0,0 +1,96 @@
+import { act, fireEvent, render, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Email, Mailbox } from '@/lib/jmap/types';
+
+vi.mock('@/components/plugins/plugin-slot', () => ({
+ PluginSlot: () => null,
+}));
+
+import { EmailContextMenu } from '../email-context-menu';
+import { useAuthStore } from '@/stores/auth-store';
+import { useSettingsStore } from '@/stores/settings-store';
+import { loadSharedTags, resetSharedTags, sharedTagsKey, useSharedTagsStore } from '@/stores/shared-tags-store';
+
+// The context menu's tag list for a selection offers the tags of the
+// selection's account (commonTagScope, like the toolbar), not those of the
+// row that was right-clicked.
+
+const TEAM = { loginId: 'login-1', accountId: 'team' };
+
+// A message in the group account, right-clicked.
+const email = {
+ id: 'e1',
+ threadId: 't1',
+ mailboxIds: { 'team:inbox': true },
+ keywords: { $seen: true },
+ from: [{ email: 'alice@example.com' }],
+ subject: 'Hello',
+ receivedAt: '2026-01-01T00:00:00Z',
+ size: 100,
+ sourceClientAccountId: 'login-1',
+ sourceAccountId: 'team',
+} as unknown as Email;
+
+const mailboxes = [{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } }] as unknown as Mailbox[];
+
+function openTags(batchTagScope: typeof TEAM | null) {
+ render(
+ ,
+ );
+ act(() => { fireEvent.mouseEnter(screen.getByTestId('ctx-tag')); });
+}
+
+beforeEach(async () => {
+ resetSharedTags();
+ // The group's tags come through the real loading path.
+ const client = {
+ getAccountId: () => 'me',
+ getSieveAccountId: () => 'me',
+ supportsSieve: () => false,
+ getSharedAccounts: () => [],
+ getAccountInfo: () => ({ isPersonal: false }),
+ getPrincipalTypes: async () => ({ team: 'group' }),
+ advertisesBulwarkMetadata: () => true,
+ supportsBulwarkMetadata: () => true,
+ getBulwarkMetadata: async () => ({ tags: { tags: { urgent: { label: 'Team urgent', color: 'red' } } } }),
+ discoverKeywords: async () => ({ keywords: {}, scanned: 0, total: 0, complete: true }),
+ };
+ useAuthStore.setState({
+ client: client as never,
+ activeAccountId: 'login-1',
+ getClientForAccount: ((id: string) => (id === 'login-1' ? client : undefined)) as never,
+ });
+ useSettingsStore.setState({ emailKeywords: [{ id: 'mine', label: 'Mine', color: 'blue' }], nestedTags: false });
+ await loadSharedTags(sharedTagsKey(TEAM.loginId, TEAM.accountId), TEAM.accountId, client);
+ expect(useSharedTagsStore.getState().entries[sharedTagsKey(TEAM.loginId, TEAM.accountId)]?.status).toBe('ready');
+});
+afterEach(() => resetSharedTags());
+
+describe('EmailContextMenu tags for a selection', () => {
+ it('offers the group\'s shared tags when the whole selection is in the group', async () => {
+ openTags(TEAM);
+ expect(await screen.findByText('Team urgent')).toBeInTheDocument();
+ expect(screen.queryByText('Mine')).not.toBeInTheDocument();
+ });
+
+ it('offers the own tags when the selection mixes accounts, even from a group row', async () => {
+ openTags(null);
+ expect(await screen.findByText('Mine')).toBeInTheDocument();
+ expect(screen.queryByText('Team urgent')).not.toBeInTheDocument();
+ });
+});
diff --git a/components/email/email-context-menu.tsx b/components/email/email-context-menu.tsx
index 1ec0402d4..161e65e09 100644
--- a/components/email/email-context-menu.tsx
+++ b/components/email/email-context-menu.tsx
@@ -4,7 +4,7 @@ import { useMemo } from "react";
import { useTranslations } from "next-intl";
import { useSettingsStore } from "@/stores/settings-store";
import { useEmailStore, removalFolder, canRemoveFromFolder, emailLinkSource } from "@/stores/email-store";
-import { useEmailTagScope } from "@/hooks/use-account-tags";
+import { useEmailTagScope, type TagScope } from "@/hooks/use-account-tags";
import { Email, Mailbox } from "@/lib/jmap/types";
import {
ContextMenu,
@@ -113,6 +113,11 @@ interface EmailContextMenuProps {
/** Tags every selected message carries, and those only some of them do. */
batchTagIds?: string[];
batchPartialTagIds?: string[];
+ /**
+ * The account of the whole selection (commonTagScope): its tags are
+ * offered. Null when the selection mixes accounts: the user's own tags.
+ */
+ batchTagScope?: TagScope | null;
/** Takes the tag off when the whole selection has it, else puts it on all. */
onBatchToggleTag?: (tagId: string) => void;
onBatchDelete?: () => void;
@@ -180,6 +185,7 @@ export function EmailContextMenu({
onBatchMarkAsRead,
batchTagIds,
batchPartialTagIds,
+ batchTagScope = null,
onBatchToggleTag,
onBatchDelete,
onBatchArchive,
@@ -207,7 +213,7 @@ export function EmailContextMenu({
const isPinned = email.keywords?.['$pinned'] === true;
const isDraft = email.keywords?.['$draft'] === true;
const currentTagIds = getEmailTagIds(email.keywords);
- // The row's account names the tags, for a selection too: it is part of it.
+ // The row's account names its tags; a selection uses batchTagScope.
const tagScope = useEmailTagScope(email);
const showBatchActions = isMultiSelect && selectedCount > 1;
const isInJunkFolder = currentMailboxRole === 'junk';
@@ -500,7 +506,7 @@ export function EmailContextMenu({
selectedIds={batchTagIds ?? []}
partialIds={batchPartialTagIds}
onToggle={(tagId) => onBatchToggleTag?.(tagId)}
- scope={tagScope}
+ scope={batchTagScope}
/>
) : (
markSelectionRead(read)}
batchTagIds={selectionTags.all}
batchPartialTagIds={selectionTags.some}
+ batchTagScope={selectionTags.scope}
onBatchToggleTag={handleBatchToggleTag}
onBatchDelete={async () => {
if (!client) return;