diff --git a/src/api/__tests__/live-stalwart-delegation.test.ts b/src/api/__tests__/live-stalwart-delegation.test.ts index f2a3dcfd..ed20f910 100644 --- a/src/api/__tests__/live-stalwart-delegation.test.ts +++ b/src/api/__tests__/live-stalwart-delegation.test.ts @@ -22,6 +22,10 @@ import { JMAPClient } from '../jmap-client'; import { CAPABILITIES } from '../types'; import { applyShareChanges, getShareTargets, supportsDelegation, supportsMailboxSharing } from '../delegation'; import { buildGrantChanges, buildRevokeChanges, summarizeDelegates } from '../../lib/delegation/delegation'; +import { principalRowDisplay, resolvePrincipal } from '../../lib/delegation/display'; +import { listPrincipals } from '../files'; + +const t = (_key: string, fallback?: string) => fallback ?? ''; const SERVER = process.env.BULWARK_LIVE_JMAP; const USER = process.env.BULWARK_LIVE_USER ?? 'usera@example.org'; @@ -120,4 +124,32 @@ live('live Stalwart: delegates', () => { expect(gone.calendars.filter((id) => changes.some((c) => c.id === id))).toEqual([]); expect(gone.addressBooks.filter((id) => changes.some((c) => c.id === id))).toEqual([]); }, 120_000); + + // Stalwart 1.0 with directory queries off (VM 106's 1.0.1) names nobody + // the owner merely shares with: Principal/get answers notFound for the + // delegate's id, a letter or two. The row reads "Unknown person" then, not + // "Unknown person / e"; 0.16 names them. + it('names who has access where the server lets it, and shows no id where it does not', async () => { + const { targets } = await getShareTargets(); + const principalId = delegate.accountId; + const results = await applyShareChanges(buildGrantChanges(targets, principalId, { mail: 'none', calendars: 'none', addressBooks: 'default' }, 'read')); + expect(results.filter((r) => !r.ok)).toEqual([]); + try { + const listing = await listPrincipals(); + const sessionAccounts = new Map(Object.entries(owner.currentSession?.accounts ?? {}) + .filter(([id]) => id !== owner.accountId) + .map(([id, info]) => [id, info.name])); + const principal = resolvePrincipal(principalId, { directory: listing.principals, remembered: new Map(), sessionAccounts }); + const row = principalRowDisplay(principal, t); + if (listing.directoryQueriesOff) { + expect(principal).toBeNull(); + expect(row).toEqual({ name: 'Unknown person', detail: null }); + } else { + expect(principal?.email?.toLowerCase()).toBe(USER2!.toLowerCase()); + expect(row.name).not.toBe('Unknown person'); + } + } finally { + expect(await revokeAll()).toBe(true); + } + }, 120_000); }); diff --git a/src/components/sharing/ShareCollectionSheet.tsx b/src/components/sharing/ShareCollectionSheet.tsx index 59e82ff9..48d2c9bd 100644 --- a/src/components/sharing/ShareCollectionSheet.tsx +++ b/src/components/sharing/ShareCollectionSheet.tsx @@ -164,8 +164,10 @@ export function ShareCollectionSheet({ {p?.description || p?.name || (unresolved ? t('sharing.unknown_user', 'Unknown user') : principalId)} - {p?.email || unresolved ? ( - {p?.email || principalId} + {/* Never the id in its place: Stalwart 1.0's are a letter or two + ("e") and read as a broken address. */} + {p?.email ? ( + {p.email} ) : null} ); diff --git a/src/lib/delegation/__tests__/unknown-principal.test.ts b/src/lib/delegation/__tests__/unknown-principal.test.ts new file mode 100644 index 00000000..4159ce37 --- /dev/null +++ b/src/lib/delegation/__tests__/unknown-principal.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { translate } from '../../../i18n'; +import { principalRowDisplay, resolvePrincipal } from '../display'; + +const tEn = (key: string, fallback?: string, params?: Record) => translate('en', key, fallback, params); + +describe('someone with access the server does not name', () => { + // Stalwart 1.0 with directory queries off: Principal/get answers notFound + // for the sharee's id ("e") and the session does not list them. + it('reads as "Unknown person" without the id as an address', () => { + const principal = resolvePrincipal('e', { directory: [], remembered: new Map(), sessionAccounts: new Map() }); + expect(principal).toBeNull(); + expect(principalRowDisplay(principal, tEn)).toEqual({ name: 'Unknown person', detail: null }); + }); + + it('reads as the person when the directory names them (0.16)', () => { + const userB = { id: 'e', type: 'individual' as const, name: 'userb@example.org', email: 'userb@example.org', description: 'User B' }; + const principal = resolvePrincipal('e', { directory: [userB], remembered: new Map(), sessionAccounts: new Map() }); + expect(principalRowDisplay(principal, tEn)).toEqual({ name: 'userb@example.org', detail: 'User B' }); + }); + + it('gets no id for an address line in the calendar and address book share sheet', () => { + const sheet = fs.readFileSync(path.resolve(__dirname, '../../../components/sharing/ShareCollectionSheet.tsx'), 'utf8'); + const label = sheet.slice(sheet.indexOf('const renderPrincipalLabel'), sheet.indexOf('return (', sheet.indexOf('const renderPrincipalLabel')) + 900); + expect(label).not.toMatch(/p\?\.email \|\| principalId/); + expect(label).toMatch(/\{p\?\.email \? \(/); + }); +}); diff --git a/src/lib/delegation/display.ts b/src/lib/delegation/display.ts index 540063d3..c3de31be 100644 --- a/src/lib/delegation/display.ts +++ b/src/lib/delegation/display.ts @@ -17,6 +17,17 @@ export function principalDisplay(principal: Pick