From 959d4bd6ce58291ca8a55dc966d594b5c9774eda Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:18:59 +0200 Subject: [PATCH] fix: assign uid to contact cards on creation #644 --- app/(main)/[locale]/contacts/page.tsx | 4 +-- lib/__tests__/jmap-contact-client.test.ts | 42 +++++++++++++++++++++++ lib/jmap/client.ts | 2 ++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/(main)/[locale]/contacts/page.tsx b/app/(main)/[locale]/contacts/page.tsx index b84df961..f27afd39 100644 --- a/app/(main)/[locale]/contacts/page.tsx +++ b/app/(main)/[locale]/contacts/page.tsx @@ -400,8 +400,8 @@ export default function ContactsPage() { }, [clearSelection, toggleContactSelection, groups.length]); const handleDuplicateContact = useCallback(async (source: ContactCard) => { - const { id: _id, created: _created, updated: _updated, ...rest } = source; - void _id; void _created; void _updated; + const { id: _id, uid: _uid, created: _created, updated: _updated, ...rest } = source; + void _id; void _uid; void _created; void _updated; const data: Partial = JSON.parse(JSON.stringify(rest)); if (supportsSync && client) { await createContact(client, data); diff --git a/lib/__tests__/jmap-contact-client.test.ts b/lib/__tests__/jmap-contact-client.test.ts index ec1aea62..c0be5fd5 100644 --- a/lib/__tests__/jmap-contact-client.test.ts +++ b/lib/__tests__/jmap-contact-client.test.ts @@ -301,6 +301,48 @@ describe('JMAPClient contact methods', () => { expect(fetchSpy).toHaveBeenCalledTimes(2); }); + it('should generate a urn:uuid uid when none is provided (#644)', async () => { + const client = createClient(); + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + + mockFetchOnce(fetchSpy, { + methodResponses: [['ContactCard/set', { created: { 'new-contact': { id: 'new-id' } } }, '0']], + }); + mockFetchOnce(fetchSpy, { + methodResponses: [['ContactCard/get', { list: [{ ...mockContact, id: 'new-id' }] }, '0']], + }); + + await client.createContact({ + emails: { email: { address: 'trusted@example.com' } }, + addressBookIds: { 'ab-1': true }, + }); + + const setBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const created = setBody.methodCalls[0][1].create['new-contact']; + expect(created.uid).toMatch(/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + }); + + it('should preserve a caller-provided uid', async () => { + const client = createClient(); + const fetchSpy = vi.spyOn(globalThis, 'fetch'); + + mockFetchOnce(fetchSpy, { + methodResponses: [['ContactCard/set', { created: { 'new-contact': { id: 'new-id' } } }, '0']], + }); + mockFetchOnce(fetchSpy, { + methodResponses: [['ContactCard/get', { list: [{ ...mockContact, id: 'new-id' }] }, '0']], + }); + + await client.createContact({ + uid: 'urn:uuid:12345678-1234-1234-1234-123456789abc', + addressBookIds: { 'ab-1': true }, + }); + + const setBody = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string); + const created = setBody.methodCalls[0][1].create['new-contact']; + expect(created.uid).toBe('urn:uuid:12345678-1234-1234-1234-123456789abc'); + }); + it('should throw on notCreated error with description', async () => { const client = createClient(); const fetchSpy = vi.spyOn(globalThis, 'fetch'); diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 9c83b076..770d28bd 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -4398,6 +4398,8 @@ export class JMAPClient implements IJMAPClient { create: { "new-contact": { ...contactData, + // Stalwart stores the card without one if omitted (#644) + uid: contactData.uid || `urn:uuid:${crypto.randomUUID()}`, addressBookIds, } }