From 7bf62e4bdcd5d4402b21c09e07f6c5316906ae3b Mon Sep 17 00:00:00 2001 From: Paulhenry Saux Date: Wed, 22 Jul 2026 19:34:45 +0200 Subject: [PATCH 1/2] feat: add contact methods in plugin API --- lib/plugin-sandbox/host-api.ts | 45 ++++++++++++++++++++++++++++++++++ lib/plugin-sandbox/protocol.ts | 1 + lib/plugin-sandbox/runtime.tsx | 7 ++++++ 3 files changed, 53 insertions(+) diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index 5ffd8acf..b1f05cd4 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -14,6 +14,7 @@ import { apiFetch } from '../browser-navigation'; import { awaitDialog, awaitPrompt, type PromptField } from './host-dialog'; import { fileStorage } from '../plugin-storage'; import { generateUUID } from '../utils'; +import { ContactCard } from '../jmap/types'; /** * Methods only callable from the privileged (same-origin) tier. These expose @@ -56,6 +57,11 @@ const PERM_PER_METHOD: Record = { 'upfiles.get' : 'email:blob-write', 'upfiles.save' : 'email:blob-write', 'webauthn.getOrCreate': 'crypto:full', + // contact + 'contact.get': 'contacts:read', + 'contact.update': 'contacts:write', + 'contact.create': 'contacts:write', + 'contact.search': 'contacts:read', // admin 'admin.getConfig': 'admin:config', 'admin.getAllConfig': 'admin:config', @@ -388,6 +394,40 @@ async function doJmapImportRaw( ); } +async function doContactSearch(query: string): Promise { + const { client } = useAuthStore.getState(); + if (!client) { + throw new Error('jmap.importRaw: no active session'); + } + return await client.searchContacts(query); +} + +async function doContactGet(contactId: string): Promise { + const { client } = useAuthStore.getState(); + if (!client) { + throw new Error('jmap.importRaw: no active session'); + } + return await client.getContact(contactId); +} + +async function doContactUpdate(id: string, contact: Partial): Promise { + const { client } = useAuthStore.getState(); + if (!client) { + throw new Error('jmap.importRaw: no active session'); + } + + await client.updateContact(id, contact); +} + +async function doContactCreate(contact: ContactCard): Promise { + const { client } = useAuthStore.getState(); + if (!client) { + throw new Error('jmap.importRaw: no active session'); + } + + return await client.createContact(contact); +} + // ─── WebAuthn (privileged tier) ───────────────────────────────────────────── // This salt acts as a constant context identifier for key derivation. @@ -699,6 +739,11 @@ export async function dispatchApiCall( case 'upfiles.get' : return getFile(args[0] as string); case 'upfiles.save' : return saveFile(args[0] as string, args[1] as File); case 'webauthn.getOrCreate': return doGetOrCreatePRF(args[0] as number[] | undefined, args[1] as string | undefined, args[2] as string | undefined); + + case 'contact.get': return doContactGet(args[0] as string); + case 'contact.update': return doContactUpdate(args[0] as string, args[1] as Partial); + case 'contact.create': return doContactCreate(args[0] as ContactCard); + case 'contact.search': return doContactSearch(args[0] as string); case 'admin.getConfig': return adminGet(plugin.id, args[0] as string); case 'admin.getAllConfig': return adminGetAll(plugin.id); diff --git a/lib/plugin-sandbox/protocol.ts b/lib/plugin-sandbox/protocol.ts index 7de67a53..2f34003f 100644 --- a/lib/plugin-sandbox/protocol.ts +++ b/lib/plugin-sandbox/protocol.ts @@ -244,6 +244,7 @@ export const API_METHODS = [ 'webauthn.getOrCreate', 'jmap.fetchBlob', 'jmap.sendRaw', 'upfiles.get', 'upfiles.save', + 'contact.get', 'contact.update', 'contact.create', 'contact.search', 'admin.getConfig', 'admin.getAllConfig', 'admin.setConfig', 'admin.deleteConfig', 'toast.success', 'toast.error', 'toast.info', 'toast.warning', 'ui.confirm', 'ui.alert', 'ui.prompt', 'ui.rerenderEmail', 'ui.rerenderFetchedEmails', 'ui.openExternalUrl', 'ui.downloadFile', diff --git a/lib/plugin-sandbox/runtime.tsx b/lib/plugin-sandbox/runtime.tsx index 13decccb..5721079a 100644 --- a/lib/plugin-sandbox/runtime.tsx +++ b/lib/plugin-sandbox/runtime.tsx @@ -29,6 +29,7 @@ import type { } from './protocol'; import { themeSnapshotToCSS, type ThemeSnapshot } from './host-theme'; import type { SlotName } from '../plugin-types'; +import { ContactCard } from '../jmap/types'; // ─── Module-scope state ────────────────────────────────────── @@ -204,6 +205,12 @@ function buildPluginApi(manifest: PluginManifest) { opts?: { keywords?: Record; accountId?: string }, ) => callApi('jmap.importRaw', [rawBytes, mailboxRoles, opts]), }, + contacts: { + get: (contactId: string) => callApi('contact.get', [contactId]) as Promise, + update: (contactId: string, updates: Partial) => callApi('contact.update', [contactId, updates]), + create: (contact: ContactCard) => callApi('contact.create', [contact]) as Promise, + search: (query: string) => callApi('contact.search', [query]) as Promise, + }, /** * Used to alterate files before they are uploaded to server. * Edited files are saved on indexedDB and remove once the upload to server begins. From 2245ad2024776de5829b27cf2ff1bbfaadce4b29 Mon Sep 17 00:00:00 2001 From: Paulhenry Saux Date: Wed, 22 Jul 2026 20:28:32 +0200 Subject: [PATCH 2/2] fix(plugins): use correct method name for message errors in host-api.ts --- lib/plugin-sandbox/host-api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index b1f05cd4..56e268cc 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -397,7 +397,7 @@ async function doJmapImportRaw( async function doContactSearch(query: string): Promise { const { client } = useAuthStore.getState(); if (!client) { - throw new Error('jmap.importRaw: no active session'); + throw new Error('contact.search: no active session'); } return await client.searchContacts(query); } @@ -405,7 +405,7 @@ async function doContactSearch(query: string): Promise { async function doContactGet(contactId: string): Promise { const { client } = useAuthStore.getState(); if (!client) { - throw new Error('jmap.importRaw: no active session'); + throw new Error('contact.get: no active session'); } return await client.getContact(contactId); } @@ -413,7 +413,7 @@ async function doContactGet(contactId: string): Promise { async function doContactUpdate(id: string, contact: Partial): Promise { const { client } = useAuthStore.getState(); if (!client) { - throw new Error('jmap.importRaw: no active session'); + throw new Error('contact.update: no active session'); } await client.updateContact(id, contact); @@ -422,7 +422,7 @@ async function doContactUpdate(id: string, contact: Partial): Promi async function doContactCreate(contact: ContactCard): Promise { const { client } = useAuthStore.getState(); if (!client) { - throw new Error('jmap.importRaw: no active session'); + throw new Error('contact.create: no active session'); } return await client.createContact(contact);