import type { NextRequest } from "next/server"; type JmapMethodCall = [string, Record, string]; async function jmapRequest( serverUrl: string, authHeader: string, methodCalls: JmapMethodCall[], using?: string[], ) { const sessionResp = await fetch(`${serverUrl}/.well-known/jmap`, { headers: { Authorization: authHeader }, }); if (!sessionResp.ok) { return { error: `Session fetch failed: ${sessionResp.status}` }; } const session = await sessionResp.json(); const apiUrl = session.apiUrl; if (!apiUrl) { return { error: "No API URL in JMAP session" }; } const body = { using: using || [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:principals", ], methodCalls, }; const resp = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json", Authorization: authHeader, }, body: JSON.stringify(body), }); if (!resp.ok) { return { error: `JMAP request failed: ${resp.status}` }; } return await resp.json(); } export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const action = searchParams.get("action"); const serverUrl = request.headers.get("X-JMAP-Server-Url"); const authHeader = request.headers.get("Authorization"); if (!serverUrl || !authHeader) { return Response.json( { error: "Missing server URL or auth header" }, { status: 400 }, ); } if (action !== "principals") { return Response.json( { error: "Invalid action" }, { status: 400 }, ); } const result = await jmapRequest(serverUrl, authHeader, [ ["Principal/query", { accountId: "" }, "0"], ["Principal/get", { accountId: "", "#ids": { resultOf: "0", name: "Principal/query", path: "/ids", }, }, "1"], ]); if ("error" in result) { return Response.json(result, { status: 502 }); } const getResp = (result as Record).methodResponses as Array<[string, Record, string]> | undefined; const principals = getResp?.find((r) => r[0] === "Principal/get")?.[1] ?.list ?? []; return Response.json({ principals }); } export async function POST(request: NextRequest) { const serverUrl = request.headers.get("X-JMAP-Server-Url"); const authHeader = request.headers.get("Authorization"); if (!serverUrl || !authHeader) { return Response.json( { error: "Missing server URL or auth header" }, { status: 400 }, ); } let body: Record; try { body = await request.json(); } catch { return Response.json({ error: "Invalid JSON body" }, { status: 400 }); } const { kind, resourceId, principalId, role } = body; if (!kind || !resourceId || !principalId) { return Response.json( { error: "Missing required fields: kind, resourceId, principalId" }, { status: 400 }, ); } let method: string; let shareProperty: string; switch (kind) { case "mailbox": method = "Mailbox/set"; shareProperty = "shareWith"; break; case "calendar": method = "Calendar/set"; shareProperty = "shareWith"; break; case "addressBook": method = "AddressBook/set"; shareProperty = "shareWith"; break; case "file": method = "FileNode/set"; shareProperty = "shareWith"; break; default: return Response.json( { error: `Invalid kind: ${kind}` }, { status: 400 }, ); } const patchValue = role === null ? null : buildRights(kind as string, role as string); const methodCalls: JmapMethodCall[] = [ [ method, { accountId: "", update: { [resourceId as string]: { [`${shareProperty}/${principalId}`]: patchValue, }, }, }, "0", ], ]; const result = await jmapRequest( serverUrl, authHeader, methodCalls, ); if ("error" in result) { return Response.json(result, { status: 502 }); } const responses = (result as Record).methodResponses as Array<[string, Record, string]> | undefined; const setResult = responses?.[0]?.[1]; if ( setResult && typeof setResult === "object" && "notUpdated" in setResult && setResult.notUpdated && typeof setResult.notUpdated === "object" && (resourceId as string) in setResult.notUpdated ) { const err = (setResult.notUpdated as Record>)[resourceId as string]; return Response.json( { error: err.description || "Failed to update share" }, { status: 400 }, ); } return Response.json({ ok: true }); } function buildRights( kind: string, role: string, ): Record | null { if (role === null) return null; switch (kind) { case "mailbox": return mailboxRights(role); case "calendar": return calendarRights(role); case "addressBook": return addressBookRights(role); case "file": return fileRights(role); default: return readRights(); } } function mailboxRights(role: string): Record { switch (role) { case "read": return { mayReadItems: true, mayAddItems: false, mayRemoveItems: false, maySetSeen: false, maySetKeywords: false, mayCreateChild: false, mayRename: false, mayDelete: false, maySubmit: false, }; case "readWrite": return { mayReadItems: true, mayAddItems: true, mayRemoveItems: false, maySetSeen: true, maySetKeywords: true, mayCreateChild: false, mayRename: false, mayDelete: false, maySubmit: true, }; case "manager": return { mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, maySetKeywords: true, mayCreateChild: true, mayRename: true, mayDelete: true, maySubmit: true, mayShare: true, }; default: return mailboxRights("read"); } } function calendarRights(role: string): Record { switch (role) { case "read": return { mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: false, mayWriteOwn: false, mayUpdatePrivate: false, mayRSVP: false, mayShare: false, mayDelete: false, }; case "readWrite": return { mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, mayUpdatePrivate: true, mayRSVP: true, mayShare: false, mayDelete: false, }; case "manager": return { mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, mayUpdatePrivate: true, mayRSVP: true, mayShare: true, mayDelete: true, }; default: return calendarRights("read"); } } function addressBookRights(role: string): Record { switch (role) { case "read": return { mayRead: true, mayWrite: false, mayShare: false, mayDelete: false, }; case "readWrite": return { mayRead: true, mayWrite: true, mayShare: false, mayDelete: false, }; case "manager": return { mayRead: true, mayWrite: true, mayShare: true, mayDelete: true, }; default: return addressBookRights("read"); } } function fileRights(role: string): Record { switch (role) { case "read": return { mayRead: true, mayAddChildren: false, mayRename: false, mayDelete: false, mayModifyContent: false, mayShare: false, }; case "readWrite": return { mayRead: true, mayAddChildren: true, mayRename: true, mayDelete: true, mayModifyContent: true, mayShare: false, }; case "manager": return { mayRead: true, mayAddChildren: true, mayRename: true, mayDelete: true, mayModifyContent: true, mayShare: true, }; default: return fileRights("read"); } } function readRights(): Record { return { mayRead: true }; }