import type { NextRequest } from "next/server"; import { resolveRights, type SharedResourceKind } from "@/lib/sharing-rights"; 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 : resolveRights(kind as SharedResourceKind, 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 }); }