···11+# JMAP RFC 8620 Core Protocol - Message Types Analysis
22+33+## Overview
44+This document provides a comprehensive analysis of all JMAP request and response message types defined in RFC 8620 (The JSON Meta Application Protocol). This analysis is intended to support OCaml type design for JMAP parsing.
55+66+---
77+88+## 1. CORE PROTOCOL STRUCTURES
99+1010+### 1.1 Invocation Data Type
1111+An Invocation is a tuple represented as a JSON array with three elements:
1212+1313+```json
1414+[
1515+ "methodName", // String: name of method to call or response
1616+ { // Object: named arguments
1717+ "arg1": "value1",
1818+ "arg2": "value2"
1919+ },
2020+ "callId" // String: method call id (echoed in response)
2121+]
2222+```
2323+2424+**Structure:**
2525+- Position 0: `String` - Method name
2626+- Position 1: `Object (String[*])` - Named arguments
2727+- Position 2: `String` - Method call ID
2828+2929+---
3030+3131+### 1.2 Request Object
3232+3333+```json
3434+{
3535+ "using": ["urn:ietf:params:jmap:core", "..."], // String[] - REQUIRED
3636+ "methodCalls": [ // Invocation[] - REQUIRED
3737+ ["method1", {"arg1": "value"}, "c1"],
3838+ ["method2", {"arg2": "value"}, "c2"]
3939+ ],
4040+ "createdIds": { // Id[Id] - OPTIONAL
4141+ "temp-id-1": "server-id-1"
4242+ }
4343+}
4444+```
4545+4646+**Fields:**
4747+- `using`: `String[]` - **REQUIRED** - Capabilities the client wishes to use
4848+- `methodCalls`: `Invocation[]` - **REQUIRED** - Array of method calls to process sequentially
4949+- `createdIds`: `Id[Id]` - **OPTIONAL** - Map of creation id to server-assigned id
5050+5151+---
5252+5353+### 1.3 Response Object
5454+5555+```json
5656+{
5757+ "methodResponses": [ // Invocation[] - REQUIRED
5858+ ["method1", {"result": "value"}, "c1"],
5959+ ["error", {"type": "serverFail"}, "c2"]
6060+ ],
6161+ "createdIds": { // Id[Id] - OPTIONAL
6262+ "temp-id-1": "server-id-1"
6363+ },
6464+ "sessionState": "75128aab4b1b" // String - REQUIRED
6565+}
6666+```
6767+6868+**Fields:**
6969+- `methodResponses`: `Invocation[]` - **REQUIRED** - Array of responses in order
7070+- `createdIds`: `Id[Id]` - **OPTIONAL** - Only returned if given in request
7171+- `sessionState`: `String` - **REQUIRED** - Current Session state
7272+7373+---
7474+7575+### 1.4 ResultReference Object
7676+7777+Used to reference results from previous method calls in the same request:
7878+7979+```json
8080+{
8181+ "resultOf": "c1", // String - REQUIRED
8282+ "name": "Foo/get", // String - REQUIRED
8383+ "path": "/list/0/id" // String - REQUIRED (JSON Pointer)
8484+}
8585+```
8686+8787+**Fields:**
8888+- `resultOf`: `String` - **REQUIRED** - Method call id of previous call
8989+- `name`: `String` - **REQUIRED** - Response name to look for
9090+- `path`: `String` - **REQUIRED** - JSON Pointer into response arguments
9191+9292+**Usage:** Prefix argument name with `#` and use ResultReference as value
9393+9494+---
9595+9696+## 2. SESSION OBJECT
9797+9898+The Session object is returned from a GET request to the JMAP Session resource:
9999+100100+```json
101101+{
102102+ "capabilities": { // String[Object] - REQUIRED
103103+ "urn:ietf:params:jmap:core": {
104104+ "maxSizeUpload": 50000000, // UnsignedInt
105105+ "maxConcurrentUpload": 4, // UnsignedInt
106106+ "maxSizeRequest": 10000000, // UnsignedInt
107107+ "maxConcurrentRequests": 4, // UnsignedInt
108108+ "maxCallsInRequest": 16, // UnsignedInt
109109+ "maxObjectsInGet": 500, // UnsignedInt
110110+ "maxObjectsInSet": 500, // UnsignedInt
111111+ "collationAlgorithms": ["i;unicode-casemap"] // String[]
112112+ }
113113+ },
114114+ "accounts": { // Id[Account] - REQUIRED
115115+ "account-id": {
116116+ "name": "user@example.com", // String
117117+ "isPersonal": true, // Boolean
118118+ "isReadOnly": false, // Boolean
119119+ "accountCapabilities": { // String[Object]
120120+ "urn:ietf:params:jmap:mail": {}
121121+ }
122122+ }
123123+ },
124124+ "primaryAccounts": { // String[Id] - REQUIRED
125125+ "urn:ietf:params:jmap:mail": "account-id"
126126+ },
127127+ "username": "user@example.com", // String - REQUIRED
128128+ "apiUrl": "https://jmap.example.com/api/", // String - REQUIRED
129129+ "downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}?type={type}", // String - REQUIRED
130130+ "uploadUrl": "https://jmap.example.com/upload/{accountId}/", // String - REQUIRED
131131+ "eventSourceUrl": "https://jmap.example.com/eventsource/?types={types}&closeafter={closeafter}&ping={ping}", // String - REQUIRED
132132+ "state": "cyrus-0" // String - REQUIRED
133133+}
134134+```
135135+136136+### 2.1 Account Object
137137+138138+```json
139139+{
140140+ "name": "user@example.com", // String - REQUIRED
141141+ "isPersonal": true, // Boolean - REQUIRED
142142+ "isReadOnly": false, // Boolean - REQUIRED
143143+ "accountCapabilities": { // String[Object] - REQUIRED
144144+ "urn:ietf:params:jmap:mail": {},
145145+ "urn:ietf:params:jmap:contacts": {}
146146+ }
147147+}
148148+```
149149+150150+---
151151+152152+## 3. STANDARD METHOD TYPES
153153+154154+### 3.1 Foo/get
155155+156156+**Request:**
157157+```json
158158+{
159159+ "accountId": "account-id", // Id - REQUIRED
160160+ "ids": ["id1", "id2"], // Id[]|null - REQUIRED
161161+ "properties": ["id", "name", "prop1"] // String[]|null - OPTIONAL
162162+}
163163+```
164164+165165+**Response:**
166166+```json
167167+{
168168+ "accountId": "account-id", // Id - REQUIRED
169169+ "state": "state-string", // String - REQUIRED
170170+ "list": [ // Foo[] - REQUIRED
171171+ {"id": "id1", "name": "Object 1"},
172172+ {"id": "id2", "name": "Object 2"}
173173+ ],
174174+ "notFound": ["id3"] // Id[] - REQUIRED
175175+}
176176+```
177177+178178+**Errors:**
179179+- `requestTooLarge` - Too many ids requested
180180+181181+---
182182+183183+### 3.2 Foo/changes
184184+185185+**Request:**
186186+```json
187187+{
188188+ "accountId": "account-id", // Id - REQUIRED
189189+ "sinceState": "old-state", // String - REQUIRED
190190+ "maxChanges": 100 // UnsignedInt|null - OPTIONAL
191191+}
192192+```
193193+194194+**Response:**
195195+```json
196196+{
197197+ "accountId": "account-id", // Id - REQUIRED
198198+ "oldState": "old-state", // String - REQUIRED
199199+ "newState": "new-state", // String - REQUIRED
200200+ "hasMoreChanges": false, // Boolean - REQUIRED
201201+ "created": ["id1", "id2"], // Id[] - REQUIRED
202202+ "updated": ["id3", "id4"], // Id[] - REQUIRED
203203+ "destroyed": ["id5"] // Id[] - REQUIRED
204204+}
205205+```
206206+207207+**Errors:**
208208+- `cannotCalculateChanges` - Server cannot calculate changes from state
209209+210210+---
211211+212212+### 3.3 Foo/set
213213+214214+**Request:**
215215+```json
216216+{
217217+ "accountId": "account-id", // Id - REQUIRED
218218+ "ifInState": "expected-state", // String|null - OPTIONAL
219219+ "create": { // Id[Foo]|null - OPTIONAL
220220+ "temp-id-1": {
221221+ "name": "New Object",
222222+ "property": "value"
223223+ }
224224+ },
225225+ "update": { // Id[PatchObject]|null - OPTIONAL
226226+ "existing-id": {
227227+ "property": "new-value",
228228+ "nested/field": "value"
229229+ }
230230+ },
231231+ "destroy": ["id-to-delete"] // Id[]|null - OPTIONAL
232232+}
233233+```
234234+235235+**PatchObject Structure:**
236236+- Keys are JSON Pointer paths (without leading `/`)
237237+- Values are: null (to remove/reset to default) or new value to set
238238+- Cannot reference inside arrays
239239+- Cannot have overlapping paths
240240+241241+**Response:**
242242+```json
243243+{
244244+ "accountId": "account-id", // Id - REQUIRED
245245+ "oldState": "old-state", // String|null - REQUIRED
246246+ "newState": "new-state", // String - REQUIRED
247247+ "created": { // Id[Foo]|null - REQUIRED
248248+ "temp-id-1": {
249249+ "id": "server-id-1",
250250+ "serverSetProperty": "value"
251251+ }
252252+ },
253253+ "updated": { // Id[Foo|null]|null - REQUIRED
254254+ "existing-id": {
255255+ "serverSetProperty": "new-value"
256256+ }
257257+ },
258258+ "destroyed": ["id-deleted"], // Id[]|null - REQUIRED
259259+ "notCreated": { // Id[SetError]|null - REQUIRED
260260+ "temp-id-2": {
261261+ "type": "invalidProperties",
262262+ "description": "Property 'name' is required"
263263+ }
264264+ },
265265+ "notUpdated": { // Id[SetError]|null - REQUIRED
266266+ "bad-id": {
267267+ "type": "notFound"
268268+ }
269269+ },
270270+ "notDestroyed": { // Id[SetError]|null - REQUIRED
271271+ "protected-id": {
272272+ "type": "forbidden"
273273+ }
274274+ }
275275+}
276276+```
277277+278278+**SetError Object:**
279279+```json
280280+{
281281+ "type": "invalidProperties", // String - REQUIRED
282282+ "description": "Error details", // String|null - OPTIONAL
283283+ "properties": ["field1", "field2"] // String[] - OPTIONAL (for invalidProperties)
284284+}
285285+```
286286+287287+**SetError Types:**
288288+- `forbidden` - ACL/permission violation
289289+- `overQuota` - Would exceed quota
290290+- `tooLarge` - Object too large
291291+- `rateLimit` - Rate limit reached
292292+- `notFound` - Id not found
293293+- `invalidPatch` - Invalid PatchObject
294294+- `willDestroy` - Object updated and destroyed in same request
295295+- `invalidProperties` - Invalid properties (includes `properties` field)
296296+- `singleton` - Cannot create/destroy singleton
297297+298298+**Method-Level Errors:**
299299+- `requestTooLarge` - Too many operations
300300+- `stateMismatch` - ifInState doesn't match
301301+302302+---
303303+304304+### 3.4 Foo/copy
305305+306306+**Request:**
307307+```json
308308+{
309309+ "fromAccountId": "source-account", // Id - REQUIRED
310310+ "ifFromInState": "source-state", // String|null - OPTIONAL
311311+ "accountId": "dest-account", // Id - REQUIRED
312312+ "ifInState": "dest-state", // String|null - OPTIONAL
313313+ "create": { // Id[Foo] - REQUIRED
314314+ "temp-id-1": {
315315+ "id": "source-id", // Must include source id
316316+ "property": "override-value" // Optional overrides
317317+ }
318318+ },
319319+ "onSuccessDestroyOriginal": false, // Boolean - OPTIONAL (default: false)
320320+ "destroyFromIfInState": "source-state" // String|null - OPTIONAL
321321+}
322322+```
323323+324324+**Response:**
325325+```json
326326+{
327327+ "fromAccountId": "source-account", // Id - REQUIRED
328328+ "accountId": "dest-account", // Id - REQUIRED
329329+ "oldState": "old-dest-state", // String|null - REQUIRED
330330+ "newState": "new-dest-state", // String - REQUIRED
331331+ "created": { // Id[Foo]|null - REQUIRED
332332+ "temp-id-1": {
333333+ "id": "new-id"
334334+ }
335335+ },
336336+ "notCreated": { // Id[SetError]|null - REQUIRED
337337+ "temp-id-2": {
338338+ "type": "alreadyExists",
339339+ "existingId": "existing-id"
340340+ }
341341+ }
342342+}
343343+```
344344+345345+**Additional SetError Type:**
346346+- `alreadyExists` - Duplicate exists (includes `existingId` field)
347347+348348+**Method-Level Errors:**
349349+- `fromAccountNotFound` - Source account not found
350350+- `fromAccountNotSupportedByMethod` - Source account doesn't support type
351351+- `stateMismatch` - ifInState or ifFromInState doesn't match
352352+353353+---
354354+355355+### 3.5 Foo/query
356356+357357+**Request:**
358358+```json
359359+{
360360+ "accountId": "account-id", // Id - REQUIRED
361361+ "filter": { // FilterOperator|FilterCondition|null - OPTIONAL
362362+ "operator": "AND",
363363+ "conditions": [
364364+ {"property": "value"},
365365+ {
366366+ "operator": "OR",
367367+ "conditions": [...]
368368+ }
369369+ ]
370370+ },
371371+ "sort": [ // Comparator[]|null - OPTIONAL
372372+ {
373373+ "property": "name",
374374+ "isAscending": true,
375375+ "collation": "i;unicode-casemap"
376376+ }
377377+ ],
378378+ "position": 0, // Int - OPTIONAL (default: 0)
379379+ "anchor": "anchor-id", // Id|null - OPTIONAL
380380+ "anchorOffset": 0, // Int - OPTIONAL (default: 0)
381381+ "limit": 50, // UnsignedInt|null - OPTIONAL
382382+ "calculateTotal": false // Boolean - OPTIONAL (default: false)
383383+}
384384+```
385385+386386+**FilterOperator:**
387387+```json
388388+{
389389+ "operator": "AND", // String - REQUIRED ("AND", "OR", "NOT")
390390+ "conditions": [...] // (FilterOperator|FilterCondition)[] - REQUIRED
391391+}
392392+```
393393+394394+**FilterCondition:**
395395+- Type-specific object
396396+- MUST NOT have an "operator" property
397397+398398+**Comparator:**
399399+```json
400400+{
401401+ "property": "fieldName", // String - REQUIRED
402402+ "isAscending": true, // Boolean - OPTIONAL (default: true)
403403+ "collation": "i;unicode-casemap" // String - OPTIONAL
404404+}
405405+```
406406+407407+**Response:**
408408+```json
409409+{
410410+ "accountId": "account-id", // Id - REQUIRED
411411+ "queryState": "query-state-string", // String - REQUIRED
412412+ "canCalculateChanges": true, // Boolean - REQUIRED
413413+ "position": 0, // UnsignedInt - REQUIRED
414414+ "ids": ["id1", "id2", "id3"], // Id[] - REQUIRED
415415+ "total": 150, // UnsignedInt - OPTIONAL (only if requested)
416416+ "limit": 50 // UnsignedInt - OPTIONAL (if set by server)
417417+}
418418+```
419419+420420+**Errors:**
421421+- `anchorNotFound` - Anchor id not in results
422422+- `unsupportedSort` - Sort property/collation not supported
423423+- `unsupportedFilter` - Filter cannot be processed
424424+425425+---
426426+427427+### 3.6 Foo/queryChanges
428428+429429+**Request:**
430430+```json
431431+{
432432+ "accountId": "account-id", // Id - REQUIRED
433433+ "filter": {...}, // FilterOperator|FilterCondition|null - OPTIONAL
434434+ "sort": [...], // Comparator[]|null - OPTIONAL
435435+ "sinceQueryState": "old-query-state", // String - REQUIRED
436436+ "maxChanges": 100, // UnsignedInt|null - OPTIONAL
437437+ "upToId": "last-cached-id", // Id|null - OPTIONAL
438438+ "calculateTotal": false // Boolean - OPTIONAL (default: false)
439439+}
440440+```
441441+442442+**Response:**
443443+```json
444444+{
445445+ "accountId": "account-id", // Id - REQUIRED
446446+ "oldQueryState": "old-query-state", // String - REQUIRED
447447+ "newQueryState": "new-query-state", // String - REQUIRED
448448+ "total": 155, // UnsignedInt - OPTIONAL (only if requested)
449449+ "removed": ["id5", "id7"], // Id[] - REQUIRED
450450+ "added": [ // AddedItem[] - REQUIRED
451451+ {
452452+ "id": "id8",
453453+ "index": 0
454454+ },
455455+ {
456456+ "id": "id9",
457457+ "index": 5
458458+ }
459459+ ]
460460+}
461461+```
462462+463463+**AddedItem:**
464464+```json
465465+{
466466+ "id": "object-id", // Id - REQUIRED
467467+ "index": 5 // UnsignedInt - REQUIRED
468468+}
469469+```
470470+471471+**Errors:**
472472+- `tooManyChanges` - More changes than maxChanges
473473+- `cannotCalculateChanges` - Cannot calculate from queryState
474474+475475+---
476476+477477+### 3.7 Core/echo
478478+479479+**Request:**
480480+```json
481481+{
482482+ "arbitrary": "data",
483483+ "can": "be",
484484+ "anything": true
485485+}
486486+```
487487+488488+**Response:**
489489+```json
490490+{
491491+ "arbitrary": "data",
492492+ "can": "be",
493493+ "anything": true
494494+}
495495+```
496496+497497+Returns exactly the same arguments it receives. Used for testing authentication.
498498+499499+---
500500+501501+## 4. BINARY DATA METHODS
502502+503503+### 4.1 Upload Response
504504+505505+Returned from POST to upload endpoint:
506506+507507+```json
508508+{
509509+ "accountId": "account-id", // Id - REQUIRED
510510+ "blobId": "blob-id", // Id - REQUIRED
511511+ "type": "image/jpeg", // String - REQUIRED
512512+ "size": 12345 // UnsignedInt - REQUIRED
513513+}
514514+```
515515+516516+### 4.2 Blob/copy
517517+518518+**Request:**
519519+```json
520520+{
521521+ "fromAccountId": "source-account", // Id - REQUIRED
522522+ "accountId": "dest-account", // Id - REQUIRED
523523+ "blobIds": ["blob-id-1", "blob-id-2"] // Id[] - REQUIRED
524524+}
525525+```
526526+527527+**Response:**
528528+```json
529529+{
530530+ "fromAccountId": "source-account", // Id - REQUIRED
531531+ "accountId": "dest-account", // Id - REQUIRED
532532+ "copied": { // Id[Id]|null - REQUIRED
533533+ "blob-id-1": "new-blob-id-1"
534534+ },
535535+ "notCopied": { // Id[SetError]|null - REQUIRED
536536+ "blob-id-2": {
537537+ "type": "notFound"
538538+ }
539539+ }
540540+}
541541+```
542542+543543+**Method-Level Errors:**
544544+- `fromAccountNotFound` - Source account not found
545545+546546+---
547547+548548+## 5. PUSH NOTIFICATION TYPES
549549+550550+### 5.1 StateChange Object
551551+552552+Pushed to client when server state changes:
553553+554554+```json
555555+{
556556+ "@type": "StateChange", // String - REQUIRED
557557+ "changed": { // Id[TypeState] - REQUIRED
558558+ "account-id-1": {
559559+ "Email": "d35ecb040aab",
560560+ "Mailbox": "0af7a512ce70"
561561+ },
562562+ "account-id-2": {
563563+ "CalendarEvent": "7a4297cecd76"
564564+ }
565565+ }
566566+}
567567+```
568568+569569+**TypeState:** Map of type name (e.g., "Email") to state string
570570+571571+---
572572+573573+### 5.2 PushSubscription Object
574574+575575+```json
576576+{
577577+ "id": "push-sub-id", // Id - REQUIRED (immutable, server-set)
578578+ "deviceClientId": "device-hash", // String - REQUIRED (immutable)
579579+ "url": "https://push.example.com/push", // String - REQUIRED (immutable)
580580+ "keys": { // Object|null - OPTIONAL (immutable)
581581+ "p256dh": "base64-encoded-key",
582582+ "auth": "base64-encoded-secret"
583583+ },
584584+ "verificationCode": "verification-code", // String|null - OPTIONAL
585585+ "expires": "2024-12-31T23:59:59Z", // UTCDate|null - OPTIONAL
586586+ "types": ["Email", "Mailbox"] // String[]|null - OPTIONAL
587587+}
588588+```
589589+590590+---
591591+592592+### 5.3 PushSubscription/get
593593+594594+**Request:**
595595+```json
596596+{
597597+ "ids": ["sub-id-1", "sub-id-2"], // Id[]|null - REQUIRED
598598+ "properties": ["id", "expires", "types"] // String[]|null - OPTIONAL
599599+}
600600+```
601601+602602+**Notes:**
603603+- NO `accountId` argument
604604+- NO `state` in response
605605+- `url` and `keys` properties MUST NOT be returned (use `forbidden` error if requested)
606606+607607+**Response:**
608608+```json
609609+{
610610+ "list": [...], // PushSubscription[] - REQUIRED
611611+ "notFound": [...] // Id[] - REQUIRED
612612+}
613613+```
614614+615615+---
616616+617617+### 5.4 PushSubscription/set
618618+619619+**Request:**
620620+```json
621621+{
622622+ "create": { // Id[PushSubscription]|null - OPTIONAL
623623+ "temp-id": {
624624+ "deviceClientId": "device-hash",
625625+ "url": "https://push.example.com/push",
626626+ "keys": {...},
627627+ "types": ["Email"]
628628+ }
629629+ },
630630+ "update": { // Id[PatchObject]|null - OPTIONAL
631631+ "sub-id": {
632632+ "expires": "2025-12-31T23:59:59Z",
633633+ "types": ["Email", "Mailbox"]
634634+ }
635635+ },
636636+ "destroy": ["sub-id-2"] // Id[]|null - OPTIONAL
637637+}
638638+```
639639+640640+**Notes:**
641641+- NO `accountId` argument
642642+- NO `ifInState` argument
643643+- NO `oldState` or `newState` in response
644644+- `url` and `keys` are immutable
645645+646646+**Response:**
647647+```json
648648+{
649649+ "created": {...}, // Id[PushSubscription]|null - REQUIRED
650650+ "updated": {...}, // Id[PushSubscription|null]|null - REQUIRED
651651+ "destroyed": [...], // Id[]|null - REQUIRED
652652+ "notCreated": {...}, // Id[SetError]|null - REQUIRED
653653+ "notUpdated": {...}, // Id[SetError]|null - REQUIRED
654654+ "notDestroyed": {...} // Id[SetError]|null - REQUIRED
655655+}
656656+```
657657+658658+---
659659+660660+### 5.5 PushVerification Object
661661+662662+Sent to URL when PushSubscription is created:
663663+664664+```json
665665+{
666666+ "@type": "PushVerification", // String - REQUIRED
667667+ "pushSubscriptionId": "sub-id", // String - REQUIRED
668668+ "verificationCode": "random-code" // String - REQUIRED
669669+}
670670+```
671671+672672+---
673673+674674+## 6. ERROR TYPES
675675+676676+### 6.1 Request-Level Errors
677677+678678+HTTP error responses with JSON problem details (RFC 7807):
679679+680680+```json
681681+{
682682+ "type": "urn:ietf:params:jmap:error:unknownCapability", // String
683683+ "status": 400, // Number
684684+ "detail": "Description of the error", // String
685685+ "limit": "maxSizeRequest" // String (for limit errors)
686686+}
687687+```
688688+689689+**Error Types:**
690690+- `urn:ietf:params:jmap:error:unknownCapability` - Unsupported capability in "using"
691691+- `urn:ietf:params:jmap:error:notJSON` - Not application/json or invalid I-JSON
692692+- `urn:ietf:params:jmap:error:notRequest` - JSON doesn't match Request type
693693+- `urn:ietf:params:jmap:error:limit` - Request limit exceeded (includes `limit` property)
694694+695695+---
696696+697697+### 6.2 Method-Level Errors
698698+699699+Error response Invocation:
700700+701701+```json
702702+[
703703+ "error", // String - response name
704704+ {
705705+ "type": "unknownMethod", // String - REQUIRED
706706+ "description": "Additional details" // String|null - OPTIONAL
707707+ },
708708+ "call-id" // String - method call id
709709+]
710710+```
711711+712712+**Error Types (General):**
713713+- `serverUnavailable` - Temporary server issue, retry later
714714+- `serverFail` - Unexpected error, includes `description`
715715+- `serverPartialFail` - Some changes succeeded, must resync
716716+- `unknownMethod` - Method name not recognized
717717+- `invalidArguments` - Invalid/missing arguments, may include `description`
718718+- `invalidResultReference` - Result reference failed to resolve
719719+- `forbidden` - ACL/permission violation
720720+- `accountNotFound` - Invalid accountId
721721+- `accountNotSupportedByMethod` - Account doesn't support this method
722722+- `accountReadOnly` - Account is read-only
723723+724724+**Method-Specific Errors:** (See sections 3.1-3.6 above)
725725+726726+---
727727+728728+### 6.3 SetError Types
729729+730730+Used in /set and /copy responses:
731731+732732+```json
733733+{
734734+ "type": "invalidProperties", // String - REQUIRED
735735+ "description": "Error details", // String|null - OPTIONAL
736736+ "properties": ["field1", "field2"], // String[] - OPTIONAL
737737+ "existingId": "existing-id" // Id - OPTIONAL (for alreadyExists)
738738+}
739739+```
740740+741741+**All SetError Types:**
742742+- `forbidden` - Permission denied
743743+- `overQuota` - Quota exceeded
744744+- `tooLarge` - Object too large
745745+- `rateLimit` - Rate limit hit
746746+- `notFound` - Id not found
747747+- `invalidPatch` - Invalid PatchObject
748748+- `willDestroy` - Object both updated and destroyed
749749+- `invalidProperties` - Invalid properties (includes `properties` array)
750750+- `singleton` - Cannot create/destroy singleton
751751+- `alreadyExists` - Duplicate exists (includes `existingId`)
752752+753753+---
754754+755755+## 7. COMMON DATA TYPES
756756+757757+### 7.1 Primitive Types
758758+759759+- `Id` - String, minimum length 1, maximum length 255
760760+- `Int` - Signed 53-bit integer (-2^53 + 1 to 2^53 - 1)
761761+- `UnsignedInt` - Unsigned integer (0 to 2^53 - 1)
762762+- `Date` - String in RFC 3339 date-time format (local or with timezone)
763763+- `UTCDate` - String in RFC 3339 format with "Z" timezone
764764+- `Boolean` - JSON boolean (true/false)
765765+- `String` - JSON string
766766+- `Number` - JSON number
767767+768768+### 7.2 Complex Types
769769+770770+- `T[]` - JSON array of type T
771771+- `T|null` - Either type T or null
772772+- `String[T]` - JSON object with string keys and values of type T
773773+- `Id[T]` - JSON object with Id keys and values of type T
774774+775775+### 7.3 PatchObject
776776+777777+Type: `String[*]` - Keys are JSON Pointer paths, values are new values or null
778778+779779+**Rules:**
780780+- Keys must be valid JSON Pointer paths (without leading `/`)
781781+- Cannot reference inside arrays
782782+- Cannot have overlapping paths
783783+- null value means remove/reset to default
784784+- Any other value is set/replaced
785785+786786+---
787787+788788+## 8. METHOD CALL PATTERNS
789789+790790+### 8.1 Argument Naming Conventions
791791+792792+**Common Required Arguments:**
793793+- `accountId`: `Id` - Present in most methods (except PushSubscription/*)
794794+- `ids`: `Id[]|null` - For /get methods
795795+- `sinceState`: `String` - For /changes methods
796796+- `sinceQueryState`: `String` - For /queryChanges methods
797797+798798+**Common Optional Arguments:**
799799+- `properties`: `String[]|null` - Property filter
800800+- `maxChanges`: `UnsignedInt|null` - Limit for /changes and /queryChanges
801801+- `ifInState`: `String|null` - Conditional state for /set and /copy
802802+- `calculateTotal`: `Boolean` - Request total count (default: false)
803803+804804+### 8.2 Response Naming Conventions
805805+806806+**Common Response Fields:**
807807+- `accountId`: `Id` - Echo of request accountId
808808+- `state`: `String` - Current state (in /get responses)
809809+- `oldState`: `String` - Previous state (in /set, /changes responses)
810810+- `newState`: `String` - New state (in /set, /changes responses)
811811+812812+**State Tracking:**
813813+- State strings MUST change when data changes
814814+- State strings SHOULD NOT change when data is unchanged
815815+- Used for efficient synchronization
816816+817817+---
818818+819819+## 9. SPECIAL FEATURES
820820+821821+### 9.1 Back-References
822822+823823+Arguments prefixed with `#` reference previous method results:
824824+825825+```json
826826+{
827827+ "methodCalls": [
828828+ ["Foo/changes", {"accountId": "a1", "sinceState": "s0"}, "c1"],
829829+ ["Foo/get", {
830830+ "accountId": "a1",
831831+ "#ids": {
832832+ "resultOf": "c1",
833833+ "name": "Foo/changes",
834834+ "path": "/created"
835835+ }
836836+ }, "c2"]
837837+ ]
838838+}
839839+```
840840+841841+### 9.2 Creation ID References
842842+843843+Use `#` prefix with creation id to reference newly created objects:
844844+845845+```json
846846+{
847847+ "methodCalls": [
848848+ ["Mailbox/set", {
849849+ "accountId": "a1",
850850+ "create": {
851851+ "temp1": {"name": "Drafts"}
852852+ }
853853+ }, "c1"],
854854+ ["Email/set", {
855855+ "accountId": "a1",
856856+ "create": {
857857+ "temp2": {
858858+ "mailboxIds": {"#temp1": true} // Reference to created mailbox
859859+ }
860860+ }
861861+ }, "c2"]
862862+ ]
863863+}
864864+```
865865+866866+### 9.3 JSON Pointer with Wildcards
867867+868868+The `path` in ResultReference supports `*` for array mapping:
869869+870870+- `/list/*/id` - Maps through array, collecting all ids
871871+- Flattens nested arrays automatically
872872+873873+---
874874+875875+## 10. IMPLEMENTATION NOTES FOR OCAML
876876+877877+### 10.1 Required Type Definitions
878878+879879+1. **Core Protocol Types:**
880880+ - `invocation` - 3-tuple of (string * Yojson.Basic.t * string)
881881+ - `request` - Record with using, methodCalls, createdIds?
882882+ - `response` - Record with methodResponses, createdIds?, sessionState
883883+ - `result_reference` - Record with resultOf, name, path
884884+885885+2. **Session Types:**
886886+ - `session` - All session fields
887887+ - `account` - Account object fields
888888+ - `capabilities` - Core capability fields
889889+890890+3. **Standard Method Types:**
891891+ - `get_request`, `get_response`
892892+ - `changes_request`, `changes_response`
893893+ - `set_request`, `set_response`
894894+ - `copy_request`, `copy_response`
895895+ - `query_request`, `query_response`
896896+ - `query_changes_request`, `query_changes_response`
897897+898898+4. **Error Types:**
899899+ - `request_error` - Request-level errors
900900+ - `method_error` - Method-level errors
901901+ - `set_error` - SetError with type and optional fields
902902+903903+5. **Filter/Sort Types:**
904904+ - `filter_operator` - Recursive filter structure
905905+ - `comparator` - Sort comparator
906906+907907+6. **Push Types:**
908908+ - `state_change` - StateChange object
909909+ - `push_subscription` - PushSubscription object
910910+ - `push_verification` - PushVerification object
911911+912912+### 10.2 Parsing Considerations
913913+914914+1. **Invocations are heterogeneous:** The arguments object varies by method name
915915+2. **Optional fields:** Many fields are `|null` or absent with defaults
916916+3. **Maps vs Records:** Use appropriate OCaml types (Hashtbl vs record)
917917+4. **State strings:** Opaque strings, no internal structure required
918918+5. **JSON Pointer parsing:** Need separate parser for path evaluation
919919+6. **Type safety:** Consider GADTs for type-safe method call/response pairing
920920+921921+### 10.3 Suggested Module Structure
922922+923923+```ocaml
924924+module JMAP : sig
925925+ module Types : sig
926926+ (* Primitive types *)
927927+ (* Core protocol types *)
928928+ (* Error types *)
929929+ end
930930+931931+ module Session : sig
932932+ (* Session-related types and functions *)
933933+ end
934934+935935+ module Methods : sig
936936+ (* Standard method request/response types *)
937937+ module Get : sig ... end
938938+ module Changes : sig ... end
939939+ module Set : sig ... end
940940+ module Copy : sig ... end
941941+ module Query : sig ... end
942942+ module QueryChanges : sig ... end
943943+ end
944944+945945+ module Push : sig
946946+ (* Push notification types *)
947947+ end
948948+949949+ module Binary : sig
950950+ (* Binary data types *)
951951+ end
952952+953953+ module Parser : sig
954954+ (* JSON parsing functions *)
955955+ end
956956+end
957957+```
958958+959959+---
960960+961961+## SUMMARY
962962+963963+This specification defines:
964964+- **3 core message types** (Request, Response, Invocation)
965965+- **1 session type** (Session with nested Account)
966966+- **7 standard method patterns** (/get, /changes, /set, /copy, /query, /queryChanges, /echo)
967967+- **3 binary data operations** (upload, download, Blob/copy)
968968+- **3 push notification types** (StateChange, PushSubscription, PushVerification)
969969+- **33 error codes** (4 request-level, 11 method-level, 18 set-level)
970970+- **Multiple supporting types** (ResultReference, FilterOperator, Comparator, PatchObject, SetError, etc.)
971971+972972+All methods follow consistent patterns with predictable argument and response structures, making systematic OCaml type generation feasible.
+118
jmap/MODULE_STRUCTURE.md
···11+# JMAP OCaml Module Structure
22+33+## Overview
44+55+The JMAP libraries use proper OCaml module wrapping with module aliases for re-export. This provides a clean namespace while allowing both qualified and unqualified access to submodules.
66+77+## Library Structure
88+99+### jmap-core
1010+1111+**Top-level module:** `Jmap_core`
1212+1313+**Submodules (re-exported):**
1414+- `Jmap_core.Jmap_error` - Error types and exceptions
1515+- `Jmap_core.Jmap_id` - JMAP ID type
1616+- `Jmap_core.Jmap_primitives` - Primitive types (Int53, UnsignedInt, Date, UTCDate)
1717+- `Jmap_core.Jmap_capability` - Server capabilities
1818+- `Jmap_core.Jmap_filter` - Generic filter combinators
1919+- `Jmap_core.Jmap_comparator` - Sort comparators
2020+- `Jmap_core.Jmap_standard_methods` - Standard JMAP methods (Get, Set, Query, etc.)
2121+- `Jmap_core.Jmap_invocation` - Type-safe method invocations (GADT-based)
2222+- `Jmap_core.Jmap_request` - JMAP request wrapper
2323+- `Jmap_core.Jmap_response` - JMAP response wrapper
2424+- `Jmap_core.Jmap_session` - Session discovery
2525+- `Jmap_core.Jmap_push` - Push notifications
2626+- `Jmap_core.Jmap_binary` - Binary data operations
2727+- `Jmap_core.Jmap_parser` - JSON parsing helpers
2828+2929+### jmap-mail
3030+3131+**Top-level module:** `Jmap_mail`
3232+3333+**Submodules (re-exported):**
3434+- `Jmap_mail.Jmap_mailbox` - Mailbox type and operations
3535+- `Jmap_mail.Jmap_thread` - Thread type and operations
3636+- `Jmap_mail.Jmap_email` - Email type and operations
3737+- `Jmap_mail.Jmap_identity` - Identity type and operations
3838+- `Jmap_mail.Jmap_email_submission` - Email submission type and operations
3939+- `Jmap_mail.Jmap_vacation_response` - Vacation response type and operations
4040+- `Jmap_mail.Jmap_search_snippet` - Search snippet type and operations
4141+- `Jmap_mail.Jmap_mail_parser` - Mail-specific JSON parsing
4242+4343+### jmap-client
4444+4545+**Top-level module:** `Jmap_client`
4646+4747+**Files:**
4848+- `Jmap_client` - HTTP client (stub implementation)
4949+- `Jmap_connection` - Connection management (stub implementation)
5050+5151+## Usage Patterns
5252+5353+### Pattern 1: Qualified Access
5454+5555+```ocaml
5656+let id = Jmap_core.Jmap_id.of_string "abc123"
5757+let mailbox = Jmap_mail.Jmap_mailbox.v
5858+ ~id ~name:"Inbox"
5959+ ~sort_order:(Jmap_core.Jmap_primitives.UnsignedInt.of_int 0)
6060+ ...
6161+```
6262+6363+### Pattern 2: Open for Direct Access
6464+6565+```ocaml
6666+open Jmap_core
6767+open Jmap_mail
6868+6969+let id = Jmap_id.of_string "abc123"
7070+let mailbox = Jmap_mailbox.v
7171+ ~id ~name:"Inbox"
7272+ ~sort_order:(Jmap_primitives.UnsignedInt.of_int 0)
7373+ ...
7474+```
7575+7676+### Pattern 3: Mixed (Recommended)
7777+7878+```ocaml
7979+(* Open Jmap_core for common types *)
8080+open Jmap_core
8181+8282+(* Use qualified names for specific modules *)
8383+let mailbox = Jmap_mail.Jmap_mailbox.v
8484+ ~id:(Jmap_id.of_string "inbox")
8585+ ~name:"Inbox"
8686+ ...
8787+```
8888+8989+## Benefits of This Structure
9090+9191+1. **Namespace Control**: All modules are under `Jmap_core` or `Jmap_mail`, avoiding name collisions
9292+2. **Flexible Access**: Use qualified or unqualified names as needed
9393+3. **Clear Dependencies**: Module hierarchy reflects the protocol structure
9494+4. **Standard Practice**: Follows OCaml best practices for library design
9595+5. **Tool Compatibility**: Works well with merlin, ocamllsp, and other OCaml tools
9696+9797+## Building
9898+9999+```bash
100100+dune build
101101+```
102102+103103+## Testing
104104+105105+```bash
106106+dune test
107107+```
108108+109109+## Installation
110110+111111+```bash
112112+dune install
113113+```
114114+115115+This will install three packages:
116116+- `jmap-core` - Core JMAP protocol (RFC 8620)
117117+- `jmap-mail` - Mail extension (RFC 8621)
118118+- `jmap-client` - HTTP client (stub)
+506
jmap/PARSER_IMPLEMENTATION_GUIDE.md
···11+# Parser Implementation Guide
22+33+This guide will help you complete the JSON parser implementations throughout the JMAP codebase. All type definitions are complete - only the parsing logic needs to be filled in.
44+55+## Overview
66+77+**Status**: All `of_json` and `to_json` functions have stub implementations that raise "not yet implemented" errors.
88+99+**Goal**: Implement these functions using the provided test JSON files as specifications.
1010+1111+**Tools**: Use `Jmap_parser.Helpers` module for common parsing operations.
1212+1313+## Implementation Strategy
1414+1515+### Step 1: Start with Primitives (Easiest)
1616+1717+These are already complete, but review them as examples:
1818+1919+```ocaml
2020+(* jmap-core/jmap_id.ml - COMPLETE *)
2121+let of_json json =
2222+ match json with
2323+ | `String s -> of_string s
2424+ | _ -> raise (Jmap_error.Parse_error "Id must be a JSON string")
2525+2626+(* jmap-core/jmap_primitives.ml - COMPLETE *)
2727+module UnsignedInt = struct
2828+ let of_json = function
2929+ | `Float f -> of_int (int_of_float f)
3030+ | `Int i -> if i >= 0 then of_int i else raise (Parse_error "...")
3131+ | _ -> raise (Parse_error "Expected number")
3232+end
3333+```
3434+3535+### Step 2: Implement Core Parsers
3636+3737+#### 2.1 Comparator (Simple Object)
3838+3939+**File**: `jmap-core/jmap_comparator.ml`
4040+**Test**: `test/data/core/request_query.json` (sort field)
4141+4242+```ocaml
4343+let of_json json =
4444+ let open Jmap_parser.Helpers in
4545+ let fields = expect_object json in
4646+ let property = get_string "property" fields in
4747+ let is_ascending = get_bool_opt "isAscending" fields true in
4848+ let collation = get_string_opt "collation" fields in
4949+ { property; is_ascending; collation }
5050+```
5151+5252+#### 2.2 Filter (Recursive Type)
5353+5454+**File**: `jmap-core/jmap_filter.ml`
5555+**Test**: `test/data/core/request_query.json` (filter field)
5656+5757+The generic `of_json` function is already complete. You need to implement condition parsers for each type (Mailbox, Email, etc.).
5858+5959+#### 2.3 Session (Complex Nested Object)
6060+6161+**File**: `jmap-core/jmap_session.ml`
6262+**Test**: `test/data/core/session.json`
6363+6464+```ocaml
6565+(* Account parser *)
6666+module Account = struct
6767+ let of_json json =
6868+ let open Jmap_parser.Helpers in
6969+ let fields = expect_object json in
7070+ {
7171+ name = get_string "name" fields;
7272+ is_personal = get_bool "isPersonal" fields;
7373+ is_read_only = get_bool "isReadOnly" fields;
7474+ account_capabilities = parse_map (fun v -> v)
7575+ (require_field "accountCapabilities" fields);
7676+ }
7777+end
7878+7979+(* Session parser *)
8080+let of_json json =
8181+ let open Jmap_parser.Helpers in
8282+ let fields = expect_object json in
8383+ {
8484+ capabilities = parse_map (fun v -> v) (require_field "capabilities" fields);
8585+ accounts = parse_map Account.of_json (require_field "accounts" fields);
8686+ primary_accounts = parse_map expect_string (require_field "primaryAccounts" fields);
8787+ username = get_string "username" fields;
8888+ api_url = get_string "apiUrl" fields;
8989+ download_url = get_string "downloadUrl" fields;
9090+ upload_url = get_string "uploadUrl" fields;
9191+ event_source_url = get_string "eventSourceUrl" fields;
9292+ state = get_string "state" fields;
9393+ }
9494+```
9595+9696+#### 2.4 Invocation (3-tuple Array)
9797+9898+**File**: `jmap-core/jmap_invocation.ml`
9999+**Test**: Any request or response file (methodCalls/methodResponses field)
100100+101101+```ocaml
102102+let of_json json =
103103+ let open Jmap_parser.Helpers in
104104+ match json with
105105+ | `A [method_name_json; arguments_json; call_id_json] ->
106106+ let method_name = expect_string method_name_json in
107107+ let call_id = expect_string call_id_json in
108108+109109+ (* Parse based on method name *)
110110+ begin match witness_of_method_name method_name with
111111+ | Packed template ->
112112+ (* Parse arguments based on witness type *)
113113+ (* Return properly typed invocation *)
114114+ (* TODO: Complete this logic *)
115115+ raise (Parse_error "Invocation parsing not complete")
116116+ end
117117+118118+ | _ -> raise (Parse_error "Invocation must be 3-element array")
119119+```
120120+121121+#### 2.5 Request and Response
122122+123123+**File**: `jmap-core/jmap_request.ml` and `jmap_response.ml`
124124+**Test**: All `test/data/core/request_*.json` and `response_*.json`
125125+126126+```ocaml
127127+(* Request *)
128128+let of_json json =
129129+ let open Jmap_parser.Helpers in
130130+ let fields = expect_object json in
131131+ {
132132+ using = parse_array Jmap_capability.of_json
133133+ (require_field "using" fields);
134134+ method_calls = parse_array Jmap_invocation.of_json
135135+ (require_field "methodCalls" fields);
136136+ created_ids = match find_field "createdIds" fields with
137137+ | Some obj -> Some (parse_map Jmap_id.of_json obj)
138138+ | None -> None;
139139+ }
140140+141141+(* Response - similar pattern *)
142142+```
143143+144144+### Step 3: Implement Standard Method Parsers
145145+146146+These follow predictable patterns. Example for Get:
147147+148148+**File**: `jmap-core/jmap_standard_methods.ml`
149149+**Tests**: `test/data/core/request_get.json`, `response_get.json`
150150+151151+```ocaml
152152+module Get = struct
153153+ let request_of_json parse_obj json =
154154+ let open Jmap_parser.Helpers in
155155+ let fields = expect_object json in
156156+ {
157157+ account_id = Jmap_id.of_json (require_field "accountId" fields);
158158+ ids = parse_array_opt Jmap_id.of_json (find_field "ids" fields);
159159+ properties = parse_array_opt expect_string (find_field "properties" fields);
160160+ }
161161+162162+ let response_of_json parse_obj json =
163163+ let open Jmap_parser.Helpers in
164164+ let fields = expect_object json in
165165+ {
166166+ account_id = Jmap_id.of_json (require_field "accountId" fields);
167167+ state = get_string "state" fields;
168168+ list = parse_array parse_obj (require_field "list" fields);
169169+ not_found = parse_array Jmap_id.of_json (require_field "notFound" fields);
170170+ }
171171+end
172172+173173+(* Repeat for Changes, Set, Copy, Query, QueryChanges *)
174174+```
175175+176176+### Step 4: Implement Mail Type Parsers
177177+178178+#### 4.1 Mailbox (Simple Mail Type)
179179+180180+**File**: `jmap-mail/jmap_mailbox.ml`
181181+**Tests**: `test/data/mail/mailbox_get_response.json`
182182+183183+```ocaml
184184+module Rights = struct
185185+ let of_json json =
186186+ let open Jmap_parser.Helpers in
187187+ let fields = expect_object json in
188188+ {
189189+ may_read_items = get_bool "mayReadItems" fields;
190190+ may_add_items = get_bool "mayAddItems" fields;
191191+ may_remove_items = get_bool "mayRemoveItems" fields;
192192+ may_set_seen = get_bool "maySetSeen" fields;
193193+ may_set_keywords = get_bool "maySetKeywords" fields;
194194+ may_create_child = get_bool "mayCreateChild" fields;
195195+ may_rename = get_bool "mayRename" fields;
196196+ may_delete = get_bool "mayDelete" fields;
197197+ may_submit = get_bool "maySubmit" fields;
198198+ }
199199+end
200200+201201+let of_json json =
202202+ let open Jmap_parser.Helpers in
203203+ let fields = expect_object json in
204204+ {
205205+ id = Jmap_id.of_json (require_field "id" fields);
206206+ name = get_string "name" fields;
207207+ parent_id = Option.map Jmap_id.of_json (find_field "parentId" fields);
208208+ role = get_string_opt "role" fields;
209209+ sort_order = Jmap_primitives.UnsignedInt.of_json
210210+ (require_field "sortOrder" fields);
211211+ total_emails = Jmap_primitives.UnsignedInt.of_json
212212+ (require_field "totalEmails" fields);
213213+ unread_emails = Jmap_primitives.UnsignedInt.of_json
214214+ (require_field "unreadEmails" fields);
215215+ total_threads = Jmap_primitives.UnsignedInt.of_json
216216+ (require_field "totalThreads" fields);
217217+ unread_threads = Jmap_primitives.UnsignedInt.of_json
218218+ (require_field "unreadThreads" fields);
219219+ my_rights = Rights.of_json (require_field "myRights" fields);
220220+ is_subscribed = get_bool "isSubscribed" fields;
221221+ }
222222+```
223223+224224+#### 4.2 Email (Most Complex)
225225+226226+**File**: `jmap-mail/jmap_email.ml`
227227+**Tests**:
228228+- `test/data/mail/email_get_response.json` (basic)
229229+- `test/data/mail/email_get_full_response.json` (with body structure)
230230+231231+Start with submodules:
232232+233233+```ocaml
234234+module EmailAddress = struct
235235+ let of_json json =
236236+ let open Jmap_parser.Helpers in
237237+ let fields = expect_object json in
238238+ {
239239+ name = get_string_opt "name" fields;
240240+ email = get_string "email" fields;
241241+ }
242242+end
243243+244244+module BodyPart = struct
245245+ (* Recursive parser for MIME structure *)
246246+ let rec of_json json =
247247+ let open Jmap_parser.Helpers in
248248+ let fields = expect_object json in
249249+ {
250250+ part_id = get_string_opt "partId" fields;
251251+ blob_id = Option.map Jmap_id.of_json (find_field "blobId" fields);
252252+ size = Jmap_primitives.UnsignedInt.of_json (require_field "size" fields);
253253+ headers = parse_array parse_header (require_field "headers" fields);
254254+ name = get_string_opt "name" fields;
255255+ type_ = get_string "type" fields;
256256+ charset = get_string_opt "charset" fields;
257257+ disposition = get_string_opt "disposition" fields;
258258+ cid = get_string_opt "cid" fields;
259259+ language = parse_array_opt expect_string (find_field "language" fields);
260260+ location = get_string_opt "location" fields;
261261+ sub_parts = parse_array_opt of_json (find_field "subParts" fields);
262262+ }
263263+264264+ and parse_header json =
265265+ let open Jmap_parser.Helpers in
266266+ let fields = expect_object json in
267267+ (get_string "name" fields, get_string "value" fields)
268268+end
269269+270270+(* Main Email parser *)
271271+let of_json json =
272272+ let open Jmap_parser.Helpers in
273273+ let fields = expect_object json in
274274+ {
275275+ (* Parse all 24 fields *)
276276+ id = Jmap_id.of_json (require_field "id" fields);
277277+ blob_id = Jmap_id.of_json (require_field "blobId" fields);
278278+ thread_id = Jmap_id.of_json (require_field "threadId" fields);
279279+ mailbox_ids = parse_map (fun _ -> true) (require_field "mailboxIds" fields);
280280+ keywords = parse_map (fun _ -> true) (require_field "keywords" fields);
281281+ (* ... continue for all fields ... *)
282282+ from = parse_array_opt EmailAddress.of_json (find_field "from" fields);
283283+ to_ = parse_array_opt EmailAddress.of_json (find_field "to" fields);
284284+ body_structure = Option.map BodyPart.of_json (find_field "bodyStructure" fields);
285285+ (* ... etc ... *)
286286+ }
287287+```
288288+289289+### Step 5: Testing Pattern
290290+291291+For each parser you implement:
292292+293293+```ocaml
294294+(* In test/test_jmap.ml *)
295295+296296+let test_mailbox_parse () =
297297+ (* Load test JSON *)
298298+ let json = load_json "test/data/mail/mailbox_get_response.json" in
299299+300300+ (* Parse response *)
301301+ let response = Jmap_mail.Jmap_mailbox.Get.response_of_json
302302+ Jmap_mailbox.Parser.of_json json in
303303+304304+ (* Validate *)
305305+ check int "Mailbox count" 3 (List.length response.list);
306306+307307+ let inbox = List.hd response.list in
308308+ check string "Inbox name" "Inbox" inbox.name;
309309+ check (option string) "Inbox role" (Some "inbox") inbox.role;
310310+ check bool "Can read" true inbox.my_rights.may_read_items;
311311+312312+let () =
313313+ run "JMAP" [
314314+ "Mailbox", [
315315+ test_case "Parse mailbox response" `Quick test_mailbox_parse;
316316+ ];
317317+ ]
318318+```
319319+320320+## Common Patterns
321321+322322+### Optional Fields
323323+324324+```ocaml
325325+(* Option with default *)
326326+let is_ascending = get_bool_opt "isAscending" fields true
327327+328328+(* Option without default *)
329329+let collation = get_string_opt "collation" fields
330330+331331+(* Map with option *)
332332+parent_id = Option.map Jmap_id.of_json (find_field "parentId" fields)
333333+```
334334+335335+### Arrays
336336+337337+```ocaml
338338+(* Required array *)
339339+ids = parse_array Jmap_id.of_json (require_field "ids" fields)
340340+341341+(* Optional array (null or array) *)
342342+properties = parse_array_opt expect_string (find_field "properties" fields)
343343+```
344344+345345+### Maps (JSON Objects)
346346+347347+```ocaml
348348+(* String -> value *)
349349+keywords = parse_map (fun v -> true) (require_field "keywords" fields)
350350+351351+(* Id -> value *)
352352+mailbox_ids = parse_map Jmap_id.of_json (require_field "mailboxIds" fields)
353353+```
354354+355355+### Recursive Types
356356+357357+```ocaml
358358+(* Mutually recursive *)
359359+let rec parse_filter parse_condition json =
360360+ match json with
361361+ | `O fields ->
362362+ match List.assoc_opt "operator" fields with
363363+ | Some op -> (* FilterOperator *)
364364+ let conditions = parse_array (parse_filter parse_condition) ... in
365365+ Operator (op, conditions)
366366+ | None -> (* FilterCondition *)
367367+ Condition (parse_condition json)
368368+ | _ -> raise (Parse_error "...")
369369+```
370370+371371+## Helper Reference
372372+373373+```ocaml
374374+(* From Jmap_parser.Helpers *)
375375+376376+(* Type expectations *)
377377+expect_object : json -> (string * json) list
378378+expect_array : json -> json list
379379+expect_string : json -> string
380380+expect_int : json -> int
381381+expect_bool : json -> bool
382382+383383+(* Field access *)
384384+find_field : string -> fields -> json option
385385+require_field : string -> fields -> json
386386+387387+(* Typed getters *)
388388+get_string : string -> fields -> string
389389+get_string_opt : string -> fields -> string option
390390+get_bool : string -> fields -> bool
391391+get_bool_opt : string -> fields -> bool -> bool (* with default *)
392392+get_int : string -> fields -> int
393393+get_int_opt : string -> fields -> int option
394394+395395+(* Parsers *)
396396+parse_map : (json -> 'a) -> json -> (string * 'a) list
397397+parse_array : (json -> 'a) -> json -> 'a list
398398+parse_array_opt : (json -> 'a) -> json option -> 'a list option
399399+```
400400+401401+## Order of Implementation
402402+403403+Recommended order (easiest to hardest):
404404+405405+1. ✅ Primitives (already done)
406406+2. `Jmap_comparator` - Simple object
407407+3. `Jmap_capability.CoreCapability` - Nested object
408408+4. `Jmap_session` - Complex nested object with maps
409409+5. `Jmap_standard_methods.Get.request` - Simple with optionals
410410+6. `Jmap_standard_methods.Get.response` - With generic list
411411+7. Other standard methods (Changes, Query, etc.)
412412+8. `Jmap_invocation` - Array tuple with GADT dispatch
413413+9. `Jmap_request` and `Jmap_response` - Top-level protocol
414414+10. `Jmap_mailbox` - Simplest mail type
415415+11. `Jmap_thread` - Very simple (2 fields)
416416+12. `Jmap_identity` - Medium complexity
417417+13. `Jmap_vacation_response` - Singleton pattern
418418+14. `Jmap_search_snippet` - Search results
419419+15. `Jmap_email_submission` - With enums and envelope
420420+16. `Jmap_email` - Most complex (save for last)
421421+422422+## Validation Strategy
423423+424424+For each parser:
425425+426426+1. **Parse test file**: Ensure no exceptions
427427+2. **Check required fields**: Verify non-optional fields are present
428428+3. **Validate values**: Check actual values match test file
429429+4. **Round-trip**: Serialize and parse again, compare
430430+5. **Error cases**: Try malformed JSON, missing fields
431431+432432+## Serialization (to_json)
433433+434434+After parsing is complete, implement serialization:
435435+436436+```ocaml
437437+let to_json t =
438438+ `O [
439439+ ("id", Jmap_id.to_json t.id);
440440+ ("name", `String t.name);
441441+ ("sortOrder", Jmap_primitives.UnsignedInt.to_json t.sort_order);
442442+ (* ... *)
443443+ ]
444444+```
445445+446446+Remove fields that are None:
447447+448448+```ocaml
449449+let fields = [
450450+ ("id", Jmap_id.to_json t.id);
451451+ ("name", `String t.name);
452452+] in
453453+let fields = match t.parent_id with
454454+ | Some pid -> ("parentId", Jmap_id.to_json pid) :: fields
455455+ | None -> fields
456456+in
457457+`O fields
458458+```
459459+460460+## Common Pitfalls
461461+462462+1. **Case sensitivity**: JSON field names are case-sensitive
463463+ - Use `"receivedAt"` not `"receivedat"`
464464+465465+2. **Null vs absent**: Distinguish between `null` and field not present
466466+ ```ocaml
467467+ | Some `Null -> None (* null *)
468468+ | Some value -> Some (parse value) (* present *)
469469+ | None -> None (* absent *)
470470+ ```
471471+472472+3. **Empty arrays**: `[]` is different from `null`
473473+ ```ocaml
474474+ parse_array_opt (* Returns None for null, Some [] for [] *)
475475+ ```
476476+477477+4. **Number types**: JSON doesn't distinguish int/float
478478+ ```ocaml
479479+ | `Float f -> int_of_float f
480480+ | `Int i -> i
481481+ ```
482482+483483+5. **Boolean maps**: Many fields are `Id[Boolean]`
484484+ ```ocaml
485485+ mailbox_ids = parse_map (fun _ -> true) field
486486+ ```
487487+488488+## Getting Help
489489+490490+1. **Check test files**: They contain the exact JSON structure
491491+2. **Look at existing parsers**: Id and primitives are complete
492492+3. **Use the helpers**: They handle most common cases
493493+4. **Follow the types**: Type errors will guide you
494494+495495+## Success Criteria
496496+497497+Parser implementation is complete when:
498498+499499+- [ ] All test files parse without errors
500500+- [ ] All required fields are extracted
501501+- [ ] Optional fields handled correctly
502502+- [ ] Round-trip works (parse -> serialize -> parse)
503503+- [ ] All 50 test files pass
504504+- [ ] No TODO comments remain in parser code
505505+506506+Good luck! Start simple and build up to the complex types. The type system will guide you.
+64
jmap/TESTING_STATUS.md
···11+# JMAP Testing Status
22+33+## Current Status
44+55+### ✅ Completed
66+- Session parsing (jmap-core/jmap_session.ml)
77+- Request parsing and serialization (jmap-core/jmap_request.ml)
88+- Invocation parsing and serialization (jmap-core/jmap_invocation.ml)
99+- JMAP client with Eio integration (jmap-client/)
1010+- API key configuration and loading
1111+1212+### ⚠️ Known Issue: TLS Connection Reuse
1313+1414+**Problem**: The Requests library has a bug where making multiple HTTPS requests with the same Requests instance causes a TLS error on the second request:
1515+```
1616+Fatal error: exception TLS failure: unexpected: application data
1717+```
1818+1919+**Reproduction**:
2020+```ocaml
2121+let requests = Requests.create ~sw env in
2222+let resp1 = Requests.get requests "https://api.fastmail.com/jmap/session" in
2323+(* Drain body *)
2424+let resp2 = Requests.get requests "https://api.fastmail.com/jmap/session" in
2525+(* ^ Fails with TLS error *)
2626+```
2727+2828+**Impact**: The first HTTP request (session fetch) works fine, but any subsequent requests fail.
2929+3030+**Root Cause**: Issue in Requests library's connection pooling or TLS state management when reusing connections.
3131+3232+**Workaround Options**:
3333+1. Create a new Requests instance for each request (inefficient)
3434+2. Fix the Requests library's TLS connection handling
3535+3. Disable connection pooling if that option exists
3636+3737+**Test Case**: `jmap/test/test_simple_https.ml` demonstrates the issue
3838+3939+## Test Results
4040+4141+### test_fastmail.exe
4242+- ✅ Session parsing works
4343+- ✅ First HTTPS request succeeds
4444+- ❌ Second HTTPS request fails with TLS error
4545+- Status: **Blocked on Requests library bug**
4646+4747+### What Works
4848+- Eio integration ✅
4949+- Session fetching and parsing ✅
5050+- Request building ✅
5151+- JSON serialization/deserialization ✅
5252+- API key loading ✅
5353+- Authentication headers ✅
5454+5555+### What's Blocked
5656+- Making JMAP API calls (requires multiple HTTPS requests)
5757+- Email querying
5858+- Full end-to-end testing
5959+6060+## Next Steps
6161+6262+1. Fix TLS connection reuse in Requests library
6363+2. Implement Response.Parser.of_json once requests work
6464+3. Complete end-to-end test with email querying
+169
jmap/USAGE_GUIDE.md
···11+# JMAP Library Usage Guide
22+33+## Ergonomic API Design
44+55+The JMAP library provides a clean, ergonomic API with short module names and a unified entry point.
66+77+## Module Structure
88+99+### Unified `Jmap` Module (Recommended)
1010+1111+The unified `Jmap` module combines `jmap-core`, `jmap-mail`, and `jmap-client` into a single, easy-to-use interface.
1212+1313+```ocaml
1414+let id = Jmap.Id.of_string "abc123"
1515+let email_req = Jmap.Email.Query.request_v ~account_id ...
1616+let client = Jmap.Client.create ...
1717+```
1818+1919+### Submodules (For Specialized Use)
2020+2121+You can also use the submodules directly:
2222+2323+**Jmap_core**:
2424+```ocaml
2525+Jmap_core.Session.t
2626+Jmap_core.Id.of_string
2727+Jmap_core.Request.make
2828+```
2929+3030+**Jmap_mail**:
3131+```ocaml
3232+Jmap_mail.Email.Query.request_v
3333+Jmap_mail.Mailbox.get
3434+```
3535+3636+## Module Hierarchy
3737+3838+### High-Level API (Recommended for Most Users)
3939+4040+```
4141+Jmap -- Unified interface (START HERE)
4242+├── Client -- HTTP client (from jmap-client)
4343+├── Connection -- Connection config (from jmap-client)
4444+│
4545+├── Email -- Email operations (from jmap-mail)
4646+├── Mailbox -- Mailbox operations (from jmap-mail)
4747+├── Thread -- Thread operations (from jmap-mail)
4848+├── Identity -- Identity management (from jmap-mail)
4949+├── Email_submission -- Email submission (from jmap-mail)
5050+├── Vacation_response -- Vacation responses (from jmap-mail)
5151+├── Search_snippet -- Search snippets (from jmap-mail)
5252+│
5353+├── Session -- JMAP Session (from jmap-core)
5454+├── Request -- Request building (from jmap-core)
5555+├── Response -- Response handling (from jmap-core)
5656+├── Invocation -- Method invocations (from jmap-core)
5757+├── Id -- JMAP IDs (from jmap-core)
5858+├── Capability -- Capabilities (from jmap-core)
5959+├── Filter -- Filters (from jmap-core)
6060+├── Comparator -- Sorting (from jmap-core)
6161+├── Primitives -- Primitive types (from jmap-core)
6262+├── Error -- Error handling (from jmap-core)
6363+├── Binary -- Upload/download (from jmap-core)
6464+├── Push -- Push notifications (from jmap-core)
6565+│
6666+├── Core -- Full jmap-core access
6767+└── Mail -- Full jmap-mail access
6868+```
6969+7070+### Specialized APIs (For Advanced Use Cases)
7171+7272+```
7373+Jmap_core -- Core protocol library
7474+├── Session
7575+├── Id
7676+├── Request
7777+├── Response
7878+└── ...
7979+8080+Jmap_mail -- Mail extension library
8181+├── Email
8282+├── Mailbox
8383+├── Thread
8484+└── ...
8585+8686+Jmap_client -- HTTP client library
8787+└── (unwrapped: Jmap_client, Jmap_connection)
8888+```
8989+9090+## Usage Examples
9191+9292+### Example 1: Creating a Client and Querying Emails
9393+9494+```ocaml
9595+let conn = Jmap.Connection.bearer_auth ~token:"..." () in
9696+let client = Jmap.Client.create ~sw ~env ~conn ~session_url:"..." () in
9797+let session = Jmap.Client.get_session client in
9898+9999+let query_req = Jmap.Email.Query.request_v
100100+ ~account_id:(Jmap.Id.of_string account_id)
101101+ ~limit:(Jmap.Primitives.UnsignedInt.of_int 10)
102102+ ~sort:[Jmap.Comparator.v ~property:"receivedAt" ~is_ascending:false ()]
103103+ ()
104104+in
105105+106106+let query_args = Jmap.Email.Query.request_to_json query_req in
107107+let invocation = Jmap.Invocation.Invocation {
108108+ method_name = "Email/query";
109109+ arguments = query_args;
110110+ call_id = "q1";
111111+ witness = Jmap.Invocation.Echo;
112112+} in
113113+114114+let req = Jmap.Request.make
115115+ ~using:[Jmap.Capability.core; Jmap.Capability.mail]
116116+ [Jmap.Invocation.Packed invocation]
117117+in
118118+119119+let resp = Jmap.Client.call client req in
120120+```
121121+122122+### Example 2: Using Submodules
123123+124124+```ocaml
125125+(* Use Jmap_core for core protocol operations *)
126126+let session = Jmap_core.Session.of_json json in
127127+let account_id = Jmap_core.Id.of_string "abc123" in
128128+129129+(* Use Jmap_mail for mail-specific operations *)
130130+let email_req = Jmap_mail.Email.Query.request_v
131131+ ~account_id
132132+ ~limit:(Jmap_core.Primitives.UnsignedInt.of_int 50)
133133+ ()
134134+in
135135+```
136136+137137+### Example 3: Working with IDs and Primitives
138138+139139+```ocaml
140140+let account_id = Jmap.Id.of_string "abc123" in
141141+let limit = Jmap.Primitives.UnsignedInt.of_int 50 in
142142+let id_str = Jmap.Id.to_string account_id in
143143+```
144144+145145+## Package Structure
146146+147147+- **`jmap`** - Unified interface (recommended for applications)
148148+- **`jmap-core`** - Core protocol (RFC 8620)
149149+- **`jmap-mail`** - Mail extension (RFC 8621)
150150+- **`jmap-client`** - HTTP client implementation
151151+- **`jmap-test`** - Test suite
152152+153153+Most users should depend on `jmap`, which pulls in all three libraries. For specialized use cases (e.g., you only need parsing), you can depend on individual packages.
154154+155155+## Quick Reference
156156+157157+| Use Case | Unified API | Submodule API |
158158+|----------|-------------|---------------|
159159+| IDs | `Jmap.Id` | `Jmap_core.Id` |
160160+| Requests | `Jmap.Request` | `Jmap_core.Request` |
161161+| Emails | `Jmap.Email` | `Jmap_mail.Email` |
162162+| Mailboxes | `Jmap.Mailbox` | `Jmap_mail.Mailbox` |
163163+| Client | `Jmap.Client` | `Jmap_client` |
164164+165165+## Need Help?
166166+167167+- See `jmap/lib/jmap.mli` for the complete unified API documentation
168168+- Check `jmap/test/test_unified_api.ml` for working examples
169169+- Refer to `jmap/test/test_fastmail.ml` for real-world usage
···11+(** JMAP Binary Data Operations
22+33+ Binary data (files, attachments) is handled separately from JMAP API calls
44+ through upload and download endpoints.
55+66+ Reference: RFC 8620 Section 6
77+*)
88+99+(** Upload response from POST to upload endpoint *)
1010+module Upload = struct
1111+ type t = {
1212+ account_id : Jmap_id.t;
1313+ blob_id : Jmap_id.t;
1414+ content_type : string;
1515+ size : Jmap_primitives.UnsignedInt.t;
1616+ }
1717+1818+ (** Accessors *)
1919+ let account_id t = t.account_id
2020+ let blob_id t = t.blob_id
2121+ let content_type t = t.content_type
2222+ let size t = t.size
2323+2424+ (** Constructor *)
2525+ let v ~account_id ~blob_id ~content_type ~size =
2626+ { account_id; blob_id; content_type; size }
2727+2828+ (** Parse upload response from JSON *)
2929+ let of_json _json =
3030+ (* TODO: Implement JSON parsing *)
3131+ raise (Jmap_error.Parse_error "Upload.of_json not yet implemented")
3232+end
3333+3434+(** Blob/copy method for copying blobs between accounts *)
3535+module BlobCopy = struct
3636+ type request = {
3737+ from_account_id : Jmap_id.t;
3838+ account_id : Jmap_id.t;
3939+ blob_ids : Jmap_id.t list;
4040+ }
4141+4242+ type response = {
4343+ from_account_id : Jmap_id.t;
4444+ account_id : Jmap_id.t;
4545+ copied : (Jmap_id.t * Jmap_id.t) list option; (** old id -> new id *)
4646+ not_copied : (Jmap_id.t * Jmap_error.set_error_detail) list option;
4747+ }
4848+4949+ (** Accessors for request *)
5050+ let from_account_id (r : request) = r.from_account_id
5151+ let account_id (r : request) = r.account_id
5252+ let blob_ids (r : request) = r.blob_ids
5353+5454+ (** Constructor for request *)
5555+ let request_v ~from_account_id ~account_id ~blob_ids =
5656+ { from_account_id; account_id; blob_ids }
5757+5858+ (** Accessors for response *)
5959+ let response_from_account_id (r : response) = r.from_account_id
6060+ let response_account_id (r : response) = r.account_id
6161+ let copied (r : response) = r.copied
6262+ let not_copied (r : response) = r.not_copied
6363+6464+ (** Constructor for response *)
6565+ let response_v ~from_account_id ~account_id ?copied ?not_copied () =
6666+ { from_account_id; account_id; copied; not_copied }
6767+6868+ let request_of_json _json =
6969+ (* TODO: Implement JSON parsing *)
7070+ raise (Jmap_error.Parse_error "BlobCopy.request_of_json not yet implemented")
7171+7272+ let response_of_json _json =
7373+ (* TODO: Implement JSON parsing *)
7474+ raise (Jmap_error.Parse_error "BlobCopy.response_of_json not yet implemented")
7575+end
+75
jmap/jmap-core/jmap_binary.mli
···11+(** JMAP Binary Data Operations *)
22+33+(** Upload response from POST to upload endpoint *)
44+module Upload : sig
55+ type t = {
66+ account_id : Jmap_id.t;
77+ blob_id : Jmap_id.t;
88+ content_type : string;
99+ size : Jmap_primitives.UnsignedInt.t;
1010+ }
1111+1212+ (** Accessors *)
1313+ val account_id : t -> Jmap_id.t
1414+ val blob_id : t -> Jmap_id.t
1515+ val content_type : t -> string
1616+ val size : t -> Jmap_primitives.UnsignedInt.t
1717+1818+ (** Constructor *)
1919+ val v :
2020+ account_id:Jmap_id.t ->
2121+ blob_id:Jmap_id.t ->
2222+ content_type:string ->
2323+ size:Jmap_primitives.UnsignedInt.t ->
2424+ t
2525+2626+ (** Parse upload response from JSON *)
2727+ val of_json : Ezjsonm.value -> t
2828+end
2929+3030+(** Blob/copy method for copying blobs between accounts *)
3131+module BlobCopy : sig
3232+ type request = {
3333+ from_account_id : Jmap_id.t;
3434+ account_id : Jmap_id.t;
3535+ blob_ids : Jmap_id.t list;
3636+ }
3737+3838+ type response = {
3939+ from_account_id : Jmap_id.t;
4040+ account_id : Jmap_id.t;
4141+ copied : (Jmap_id.t * Jmap_id.t) list option;
4242+ not_copied : (Jmap_id.t * Jmap_error.set_error_detail) list option;
4343+ }
4444+4545+ (** Accessors for request *)
4646+ val from_account_id : request -> Jmap_id.t
4747+ val account_id : request -> Jmap_id.t
4848+ val blob_ids : request -> Jmap_id.t list
4949+5050+ (** Constructor for request *)
5151+ val request_v :
5252+ from_account_id:Jmap_id.t ->
5353+ account_id:Jmap_id.t ->
5454+ blob_ids:Jmap_id.t list ->
5555+ request
5656+5757+ (** Accessors for response *)
5858+ val response_from_account_id : response -> Jmap_id.t
5959+ val response_account_id : response -> Jmap_id.t
6060+ val copied : response -> (Jmap_id.t * Jmap_id.t) list option
6161+ val not_copied : response -> (Jmap_id.t * Jmap_error.set_error_detail) list option
6262+6363+ (** Constructor for response *)
6464+ val response_v :
6565+ from_account_id:Jmap_id.t ->
6666+ account_id:Jmap_id.t ->
6767+ ?copied:(Jmap_id.t * Jmap_id.t) list ->
6868+ ?not_copied:(Jmap_id.t * Jmap_error.set_error_detail) list ->
6969+ unit ->
7070+ response
7171+7272+ val request_of_json : Ezjsonm.value -> request
7373+7474+ val response_of_json : Ezjsonm.value -> response
7575+end
+114
jmap/jmap-core/jmap_capability.ml
···11+(** JMAP Capability URNs
22+33+ Capabilities define which parts of JMAP are supported.
44+ They appear in the Session object's "capabilities" property
55+ and in Request "using" arrays.
66+77+ Reference: RFC 8620 Section 2
88+ Test files: test/data/core/session.json (capabilities field)
99+*)
1010+1111+(** Abstract type for capability URNs *)
1212+type t = string
1313+1414+(** Core JMAP capability (RFC 8620) *)
1515+let core = "urn:ietf:params:jmap:core"
1616+1717+(** JMAP Mail capability (RFC 8621) *)
1818+let mail = "urn:ietf:params:jmap:mail"
1919+2020+(** JMAP Mail submission capability (RFC 8621) *)
2121+let submission = "urn:ietf:params:jmap:submission"
2222+2323+(** JMAP Vacation response capability (RFC 8621) *)
2424+let vacation_response = "urn:ietf:params:jmap:vacationresponse"
2525+2626+(** Create a capability from a URN string *)
2727+let of_string s = s
2828+2929+(** Convert capability to URN string *)
3030+let to_string t = t
3131+3232+(** Parse from JSON *)
3333+let of_json = function
3434+ | `String s -> of_string s
3535+ | _ -> raise (Jmap_error.Parse_error "Capability must be a JSON string")
3636+3737+(** Convert to JSON *)
3838+let to_json t = `String t
3939+4040+(** Check if a capability is supported *)
4141+let is_supported t =
4242+ t = core || t = mail || t = submission || t = vacation_response
4343+4444+module CoreCapability = struct
4545+ (** Core capability properties (RFC 8620 Section 2) *)
4646+ type t = {
4747+ max_size_upload : int;
4848+ max_concurrent_upload : int;
4949+ max_size_request : int;
5050+ max_concurrent_requests : int;
5151+ max_calls_in_request : int;
5252+ max_objects_in_get : int;
5353+ max_objects_in_set : int;
5454+ collation_algorithms : string list;
5555+ }
5656+5757+ (** Accessors *)
5858+ let max_size_upload t = t.max_size_upload
5959+ let max_concurrent_upload t = t.max_concurrent_upload
6060+ let max_size_request t = t.max_size_request
6161+ let max_concurrent_requests t = t.max_concurrent_requests
6262+ let max_calls_in_request t = t.max_calls_in_request
6363+ let max_objects_in_get t = t.max_objects_in_get
6464+ let max_objects_in_set t = t.max_objects_in_set
6565+ let collation_algorithms t = t.collation_algorithms
6666+6767+ (** Constructor *)
6868+ let v ~max_size_upload ~max_concurrent_upload ~max_size_request ~max_concurrent_requests ~max_calls_in_request ~max_objects_in_get ~max_objects_in_set ~collation_algorithms =
6969+ { max_size_upload; max_concurrent_upload; max_size_request; max_concurrent_requests; max_calls_in_request; max_objects_in_get; max_objects_in_set; collation_algorithms }
7070+7171+ (** Parse from JSON.
7272+ Test files: test/data/core/session.json *)
7373+ let of_json _json =
7474+ (* TODO: Implement JSON parsing *)
7575+ raise (Jmap_error.Parse_error "CoreCapability.of_json not yet implemented")
7676+7777+ let to_json _t =
7878+ (* TODO: Implement JSON serialization *)
7979+ raise (Jmap_error.Parse_error "CoreCapability.to_json not yet implemented")
8080+end
8181+8282+module MailCapability = struct
8383+ (** Mail capability properties (RFC 8621 Section 1.1) *)
8484+ type t = {
8585+ max_mailboxes_per_email : int option;
8686+ max_mailbox_depth : int option;
8787+ max_size_mailbox_name : int;
8888+ max_size_attachments_per_email : int;
8989+ email_query_sort_options : string list;
9090+ may_create_top_level_mailbox : bool;
9191+ }
9292+9393+ (** Accessors *)
9494+ let max_mailboxes_per_email t = t.max_mailboxes_per_email
9595+ let max_mailbox_depth t = t.max_mailbox_depth
9696+ let max_size_mailbox_name t = t.max_size_mailbox_name
9797+ let max_size_attachments_per_email t = t.max_size_attachments_per_email
9898+ let email_query_sort_options t = t.email_query_sort_options
9999+ let may_create_top_level_mailbox t = t.may_create_top_level_mailbox
100100+101101+ (** Constructor *)
102102+ let v ?max_mailboxes_per_email ?max_mailbox_depth ~max_size_mailbox_name ~max_size_attachments_per_email ~email_query_sort_options ~may_create_top_level_mailbox () =
103103+ { max_mailboxes_per_email; max_mailbox_depth; max_size_mailbox_name; max_size_attachments_per_email; email_query_sort_options; may_create_top_level_mailbox }
104104+105105+ (** Parse from JSON.
106106+ Test files: test/data/core/session.json *)
107107+ let of_json _json =
108108+ (* TODO: Implement JSON parsing *)
109109+ raise (Jmap_error.Parse_error "MailCapability.of_json not yet implemented")
110110+111111+ let to_json _t =
112112+ (* TODO: Implement JSON serialization *)
113113+ raise (Jmap_error.Parse_error "MailCapability.to_json not yet implemented")
114114+end
+96
jmap/jmap-core/jmap_capability.mli
···11+(** JMAP Capability URNs
22+33+ Capabilities define which parts of JMAP are supported.
44+55+ Reference: RFC 8620 Section 2
66+ Test files: test/data/core/session.json
77+*)
88+99+(** Abstract capability URN type *)
1010+type t
1111+1212+(** {1 Standard Capabilities} *)
1313+1414+val core : t
1515+val mail : t
1616+val submission : t
1717+val vacation_response : t
1818+1919+(** {1 Constructors} *)
2020+2121+val of_string : string -> t
2222+val to_string : t -> string
2323+2424+(** {1 Validation} *)
2525+2626+val is_supported : t -> bool
2727+2828+(** {1 JSON Conversion} *)
2929+3030+val of_json : Ezjsonm.value -> t
3131+val to_json : t -> Ezjsonm.value
3232+3333+(** Core capability properties *)
3434+module CoreCapability : sig
3535+ type t
3636+3737+ (** {1 Accessors} *)
3838+3939+ val max_size_upload : t -> int
4040+ val max_concurrent_upload : t -> int
4141+ val max_size_request : t -> int
4242+ val max_concurrent_requests : t -> int
4343+ val max_calls_in_request : t -> int
4444+ val max_objects_in_get : t -> int
4545+ val max_objects_in_set : t -> int
4646+ val collation_algorithms : t -> string list
4747+4848+ (** {1 Constructor} *)
4949+5050+ val v :
5151+ max_size_upload:int ->
5252+ max_concurrent_upload:int ->
5353+ max_size_request:int ->
5454+ max_concurrent_requests:int ->
5555+ max_calls_in_request:int ->
5656+ max_objects_in_get:int ->
5757+ max_objects_in_set:int ->
5858+ collation_algorithms:string list ->
5959+ t
6060+6161+ (** {1 JSON Conversion} *)
6262+6363+ val of_json : Ezjsonm.value -> t
6464+ val to_json : t -> Ezjsonm.value
6565+end
6666+6767+(** Mail capability properties *)
6868+module MailCapability : sig
6969+ type t
7070+7171+ (** {1 Accessors} *)
7272+7373+ val max_mailboxes_per_email : t -> int option
7474+ val max_mailbox_depth : t -> int option
7575+ val max_size_mailbox_name : t -> int
7676+ val max_size_attachments_per_email : t -> int
7777+ val email_query_sort_options : t -> string list
7878+ val may_create_top_level_mailbox : t -> bool
7979+8080+ (** {1 Constructor} *)
8181+8282+ val v :
8383+ ?max_mailboxes_per_email:int ->
8484+ ?max_mailbox_depth:int ->
8585+ max_size_mailbox_name:int ->
8686+ max_size_attachments_per_email:int ->
8787+ email_query_sort_options:string list ->
8888+ may_create_top_level_mailbox:bool ->
8989+ unit ->
9090+ t
9191+9292+ (** {1 JSON Conversion} *)
9393+9494+ val of_json : Ezjsonm.value -> t
9595+ val to_json : t -> Ezjsonm.value
9696+end
+70
jmap/jmap-core/jmap_comparator.ml
···11+(** JMAP Comparator for Sorting
22+33+ Comparators define how to sort query results.
44+ Multiple comparators can be chained for multi-level sorting.
55+66+ Reference: RFC 8620 Section 5.5
77+ Test files: test/data/core/request_query.json (sort field)
88+*)
99+1010+(** Comparator type for sorting *)
1111+type t = {
1212+ property : string; (** Property name to sort by *)
1313+ is_ascending : bool; (** true = ascending, false = descending *)
1414+ collation : string option; (** Collation algorithm (optional) *)
1515+}
1616+1717+(** Accessors *)
1818+let property t = t.property
1919+let is_ascending t = t.is_ascending
2020+let collation t = t.collation
2121+2222+(** Constructor *)
2323+let v ?(is_ascending=true) ?collation ~property () =
2424+ { property; is_ascending; collation }
2525+2626+(** Create a comparator *)
2727+let make ?(is_ascending=true) ?collation property =
2828+ { property; is_ascending; collation }
2929+3030+(** Parse from JSON.
3131+ Expected JSON: {
3232+ "property": "name",
3333+ "isAscending": true,
3434+ "collation": "i;unicode-casemap"
3535+ }
3636+3737+ Test files: test/data/core/request_query.json
3838+*)
3939+let of_json json =
4040+ match json with
4141+ | `O fields ->
4242+ let property = match List.assoc_opt "property" fields with
4343+ | Some (`String s) -> s
4444+ | Some _ -> raise (Jmap_error.Parse_error "Comparator property must be a string")
4545+ | None -> raise (Jmap_error.Parse_error "Comparator requires 'property' field")
4646+ in
4747+ let is_ascending = match List.assoc_opt "isAscending" fields with
4848+ | Some (`Bool b) -> b
4949+ | Some _ -> raise (Jmap_error.Parse_error "Comparator isAscending must be a boolean")
5050+ | None -> true (* Default: ascending *)
5151+ in
5252+ let collation = match List.assoc_opt "collation" fields with
5353+ | Some (`String s) -> Some s
5454+ | Some _ -> raise (Jmap_error.Parse_error "Comparator collation must be a string")
5555+ | None -> None
5656+ in
5757+ { property; is_ascending; collation }
5858+ | _ -> raise (Jmap_error.Parse_error "Comparator must be a JSON object")
5959+6060+(** Convert to JSON *)
6161+let to_json t =
6262+ let fields = [
6363+ ("property", `String t.property);
6464+ ("isAscending", `Bool t.is_ascending);
6565+ ] in
6666+ let fields = match t.collation with
6767+ | Some c -> ("collation", `String c) :: fields
6868+ | None -> fields
6969+ in
7070+ `O fields
+35
jmap/jmap-core/jmap_comparator.mli
···11+(** JMAP Comparator for Sorting
22+33+ Reference: RFC 8620 Section 5.5
44+ Test files: test/data/core/request_query.json
55+*)
66+77+(** Comparator type *)
88+type t
99+1010+(** {1 Accessors} *)
1111+1212+val property : t -> string
1313+val is_ascending : t -> bool
1414+val collation : t -> string option
1515+1616+(** {1 Constructor} *)
1717+1818+val v :
1919+ ?is_ascending:bool ->
2020+ ?collation:string ->
2121+ property:string ->
2222+ unit ->
2323+ t
2424+2525+(** Alias for constructor *)
2626+val make :
2727+ ?is_ascending:bool ->
2828+ ?collation:string ->
2929+ string ->
3030+ t
3131+3232+(** {1 JSON Conversion} *)
3333+3434+val of_json : Ezjsonm.value -> t
3535+val to_json : t -> Ezjsonm.value
···11+(** JMAP Filter Operations
22+33+ Filters are used in query methods to select which objects to return.
44+ They support AND, OR, and NOT operators for complex queries.
55+66+ Reference: RFC 8620 Section 5.5
77+ Test files: test/data/core/request_query.json (filter field)
88+*)
99+1010+(** Filter operator type *)
1111+type operator =
1212+ | AND (** All conditions must match *)
1313+ | OR (** At least one condition must match *)
1414+ | NOT (** Condition must not match *)
1515+1616+(** Filter structure - can be either an operator or a condition.
1717+ This is a recursive type that allows complex nested filters.
1818+1919+ FilterOperator MUST have an "operator" property.
2020+ FilterCondition MUST NOT have an "operator" property.
2121+*)
2222+type 'condition t =
2323+ | Operator of operator * 'condition t list (** Nested filter with operator *)
2424+ | Condition of 'condition (** Leaf condition (type-specific) *)
2525+2626+(** Convert operator to string *)
2727+let operator_to_string = function
2828+ | AND -> "AND"
2929+ | OR -> "OR"
3030+ | NOT -> "NOT"
3131+3232+(** Convert string to operator *)
3333+let operator_of_string = function
3434+ | "AND" -> AND
3535+ | "OR" -> OR
3636+ | "NOT" -> NOT
3737+ | s -> raise (Jmap_error.Parse_error (Printf.sprintf "Unknown filter operator: %s" s))
3838+3939+(** Parse operator from JSON *)
4040+let operator_of_json = function
4141+ | `String s -> operator_of_string s
4242+ | _ -> raise (Jmap_error.Parse_error "Filter operator must be a string")
4343+4444+(** Parse filter from JSON.
4545+ Requires a parser function for the condition type.
4646+4747+ Test files: test/data/core/request_query.json (complex AND/OR filters)
4848+*)
4949+let rec of_json parse_condition json =
5050+ match json with
5151+ | `O fields ->
5252+ (* Check if this is an operator or condition *)
5353+ begin match List.assoc_opt "operator" fields with
5454+ | Some op_json ->
5555+ (* This is a FilterOperator *)
5656+ let op = operator_of_json op_json in
5757+ let conditions_json = match List.assoc_opt "conditions" fields with
5858+ | Some (`A conds) -> conds
5959+ | Some _ -> raise (Jmap_error.Parse_error "FilterOperator conditions must be an array")
6060+ | None -> raise (Jmap_error.Parse_error "FilterOperator requires 'conditions' field")
6161+ in
6262+ let conditions = List.map (of_json parse_condition) conditions_json in
6363+ Operator (op, conditions)
6464+ | None ->
6565+ (* This is a FilterCondition *)
6666+ Condition (parse_condition json)
6767+ end
6868+ | _ -> raise (Jmap_error.Parse_error "Filter must be a JSON object")
6969+7070+(** Convert filter to JSON *)
7171+let rec to_json condition_to_json = function
7272+ | Operator (op, conditions) ->
7373+ `O [
7474+ ("operator", `String (operator_to_string op));
7575+ ("conditions", `A (List.map (to_json condition_to_json) conditions));
7676+ ]
7777+ | Condition cond ->
7878+ condition_to_json cond
7979+8080+(** {1 Filter Constructors} *)
8181+8282+(** Create an AND filter *)
8383+let and_ conditions = Operator (AND, conditions)
8484+8585+(** Create an OR filter *)
8686+let or_ conditions = Operator (OR, conditions)
8787+8888+(** Create a NOT filter *)
8989+let not_ condition = Operator (NOT, [condition])
9090+9191+(** Create a condition filter *)
9292+let condition cond = Condition cond
+36
jmap/jmap-core/jmap_filter.mli
···11+(** JMAP Filter Operations
22+33+ Filters support AND, OR, and NOT operators for complex queries.
44+55+ Reference: RFC 8620 Section 5.5
66+ Test files: test/data/core/request_query.json
77+*)
88+99+(** Filter operator type *)
1010+type operator = AND | OR | NOT
1111+1212+(** Filter structure parameterized by condition type *)
1313+type 'condition t =
1414+ | Operator of operator * 'condition t list
1515+ | Condition of 'condition
1616+1717+(** {1 Operator Conversion} *)
1818+1919+val operator_to_string : operator -> string
2020+val operator_of_string : string -> operator
2121+val operator_of_json : Ezjsonm.value -> operator
2222+2323+(** {1 Filter Constructors} *)
2424+2525+val and_ : 'condition t list -> 'condition t
2626+val or_ : 'condition t list -> 'condition t
2727+val not_ : 'condition t -> 'condition t
2828+val condition : 'condition -> 'condition t
2929+3030+(** {1 JSON Conversion} *)
3131+3232+(** Parse filter from JSON using a condition parser *)
3333+val of_json : (Ezjsonm.value -> 'condition) -> Ezjsonm.value -> 'condition t
3434+3535+(** Convert filter to JSON using a condition serializer *)
3636+val to_json : ('condition -> Ezjsonm.value) -> 'condition t -> Ezjsonm.value
+48
jmap/jmap-core/jmap_id.ml
···11+(** JMAP Id Type
22+33+ The Id data type is used for all object ids throughout JMAP.
44+ It is a string with minimum length 1 and maximum length 255 characters.
55+66+ Reference: RFC 8620 Section 1.2
77+*)
88+99+(** Abstract type for JMAP identifiers *)
1010+type t = string
1111+1212+(** Create an Id from a string.
1313+ @raise Invalid_argument if the string is empty or longer than 255 chars *)
1414+let of_string s =
1515+ let len = String.length s in
1616+ if len = 0 then
1717+ raise (Invalid_argument "Id cannot be empty")
1818+ else if len > 255 then
1919+ raise (Invalid_argument "Id cannot be longer than 255 characters")
2020+ else
2121+ s
2222+2323+(** Convert an Id to a string *)
2424+let to_string t = t
2525+2626+(** Parse an Id from JSON.
2727+ Expected JSON: string
2828+2929+ Test files:
3030+ - test/data/core/request_get.json (ids field)
3131+ - test/data/mail/mailbox_get_request.json (accountId, ids)
3232+*)
3333+let of_json json =
3434+ match json with
3535+ | `String s -> of_string s
3636+ | _ -> raise (Jmap_error.Parse_error "Id must be a JSON string")
3737+3838+(** Convert an Id to JSON *)
3939+let to_json t = `String t
4040+4141+(** Compare two Ids for equality *)
4242+let equal (t1 : t) (t2 : t) = String.equal t1 t2
4343+4444+(** Compare two Ids *)
4545+let compare (t1 : t) (t2 : t) = String.compare t1 t2
4646+4747+(** Hash an Id *)
4848+let hash (t : t) = Hashtbl.hash t
+40
jmap/jmap-core/jmap_id.mli
···11+(** JMAP Id Type
22+33+ Abstract type for JMAP identifiers (1-255 character strings).
44+55+ Reference: RFC 8620 Section 1.2
66+ Test files: All files with "id", "accountId", etc. fields
77+*)
88+99+(** Abstract identifier type *)
1010+type t
1111+1212+(** {1 Constructors} *)
1313+1414+(** Create an Id from a string.
1515+ @raise Invalid_argument if the string is empty or longer than 255 chars *)
1616+val of_string : string -> t
1717+1818+(** {1 Accessors} *)
1919+2020+(** Convert an Id to a string *)
2121+val to_string : t -> string
2222+2323+(** {1 Comparison} *)
2424+2525+(** Compare two Ids for equality *)
2626+val equal : t -> t -> bool
2727+2828+(** Compare two Ids *)
2929+val compare : t -> t -> int
3030+3131+(** Hash an Id *)
3232+val hash : t -> int
3333+3434+(** {1 JSON Conversion} *)
3535+3636+(** Parse an Id from JSON *)
3737+val of_json : Ezjsonm.value -> t
3838+3939+(** Convert an Id to JSON *)
4040+val to_json : t -> Ezjsonm.value
+207
jmap/jmap-core/jmap_invocation.ml
···11+(** JMAP Invocation with Type-Safe Method Dispatch
22+33+ Invocations use GADTs to ensure compile-time type safety between
44+ method calls and their responses.
55+66+ An Invocation is a 3-tuple: [method_name, arguments, call_id]
77+88+ Reference: RFC 8620 Section 3.2
99+ Test files: test/data/core/request_echo.json (methodCalls field)
1010+*)
1111+1212+(** Method witness type - encodes the relationship between
1313+ method names and their argument/response types.
1414+1515+ This GADT ensures that for each method, we know:
1616+ - What type the arguments should have
1717+ - What type the response will have
1818+*)
1919+type ('args, 'resp) method_witness =
2020+ (* Core methods *)
2121+ | Echo : (Ezjsonm.value, Ezjsonm.value) method_witness
2222+2323+ (* Standard methods - polymorphic over object type *)
2424+ | Get : string -> ('a Jmap_standard_methods.Get.request, 'a Jmap_standard_methods.Get.response) method_witness
2525+ | Changes : string -> (Jmap_standard_methods.Changes.request, Jmap_standard_methods.Changes.response) method_witness
2626+ | Set : string -> ('a Jmap_standard_methods.Set.request, 'a Jmap_standard_methods.Set.response) method_witness
2727+ | Copy : string -> ('a Jmap_standard_methods.Copy.request, 'a Jmap_standard_methods.Copy.response) method_witness
2828+ | Query : string -> ('f Jmap_standard_methods.Query.request, Jmap_standard_methods.Query.response) method_witness
2929+ | QueryChanges : string -> ('f Jmap_standard_methods.QueryChanges.request, Jmap_standard_methods.QueryChanges.response) method_witness
3030+3131+(** Type-safe invocation pairing method name with typed arguments *)
3232+type _ invocation =
3333+ | Invocation : {
3434+ method_name : string;
3535+ arguments : 'args;
3636+ call_id : string;
3737+ witness : ('args, 'resp) method_witness;
3838+ } -> 'resp invocation
3939+4040+(** Existential wrapper for heterogeneous invocation lists *)
4141+type packed_invocation =
4242+ | Packed : 'resp invocation -> packed_invocation
4343+4444+(** Heterogeneous list of invocations (for Request.method_calls) *)
4545+type invocation_list = packed_invocation list
4646+4747+(** Response invocation - pairs method name with typed response *)
4848+type _ response_invocation =
4949+ | ResponseInvocation : {
5050+ method_name : string;
5151+ response : 'resp;
5252+ call_id : string;
5353+ witness : ('args, 'resp) method_witness;
5454+ } -> 'resp response_invocation
5555+5656+(** Packed response invocation *)
5757+type packed_response =
5858+ | PackedResponse : 'resp response_invocation -> packed_response
5959+6060+(** Heterogeneous list of responses (for Response.method_responses) *)
6161+type response_list = packed_response list
6262+6363+(** Error response *)
6464+type error_response = {
6565+ error_type : Jmap_error.method_error;
6666+ call_id : string;
6767+}
6868+6969+(** Response can be either success or error *)
7070+type method_response =
7171+ | Success of packed_response
7272+ | Error of error_response
7373+7474+(** Get method name from witness *)
7575+let method_name_of_witness : type a r. (a, r) method_witness -> string = function
7676+ | Echo -> "Core/echo"
7777+ | Get typ -> typ ^ "/get"
7878+ | Changes typ -> typ ^ "/changes"
7979+ | Set typ -> typ ^ "/set"
8080+ | Copy typ -> typ ^ "/copy"
8181+ | Query typ -> typ ^ "/query"
8282+ | QueryChanges typ -> typ ^ "/queryChanges"
8383+8484+(** Parse method name and return appropriate witness *)
8585+let witness_of_method_name name : packed_invocation =
8686+ (* Extract type name from method *)
8787+ match String.split_on_char '/' name with
8888+ | ["Core"; "echo"] ->
8989+ Packed (Invocation {
9090+ method_name = name;
9191+ arguments = `Null; (* Placeholder *)
9292+ call_id = ""; (* Will be filled in *)
9393+ witness = Echo;
9494+ })
9595+ | [typ; "get"] ->
9696+ Packed (Invocation {
9797+ method_name = name;
9898+ arguments = Jmap_standard_methods.Get.{ account_id = Jmap_id.of_string ""; ids = None; properties = None }; (* Placeholder *)
9999+ call_id = "";
100100+ witness = Get typ;
101101+ })
102102+ | [typ; "changes"] ->
103103+ Packed (Invocation {
104104+ method_name = name;
105105+ arguments = Jmap_standard_methods.Changes.{ account_id = Jmap_id.of_string ""; since_state = ""; max_changes = None }; (* Placeholder *)
106106+ call_id = "";
107107+ witness = Changes typ;
108108+ })
109109+ | [typ; "set"] ->
110110+ Packed (Invocation {
111111+ method_name = name;
112112+ arguments = Jmap_standard_methods.Set.{
113113+ account_id = Jmap_id.of_string "";
114114+ if_in_state = None;
115115+ create = None;
116116+ update = None;
117117+ destroy = None;
118118+ };
119119+ call_id = "";
120120+ witness = Set typ;
121121+ })
122122+ | [typ; "query"] ->
123123+ Packed (Invocation {
124124+ method_name = name;
125125+ arguments = Jmap_standard_methods.Query.{
126126+ account_id = Jmap_id.of_string "";
127127+ filter = None;
128128+ sort = None;
129129+ position = None;
130130+ anchor = None;
131131+ anchor_offset = None;
132132+ limit = None;
133133+ calculate_total = None;
134134+ };
135135+ call_id = "";
136136+ witness = Query typ;
137137+ })
138138+ | _ ->
139139+ raise (Jmap_error.Parse_error (Printf.sprintf "Unknown method: %s" name))
140140+141141+(** Parse invocation from JSON array [method_name, arguments, call_id].
142142+ Test files: test/data/core/request_echo.json *)
143143+let of_json json =
144144+ (* Parse invocation from JSON array: [method_name, arguments, call_id] *)
145145+ match json with
146146+ | `A [(`String method_name); arguments; (`String call_id)] ->
147147+ (* For now, create a generic invocation without full type checking *)
148148+ (* We'll store the raw JSON as the arguments *)
149149+ Packed (Invocation {
150150+ method_name;
151151+ arguments; (* Store raw JSON for now *)
152152+ call_id;
153153+ witness = Echo; (* Use Echo as a generic witness *)
154154+ })
155155+ | `A _ -> raise (Jmap_error.Parse_error "Invocation must be [method, args, id]")
156156+ | _ -> raise (Jmap_error.Parse_error "Invocation must be a JSON array")
157157+158158+(** Convert invocation to JSON *)
159159+let to_json : type resp. resp invocation -> Ezjsonm.value =
160160+ fun (Invocation { method_name; arguments; call_id; witness }) ->
161161+ (* Serialize arguments based on witness type *)
162162+ let args_json : Ezjsonm.value = match witness with
163163+ | Echo -> arguments (* Echo arguments are already Ezjsonm.value *)
164164+ | Get _ ->
165165+ (* This code path should never execute - we only create invocations with Echo witness.
166166+ If it does execute, fail immediately rather than using unsafe magic. *)
167167+ failwith "to_json: Get witness not supported - use Echo witness with pre-serialized JSON"
168168+ | Changes _ ->
169169+ failwith "to_json: Changes witness not supported - use Echo witness with pre-serialized JSON"
170170+ | Set _ ->
171171+ failwith "to_json: Set witness not supported - use Echo witness with pre-serialized JSON"
172172+ | Copy _ ->
173173+ failwith "to_json: Copy witness not supported - use Echo witness with pre-serialized JSON"
174174+ | Query _ ->
175175+ failwith "to_json: Query witness not supported - use Echo witness with pre-serialized JSON"
176176+ | QueryChanges _ ->
177177+ failwith "to_json: QueryChanges witness not supported - use Echo witness with pre-serialized JSON"
178178+ in
179179+ `A [`String method_name; args_json; `String call_id]
180180+181181+(** Extract response data as JSON from a packed response.
182182+ This provides safe access to response data.
183183+184184+ NOTE: Currently all responses are parsed with Echo witness and stored as
185185+ Ezjsonm.value, so only the Echo case executes. The other cases will fail
186186+ immediately if called - they should never execute in the current implementation. *)
187187+let response_to_json : packed_response -> Ezjsonm.value = function
188188+ | PackedResponse (ResponseInvocation { response; witness; _ }) ->
189189+ (* Pattern match on witness to convert response to JSON type-safely *)
190190+ match witness with
191191+ | Echo ->
192192+ (* For Echo witness, response is already Ezjsonm.value - completely type-safe! *)
193193+ response
194194+ | Get _ ->
195195+ (* This code path should never execute - we only create responses with Echo witness.
196196+ If it does execute, fail immediately rather than using unsafe magic. *)
197197+ failwith "response_to_json: Get witness not supported - responses use Echo witness"
198198+ | Changes _ ->
199199+ failwith "response_to_json: Changes witness not supported - responses use Echo witness"
200200+ | Set _ ->
201201+ failwith "response_to_json: Set witness not supported - responses use Echo witness"
202202+ | Copy _ ->
203203+ failwith "response_to_json: Copy witness not supported - responses use Echo witness"
204204+ | Query _ ->
205205+ failwith "response_to_json: Query witness not supported - responses use Echo witness"
206206+ | QueryChanges _ ->
207207+ failwith "response_to_json: QueryChanges witness not supported - responses use Echo witness"
+70
jmap/jmap-core/jmap_invocation.mli
···11+(** JMAP Invocation with Type-Safe Method Dispatch *)
22+33+(** Method witness type - encodes the relationship between
44+ method names and their argument/response types *)
55+type ('args, 'resp) method_witness =
66+ | Echo : (Ezjsonm.value, Ezjsonm.value) method_witness
77+ | Get : string -> ('a Jmap_standard_methods.Get.request, 'a Jmap_standard_methods.Get.response) method_witness
88+ | Changes : string -> (Jmap_standard_methods.Changes.request, Jmap_standard_methods.Changes.response) method_witness
99+ | Set : string -> ('a Jmap_standard_methods.Set.request, 'a Jmap_standard_methods.Set.response) method_witness
1010+ | Copy : string -> ('a Jmap_standard_methods.Copy.request, 'a Jmap_standard_methods.Copy.response) method_witness
1111+ | Query : string -> ('f Jmap_standard_methods.Query.request, Jmap_standard_methods.Query.response) method_witness
1212+ | QueryChanges : string -> ('f Jmap_standard_methods.QueryChanges.request, Jmap_standard_methods.QueryChanges.response) method_witness
1313+1414+(** Type-safe invocation pairing method name with typed arguments *)
1515+type _ invocation =
1616+ | Invocation : {
1717+ method_name : string;
1818+ arguments : 'args;
1919+ call_id : string;
2020+ witness : ('args, 'resp) method_witness;
2121+ } -> 'resp invocation
2222+2323+(** Existential wrapper for heterogeneous invocation lists *)
2424+type packed_invocation =
2525+ | Packed : 'resp invocation -> packed_invocation
2626+2727+(** Heterogeneous list of invocations (for Request.method_calls) *)
2828+type invocation_list = packed_invocation list
2929+3030+(** Response invocation - pairs method name with typed response *)
3131+type _ response_invocation =
3232+ | ResponseInvocation : {
3333+ method_name : string;
3434+ response : 'resp;
3535+ call_id : string;
3636+ witness : ('args, 'resp) method_witness;
3737+ } -> 'resp response_invocation
3838+3939+(** Packed response invocation *)
4040+type packed_response =
4141+ | PackedResponse : 'resp response_invocation -> packed_response
4242+4343+(** Heterogeneous list of responses (for Response.method_responses) *)
4444+type response_list = packed_response list
4545+4646+(** Error response *)
4747+type error_response = {
4848+ error_type : Jmap_error.method_error;
4949+ call_id : string;
5050+}
5151+5252+(** Response can be either success or error *)
5353+type method_response =
5454+ | Success of packed_response
5555+ | Error of error_response
5656+5757+(** Get method name from witness *)
5858+val method_name_of_witness : ('a, 'r) method_witness -> string
5959+6060+(** Parse method name and return appropriate witness *)
6161+val witness_of_method_name : string -> packed_invocation
6262+6363+(** Parse invocation from JSON array [method_name, arguments, call_id] *)
6464+val of_json : Ezjsonm.value -> packed_invocation
6565+6666+(** Convert invocation to JSON *)
6767+val to_json : 'resp invocation -> Ezjsonm.value
6868+6969+(** Extract response data as JSON from a packed response *)
7070+val response_to_json : packed_response -> Ezjsonm.value
+119
jmap/jmap-core/jmap_parser.ml
···11+(** JMAP JSON Parser Utilities
22+33+ This module provides helper functions for parsing JMAP objects using jsonm/ezjsonm.
44+55+ All parsing functions should reference specific test files for expected JSON format.
66+*)
77+88+(** Helper functions for working with ezjsonm values *)
99+module Helpers = struct
1010+ (** Expect a JSON object and return field list *)
1111+ let expect_object = function
1212+ | `O fields -> fields
1313+ | _ -> raise (Jmap_error.Parse_error "Expected JSON object")
1414+1515+ (** Expect a JSON array and return element list *)
1616+ let expect_array = function
1717+ | `A items -> items
1818+ | _ -> raise (Jmap_error.Parse_error "Expected JSON array")
1919+2020+ (** Expect a JSON string *)
2121+ let expect_string = function
2222+ | `String s -> s
2323+ | _ -> raise (Jmap_error.Parse_error "Expected JSON string")
2424+2525+ (** Expect a JSON integer *)
2626+ let expect_int = function
2727+ | `Float f -> int_of_float f
2828+ | _ -> raise (Jmap_error.Parse_error "Expected JSON number")
2929+3030+ (** Expect a JSON boolean *)
3131+ let expect_bool = function
3232+ | `Bool b -> b
3333+ | _ -> raise (Jmap_error.Parse_error "Expected JSON boolean")
3434+3535+ (** Find optional field in object *)
3636+ let find_field name fields =
3737+ List.assoc_opt name fields
3838+3939+ (** Require field to be present *)
4040+ let require_field name fields =
4141+ match find_field name fields with
4242+ | Some v -> v
4343+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing required field: %s" name))
4444+4545+ (** Get optional string field *)
4646+ let get_string_opt name fields =
4747+ match find_field name fields with
4848+ | Some (`String s) -> Some s
4949+ | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
5050+ | None -> None
5151+5252+ (** Get required string field *)
5353+ let get_string name fields =
5454+ match require_field name fields with
5555+ | `String s -> s
5656+ | _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
5757+5858+ (** Get optional boolean field with default *)
5959+ let get_bool_opt name fields default =
6060+ match find_field name fields with
6161+ | Some (`Bool b) -> b
6262+ | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a boolean" name))
6363+ | None -> default
6464+6565+ (** Get required boolean field *)
6666+ let get_bool name fields =
6767+ match require_field name fields with
6868+ | `Bool b -> b
6969+ | _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a boolean" name))
7070+7171+ (** Get optional int field *)
7272+ let get_int_opt name fields =
7373+ match find_field name fields with
7474+ | Some (`Float f) -> Some (int_of_float f)
7575+ | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a number" name))
7676+ | None -> None
7777+7878+ (** Get required int field *)
7979+ let get_int name fields =
8080+ match require_field name fields with
8181+ | `Float f -> int_of_float f
8282+ | _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a number" name))
8383+8484+ (** Parse a map with string keys *)
8585+ let parse_map parse_value = function
8686+ | `O fields ->
8787+ List.map (fun (k, v) -> (k, parse_value v)) fields
8888+ | _ -> raise (Jmap_error.Parse_error "Expected JSON object for map")
8989+9090+ (** Parse an array *)
9191+ let parse_array parse_elem = function
9292+ | `A items -> List.map parse_elem items
9393+ | `Null -> []
9494+ | _ -> raise (Jmap_error.Parse_error "Expected JSON array")
9595+9696+ (** Parse optional array (null or array) *)
9797+ let parse_array_opt parse_elem = function
9898+ | `Null -> None
9999+ | `A items -> Some (List.map parse_elem items)
100100+ | _ -> raise (Jmap_error.Parse_error "Expected JSON array or null")
101101+end
102102+103103+(** TODO: Implement specific parsers for each JMAP type.
104104+ Each parser should reference its corresponding test file. *)
105105+106106+(** Parse JMAP Request
107107+ Test files: test/data/core/request_*.json *)
108108+let parse_request json =
109109+ Jmap_request.Parser.of_json json
110110+111111+(** Parse JMAP Response
112112+ Test files: test/data/core/response_*.json *)
113113+let parse_response json =
114114+ Jmap_response.Parser.of_json json
115115+116116+(** Parse JMAP Session
117117+ Test files: test/data/core/session.json *)
118118+let parse_session json =
119119+ Jmap_session.Parser.of_json json
+61
jmap/jmap-core/jmap_parser.mli
···11+(** JMAP JSON Parser Utilities *)
22+33+(** Helper functions for working with ezjsonm values *)
44+module Helpers : sig
55+ (** Expect a JSON object and return field list *)
66+ val expect_object : Ezjsonm.value -> (string * Ezjsonm.value) list
77+88+ (** Expect a JSON array and return element list *)
99+ val expect_array : Ezjsonm.value -> Ezjsonm.value list
1010+1111+ (** Expect a JSON string *)
1212+ val expect_string : Ezjsonm.value -> string
1313+1414+ (** Expect a JSON integer *)
1515+ val expect_int : Ezjsonm.value -> int
1616+1717+ (** Expect a JSON boolean *)
1818+ val expect_bool : Ezjsonm.value -> bool
1919+2020+ (** Find optional field in object *)
2121+ val find_field : string -> (string * Ezjsonm.value) list -> Ezjsonm.value option
2222+2323+ (** Require field to be present *)
2424+ val require_field : string -> (string * Ezjsonm.value) list -> Ezjsonm.value
2525+2626+ (** Get optional string field *)
2727+ val get_string_opt : string -> (string * Ezjsonm.value) list -> string option
2828+2929+ (** Get required string field *)
3030+ val get_string : string -> (string * Ezjsonm.value) list -> string
3131+3232+ (** Get optional boolean field with default *)
3333+ val get_bool_opt : string -> (string * Ezjsonm.value) list -> bool -> bool
3434+3535+ (** Get required boolean field *)
3636+ val get_bool : string -> (string * Ezjsonm.value) list -> bool
3737+3838+ (** Get optional int field *)
3939+ val get_int_opt : string -> (string * Ezjsonm.value) list -> int option
4040+4141+ (** Get required int field *)
4242+ val get_int : string -> (string * Ezjsonm.value) list -> int
4343+4444+ (** Parse a map with string keys *)
4545+ val parse_map : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> (string * 'a) list
4646+4747+ (** Parse an array *)
4848+ val parse_array : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a list
4949+5050+ (** Parse optional array (null or array) *)
5151+ val parse_array_opt : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a list option
5252+end
5353+5454+(** Parse JMAP Request *)
5555+val parse_request : Ezjsonm.value -> Jmap_request.t
5656+5757+(** Parse JMAP Response *)
5858+val parse_response : Ezjsonm.value -> Jmap_response.t
5959+6060+(** Parse JMAP Session *)
6161+val parse_session : Ezjsonm.value -> Jmap_session.t
+142
jmap/jmap-core/jmap_primitives.ml
···11+(** JMAP Primitive Data Types
22+33+ This module defines the primitive data types used in JMAP:
44+ - Int (signed 53-bit integer)
55+ - UnsignedInt (unsigned integer 0 to 2^53-1)
66+ - Date (RFC 3339 date-time)
77+ - UTCDate (RFC 3339 date-time with Z timezone)
88+99+ Reference: RFC 8620 Section 1.3
1010+*)
1111+1212+(** Signed 53-bit integer (-2^53 + 1 to 2^53 - 1)
1313+ JavaScript's safe integer range *)
1414+module Int53 = struct
1515+ type t = int
1616+1717+ let min_value = -9007199254740991 (* -(2^53 - 1) *)
1818+ let max_value = 9007199254740991 (* 2^53 - 1 *)
1919+2020+ let of_int i =
2121+ if i < min_value || i > max_value then
2222+ raise (Invalid_argument "Int53 out of range")
2323+ else
2424+ i
2525+2626+ let to_int t = t
2727+2828+ (** Parse from JSON.
2929+ Test files: test/data/core/request_query.json (position, anchorOffset) *)
3030+ let of_json = function
3131+ | `Float f ->
3232+ let i = int_of_float f in
3333+ if Float.is_integer f then
3434+ of_int i
3535+ else
3636+ raise (Jmap_error.Parse_error "Int53 must be an integer")
3737+ | _ -> raise (Jmap_error.Parse_error "Int53 must be a JSON number")
3838+3939+ let to_json t = `Float (float_of_int t)
4040+end
4141+4242+(** Unsigned integer (0 to 2^53 - 1) *)
4343+module UnsignedInt = struct
4444+ type t = int
4545+4646+ let min_value = 0
4747+ let max_value = 9007199254740991 (* 2^53 - 1 *)
4848+4949+ let of_int i =
5050+ if i < min_value || i > max_value then
5151+ raise (Invalid_argument "UnsignedInt out of range")
5252+ else
5353+ i
5454+5555+ let to_int t = t
5656+5757+ (** Parse from JSON.
5858+ Test files:
5959+ - test/data/mail/mailbox_get_response.json (totalEmails, unreadEmails, etc.)
6060+ - test/data/core/request_query.json (limit)
6161+ *)
6262+ let of_json = function
6363+ | `Float f ->
6464+ let i = int_of_float f in
6565+ if Float.is_integer f && i >= 0 then
6666+ of_int i
6767+ else
6868+ raise (Jmap_error.Parse_error "UnsignedInt must be a non-negative integer")
6969+ | _ -> raise (Jmap_error.Parse_error "UnsignedInt must be a JSON number")
7070+7171+ let to_json t = `Float (float_of_int t)
7272+end
7373+7474+(** RFC 3339 date-time (with or without timezone)
7575+ Examples: "2014-10-30T14:12:00+08:00", "2014-10-30T06:12:00Z"
7676+*)
7777+module Date = struct
7878+ type t = string
7979+8080+ (** Basic validation of RFC 3339 format *)
8181+ let validate s =
8282+ (* Simple check: contains 'T' and has reasonable length *)
8383+ String.contains s 'T' && String.length s >= 19
8484+8585+ let of_string s =
8686+ if validate s then s
8787+ else raise (Invalid_argument "Invalid RFC 3339 date-time format")
8888+8989+ let to_string t = t
9090+9191+ (** Parse from JSON.
9292+ Test files: test/data/mail/email_get_response.json (sentAt field) *)
9393+ let of_json = function
9494+ | `String s -> of_string s
9595+ | _ -> raise (Jmap_error.Parse_error "Date must be a JSON string")
9696+9797+ let to_json t = `String t
9898+end
9999+100100+(** RFC 3339 date-time with Z timezone (UTC)
101101+ Example: "2014-10-30T06:12:00Z"
102102+103103+ MUST have "Z" suffix to indicate UTC.
104104+*)
105105+module UTCDate = struct
106106+ type t = string
107107+108108+ (** Validate that string is RFC 3339 with Z suffix *)
109109+ let validate s =
110110+ String.contains s 'T' &&
111111+ String.length s >= 20 &&
112112+ s.[String.length s - 1] = 'Z'
113113+114114+ let of_string s =
115115+ if validate s then s
116116+ else raise (Invalid_argument "Invalid RFC 3339 UTCDate format (must end with Z)")
117117+118118+ let to_string t = t
119119+120120+ (** Parse from JSON.
121121+ Test files:
122122+ - test/data/mail/email_get_response.json (receivedAt field)
123123+ - test/data/mail/email_submission_get_response.json (sendAt field)
124124+ *)
125125+ let of_json = function
126126+ | `String s -> of_string s
127127+ | _ -> raise (Jmap_error.Parse_error "UTCDate must be a JSON string")
128128+129129+ let to_json t = `String t
130130+131131+ (** Get current UTC time as UTCDate *)
132132+ let now () =
133133+ let open Unix in
134134+ let tm = gmtime (time ()) in
135135+ Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ"
136136+ (tm.tm_year + 1900)
137137+ (tm.tm_mon + 1)
138138+ tm.tm_mday
139139+ tm.tm_hour
140140+ tm.tm_min
141141+ tm.tm_sec
142142+end
+67
jmap/jmap-core/jmap_primitives.mli
···11+(** JMAP Primitive Data Types
22+33+ This module defines the primitive data types used in JMAP.
44+55+ Reference: RFC 8620 Section 1.3
66+*)
77+88+(** Signed 53-bit integer (-2^53 + 1 to 2^53 - 1) *)
99+module Int53 : sig
1010+ type t
1111+1212+ val min_value : int
1313+ val max_value : int
1414+1515+ (** Create from int.
1616+ @raise Invalid_argument if out of range *)
1717+ val of_int : int -> t
1818+1919+ val to_int : t -> int
2020+ val of_json : Ezjsonm.value -> t
2121+ val to_json : t -> Ezjsonm.value
2222+end
2323+2424+(** Unsigned integer (0 to 2^53 - 1) *)
2525+module UnsignedInt : sig
2626+ type t
2727+2828+ val min_value : int
2929+ val max_value : int
3030+3131+ (** Create from int.
3232+ @raise Invalid_argument if out of range *)
3333+ val of_int : int -> t
3434+3535+ val to_int : t -> int
3636+ val of_json : Ezjsonm.value -> t
3737+ val to_json : t -> Ezjsonm.value
3838+end
3939+4040+(** RFC 3339 date-time (with or without timezone) *)
4141+module Date : sig
4242+ type t
4343+4444+ (** Create from RFC 3339 string.
4545+ @raise Invalid_argument if invalid format *)
4646+ val of_string : string -> t
4747+4848+ val to_string : t -> string
4949+ val of_json : Ezjsonm.value -> t
5050+ val to_json : t -> Ezjsonm.value
5151+end
5252+5353+(** RFC 3339 date-time with Z timezone (UTC) *)
5454+module UTCDate : sig
5555+ type t
5656+5757+ (** Create from RFC 3339 string with Z suffix.
5858+ @raise Invalid_argument if invalid format or missing Z *)
5959+ val of_string : string -> t
6060+6161+ val to_string : t -> string
6262+ val of_json : Ezjsonm.value -> t
6363+ val to_json : t -> Ezjsonm.value
6464+6565+ (** Get current UTC time as UTCDate *)
6666+ val now : unit -> t
6767+end
+117
jmap/jmap-core/jmap_push.ml
···11+(** JMAP Push Notification Types
22+33+ Push notifications allow servers to notify clients of state changes.
44+55+ Reference: RFC 8620 Section 7.1-7.2
66+ Test files:
77+ - test/data/core/push_state_change.json
88+ - test/data/core/push_subscription.json
99+*)
1010+1111+(** StateChange notification object *)
1212+module StateChange = struct
1313+ (** Map of type name to state string *)
1414+ type type_state = (string * string) list
1515+1616+ type t = {
1717+ at_type : string; (** Always "StateChange" *)
1818+ changed : (Jmap_id.t * type_state) list; (** accountId -> type -> state *)
1919+ }
2020+2121+ (** Accessors *)
2222+ let at_type t = t.at_type
2323+ let changed t = t.changed
2424+2525+ (** Constructor *)
2626+ let v ~at_type ~changed = { at_type; changed }
2727+2828+ (** Parse from JSON.
2929+ Test files: test/data/core/push_state_change.json
3030+3131+ Expected structure:
3232+ {
3333+ "@type": "StateChange",
3434+ "changed": {
3535+ "account-id-1": {
3636+ "Email": "d35ecb040aab",
3737+ "Mailbox": "0af7a512ce70"
3838+ },
3939+ "account-id-2": {
4040+ "CalendarEvent": "7a4297cecd76"
4141+ }
4242+ }
4343+ }
4444+ *)
4545+ let of_json _json =
4646+ (* TODO: Implement JSON parsing *)
4747+ raise (Jmap_error.Parse_error "StateChange.of_json not yet implemented")
4848+end
4949+5050+(** PushSubscription object *)
5151+module PushSubscription = struct
5252+ type t = {
5353+ id : Jmap_id.t;
5454+ device_client_id : string;
5555+ url : string;
5656+ keys : Ezjsonm.value option;
5757+ verification_code : string option;
5858+ expires : Jmap_primitives.UTCDate.t option;
5959+ types : string list option;
6060+ }
6161+6262+ (** Accessors *)
6363+ let id t = t.id
6464+ let device_client_id t = t.device_client_id
6565+ let url t = t.url
6666+ let keys t = t.keys
6767+ let verification_code t = t.verification_code
6868+ let expires t = t.expires
6969+ let types t = t.types
7070+7171+ (** Constructor *)
7272+ let v ~id ~device_client_id ~url ?keys ?verification_code ?expires ?types () =
7373+ { id; device_client_id; url; keys; verification_code; expires; types }
7474+7575+ (** Parse from JSON.
7676+ Test files: test/data/core/push_subscription.json
7777+7878+ Expected structure:
7979+ {
8080+ "id": "push-sub-id",
8181+ "deviceClientId": "device-hash",
8282+ "url": "https://push.example.com/push",
8383+ "keys": {
8484+ "p256dh": "base64-encoded-key",
8585+ "auth": "base64-encoded-secret"
8686+ },
8787+ "verificationCode": "verification-code",
8888+ "expires": "2024-12-31T23:59:59Z",
8989+ "types": ["Email", "Mailbox"]
9090+ }
9191+ *)
9292+ let of_json _json =
9393+ (* TODO: Implement JSON parsing *)
9494+ raise (Jmap_error.Parse_error "PushSubscription.of_json not yet implemented")
9595+end
9696+9797+(** PushVerification object (sent to push endpoint) *)
9898+module PushVerification = struct
9999+ type t = {
100100+ at_type : string; (** Always "PushVerification" *)
101101+ push_subscription_id : string;
102102+ verification_code : string;
103103+ }
104104+105105+ (** Accessors *)
106106+ let at_type t = t.at_type
107107+ let push_subscription_id t = t.push_subscription_id
108108+ let verification_code t = t.verification_code
109109+110110+ (** Constructor *)
111111+ let v ~at_type ~push_subscription_id ~verification_code =
112112+ { at_type; push_subscription_id; verification_code }
113113+114114+ let of_json _json =
115115+ (* TODO: Implement JSON parsing *)
116116+ raise (Jmap_error.Parse_error "PushVerification.of_json not yet implemented")
117117+end
+78
jmap/jmap-core/jmap_push.mli
···11+(** JMAP Push Notification Types *)
22+33+(** StateChange notification object *)
44+module StateChange : sig
55+ (** Map of type name to state string *)
66+ type type_state = (string * string) list
77+88+ type t = {
99+ at_type : string;
1010+ changed : (Jmap_id.t * type_state) list;
1111+ }
1212+1313+ (** Accessors *)
1414+ val at_type : t -> string
1515+ val changed : t -> (Jmap_id.t * type_state) list
1616+1717+ (** Constructor *)
1818+ val v : at_type:string -> changed:(Jmap_id.t * type_state) list -> t
1919+2020+ (** Parse from JSON *)
2121+ val of_json : Ezjsonm.value -> t
2222+end
2323+2424+(** PushSubscription object *)
2525+module PushSubscription : sig
2626+ type t = {
2727+ id : Jmap_id.t;
2828+ device_client_id : string;
2929+ url : string;
3030+ keys : Ezjsonm.value option;
3131+ verification_code : string option;
3232+ expires : Jmap_primitives.UTCDate.t option;
3333+ types : string list option;
3434+ }
3535+3636+ (** Accessors *)
3737+ val id : t -> Jmap_id.t
3838+ val device_client_id : t -> string
3939+ val url : t -> string
4040+ val keys : t -> Ezjsonm.value option
4141+ val verification_code : t -> string option
4242+ val expires : t -> Jmap_primitives.UTCDate.t option
4343+ val types : t -> string list option
4444+4545+ (** Constructor *)
4646+ val v :
4747+ id:Jmap_id.t ->
4848+ device_client_id:string ->
4949+ url:string ->
5050+ ?keys:Ezjsonm.value ->
5151+ ?verification_code:string ->
5252+ ?expires:Jmap_primitives.UTCDate.t ->
5353+ ?types:string list ->
5454+ unit ->
5555+ t
5656+5757+ (** Parse from JSON *)
5858+ val of_json : Ezjsonm.value -> t
5959+end
6060+6161+(** PushVerification object (sent to push endpoint) *)
6262+module PushVerification : sig
6363+ type t = {
6464+ at_type : string;
6565+ push_subscription_id : string;
6666+ verification_code : string;
6767+ }
6868+6969+ (** Accessors *)
7070+ val at_type : t -> string
7171+ val push_subscription_id : t -> string
7272+ val verification_code : t -> string
7373+7474+ (** Constructor *)
7575+ val v : at_type:string -> push_subscription_id:string -> verification_code:string -> t
7676+7777+ val of_json : Ezjsonm.value -> t
7878+end
+117
jmap/jmap-core/jmap_request.ml
···11+(** JMAP Request Object
22+33+ A Request object represents a single HTTP POST to the JMAP API endpoint.
44+ It contains capabilities the client wants to use and a list of method calls.
55+66+ Reference: RFC 8620 Section 3.3
77+ Test files:
88+ - test/data/core/request_echo.json
99+ - test/data/core/request_get.json
1010+ - All request_*.json files
1111+*)
1212+1313+(** Main request type *)
1414+type t = {
1515+ using : Jmap_capability.t list;
1616+ method_calls : Jmap_invocation.invocation_list;
1717+ created_ids : (Jmap_id.t * Jmap_id.t) list option;
1818+}
1919+2020+(** Accessors *)
2121+let using t = t.using
2222+let method_calls t = t.method_calls
2323+let created_ids t = t.created_ids
2424+2525+(** Create a request *)
2626+let make ?(created_ids=None) ~using method_calls =
2727+ { using; method_calls; created_ids }
2828+2929+(** Parser submodule *)
3030+module Parser = struct
3131+ (** Parse request from JSON value.
3232+ Test files: test/data/core/request_*.json *)
3333+ let of_json json =
3434+ match json with
3535+ | `O fields ->
3636+ let get_field name =
3737+ match List.assoc_opt name fields with
3838+ | Some v -> v
3939+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name))
4040+ in
4141+4242+ (* Parse using *)
4343+ let using =
4444+ match get_field "using" with
4545+ | `A caps ->
4646+ List.map (function
4747+ | `String cap -> Jmap_capability.of_string cap
4848+ | _ -> raise (Jmap_error.Parse_error "using values must be strings")
4949+ ) caps
5050+ | _ -> raise (Jmap_error.Parse_error "using must be an array")
5151+ in
5252+5353+ (* Parse methodCalls *)
5454+ let method_calls =
5555+ match get_field "methodCalls" with
5656+ | `A calls -> List.map Jmap_invocation.of_json calls
5757+ | _ -> raise (Jmap_error.Parse_error "methodCalls must be an array")
5858+ in
5959+6060+ (* Parse createdIds (optional) *)
6161+ let created_ids =
6262+ match List.assoc_opt "createdIds" fields with
6363+ | Some (`O ids) ->
6464+ Some (List.map (fun (k, v) ->
6565+ match v with
6666+ | `String id -> (Jmap_id.of_string k, Jmap_id.of_string id)
6767+ | _ -> raise (Jmap_error.Parse_error "createdIds values must be strings")
6868+ ) ids)
6969+ | Some _ -> raise (Jmap_error.Parse_error "createdIds must be an object")
7070+ | None -> None
7171+ in
7272+7373+ { using; method_calls; created_ids }
7474+ | _ -> raise (Jmap_error.Parse_error "Request must be a JSON object")
7575+7676+ (** Parse request from JSON string *)
7777+ let of_string s =
7878+ try
7979+ of_json (Ezjsonm.from_string s)
8080+ with
8181+ | Ezjsonm.Parse_error (_, msg) ->
8282+ raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg))
8383+8484+ (** Parse request from input channel *)
8585+ let of_channel ic =
8686+ try
8787+ of_json (Ezjsonm.from_channel ic)
8888+ with
8989+ | Ezjsonm.Parse_error (_, msg) ->
9090+ raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg))
9191+end
9292+9393+(** Serialization *)
9494+let to_json t =
9595+ let using_json = `A (List.map (fun cap ->
9696+ `String (Jmap_capability.to_string cap)
9797+ ) t.using) in
9898+9999+ let method_calls_json = `A (List.map (fun (Jmap_invocation.Packed inv) ->
100100+ Jmap_invocation.to_json inv
101101+ ) t.method_calls) in
102102+103103+ let fields = [
104104+ ("using", using_json);
105105+ ("methodCalls", method_calls_json);
106106+ ] in
107107+108108+ let fields = match t.created_ids with
109109+ | Some ids ->
110110+ let ids_json = `O (List.map (fun (k, v) ->
111111+ (Jmap_id.to_string k, `String (Jmap_id.to_string v))
112112+ ) ids) in
113113+ fields @ [("createdIds", ids_json)]
114114+ | None -> fields
115115+ in
116116+117117+ `O fields
+31
jmap/jmap-core/jmap_request.mli
···11+(** JMAP Request Object *)
22+33+(** Main request type *)
44+type t = {
55+ using : Jmap_capability.t list;
66+ method_calls : Jmap_invocation.invocation_list;
77+ created_ids : (Jmap_id.t * Jmap_id.t) list option;
88+}
99+1010+(** Accessors *)
1111+val using : t -> Jmap_capability.t list
1212+val method_calls : t -> Jmap_invocation.invocation_list
1313+val created_ids : t -> (Jmap_id.t * Jmap_id.t) list option
1414+1515+(** Constructor *)
1616+val make : ?created_ids:(Jmap_id.t * Jmap_id.t) list option -> using:Jmap_capability.t list -> Jmap_invocation.invocation_list -> t
1717+1818+(** Parser submodule *)
1919+module Parser : sig
2020+ (** Parse request from JSON value *)
2121+ val of_json : Ezjsonm.value -> t
2222+2323+ (** Parse request from JSON string *)
2424+ val of_string : string -> t
2525+2626+ (** Parse request from input channel *)
2727+ val of_channel : in_channel -> t
2828+end
2929+3030+(** Serialization *)
3131+val to_json : t -> Ezjsonm.value
+106
jmap/jmap-core/jmap_response.ml
···11+(** JMAP Response Object
22+33+ A Response object is returned from the JMAP API endpoint in response to a Request.
44+ It contains method responses and the current session state.
55+66+ Reference: RFC 8620 Section 3.4
77+ Test files:
88+ - test/data/core/response_echo.json
99+ - test/data/core/response_get.json
1010+ - All response_*.json files
1111+*)
1212+1313+(** Main response type *)
1414+type t = {
1515+ method_responses : Jmap_invocation.response_list;
1616+ created_ids : (Jmap_id.t * Jmap_id.t) list option;
1717+ session_state : string;
1818+}
1919+2020+(** Accessors *)
2121+let method_responses t = t.method_responses
2222+let created_ids t = t.created_ids
2323+let session_state t = t.session_state
2424+2525+(** Create a response *)
2626+let make ?(created_ids=None) ~method_responses ~session_state () =
2727+ { method_responses; created_ids; session_state }
2828+2929+(** Parser submodule *)
3030+module Parser = struct
3131+ (** Parse response from JSON value.
3232+ Test files: test/data/core/response_*.json *)
3333+ let of_json json =
3434+ match json with
3535+ | `O fields ->
3636+ let get_field name =
3737+ match List.assoc_opt name fields with
3838+ | Some v -> v
3939+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name))
4040+ in
4141+4242+ (* Parse methodResponses - similar to parsing request methodCalls *)
4343+ let method_responses =
4444+ match get_field "methodResponses" with
4545+ | `A responses ->
4646+ List.map (fun resp_json ->
4747+ (* Each response is ["method", {...}, "callId"] *)
4848+ (* For now, just parse as generic invocations *)
4949+ match resp_json with
5050+ | `A [(`String method_name); response; (`String call_id)] ->
5151+ (* Parse as response invocation, storing raw JSON *)
5252+ Jmap_invocation.PackedResponse (Jmap_invocation.ResponseInvocation {
5353+ method_name;
5454+ response;
5555+ call_id;
5656+ witness = Jmap_invocation.Echo;
5757+ })
5858+ | _ -> raise (Jmap_error.Parse_error "Invalid method response format")
5959+ ) responses
6060+ | _ -> raise (Jmap_error.Parse_error "methodResponses must be an array")
6161+ in
6262+6363+ (* Parse createdIds (optional) *)
6464+ let created_ids =
6565+ match List.assoc_opt "createdIds" fields with
6666+ | Some (`O ids) ->
6767+ Some (List.map (fun (k, v) ->
6868+ match v with
6969+ | `String id -> (Jmap_id.of_string k, Jmap_id.of_string id)
7070+ | _ -> raise (Jmap_error.Parse_error "createdIds values must be strings")
7171+ ) ids)
7272+ | Some _ -> raise (Jmap_error.Parse_error "createdIds must be an object")
7373+ | None -> None
7474+ in
7575+7676+ (* Parse sessionState *)
7777+ let session_state =
7878+ match get_field "sessionState" with
7979+ | `String s -> s
8080+ | _ -> raise (Jmap_error.Parse_error "sessionState must be a string")
8181+ in
8282+8383+ { method_responses; created_ids; session_state }
8484+ | _ -> raise (Jmap_error.Parse_error "Response must be a JSON object")
8585+8686+ (** Parse response from JSON string *)
8787+ let of_string s =
8888+ try
8989+ of_json (Ezjsonm.from_string s)
9090+ with
9191+ | Ezjsonm.Parse_error (_, msg) ->
9292+ raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg))
9393+9494+ (** Parse response from input channel *)
9595+ let of_channel ic =
9696+ try
9797+ of_json (Ezjsonm.from_channel ic)
9898+ with
9999+ | Ezjsonm.Parse_error (_, msg) ->
100100+ raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg))
101101+end
102102+103103+(** Serialization *)
104104+let to_json _t =
105105+ (* TODO: Implement JSON serialization *)
106106+ raise (Jmap_error.Parse_error "Response.to_json not yet implemented")
+31
jmap/jmap-core/jmap_response.mli
···11+(** JMAP Response Object *)
22+33+(** Main response type *)
44+type t = {
55+ method_responses : Jmap_invocation.response_list;
66+ created_ids : (Jmap_id.t * Jmap_id.t) list option;
77+ session_state : string;
88+}
99+1010+(** Accessors *)
1111+val method_responses : t -> Jmap_invocation.response_list
1212+val created_ids : t -> (Jmap_id.t * Jmap_id.t) list option
1313+val session_state : t -> string
1414+1515+(** Constructor *)
1616+val make : ?created_ids:(Jmap_id.t * Jmap_id.t) list option -> method_responses:Jmap_invocation.response_list -> session_state:string -> unit -> t
1717+1818+(** Parser submodule *)
1919+module Parser : sig
2020+ (** Parse response from JSON value *)
2121+ val of_json : Ezjsonm.value -> t
2222+2323+ (** Parse response from JSON string *)
2424+ val of_string : string -> t
2525+2626+ (** Parse response from input channel *)
2727+ val of_channel : in_channel -> t
2828+end
2929+3030+(** Serialization *)
3131+val to_json : t -> Ezjsonm.value
+188
jmap/jmap-core/jmap_session.ml
···11+(** JMAP Session and Account Types
22+33+ The Session object describes the server's capabilities and the accounts
44+ available to the current user.
55+66+ Reference: RFC 8620 Section 2
77+ Test files: test/data/core/session.json
88+*)
99+1010+(** Account object *)
1111+module Account = struct
1212+ type t = {
1313+ name : string;
1414+ is_personal : bool;
1515+ is_read_only : bool;
1616+ account_capabilities : (string * Ezjsonm.value) list;
1717+ }
1818+1919+ (** Accessors *)
2020+ let name t = t.name
2121+ let is_personal t = t.is_personal
2222+ let is_read_only t = t.is_read_only
2323+ let account_capabilities t = t.account_capabilities
2424+2525+ (** Constructor *)
2626+ let v ~name ~is_personal ~is_read_only ~account_capabilities =
2727+ { name; is_personal; is_read_only; account_capabilities }
2828+2929+ (** Parse from JSON.
3030+ Test files: test/data/core/session.json (accounts field) *)
3131+ let of_json json =
3232+ match json with
3333+ | `O fields ->
3434+ let get_string name =
3535+ match List.assoc_opt name fields with
3636+ | Some (`String s) -> s
3737+ | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
3838+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name))
3939+ in
4040+ let get_bool name =
4141+ match List.assoc_opt name fields with
4242+ | Some (`Bool b) -> b
4343+ | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a boolean" name))
4444+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name))
4545+ in
4646+ let name = get_string "name" in
4747+ let is_personal = get_bool "isPersonal" in
4848+ let is_read_only = get_bool "isReadOnly" in
4949+ let account_capabilities =
5050+ match List.assoc_opt "accountCapabilities" fields with
5151+ | Some (`O caps) -> caps
5252+ | Some _ -> raise (Jmap_error.Parse_error "accountCapabilities must be an object")
5353+ | None -> []
5454+ in
5555+ { name; is_personal; is_read_only; account_capabilities }
5656+ | _ -> raise (Jmap_error.Parse_error "Account must be a JSON object")
5757+end
5858+5959+(** Session object *)
6060+type t = {
6161+ capabilities : (string * Ezjsonm.value) list;
6262+ accounts : (Jmap_id.t * Account.t) list;
6363+ primary_accounts : (string * Jmap_id.t) list;
6464+ username : string;
6565+ api_url : string;
6666+ download_url : string;
6767+ upload_url : string;
6868+ event_source_url : string;
6969+ state : string;
7070+}
7171+7272+(** Accessors *)
7373+let capabilities t = t.capabilities
7474+let accounts t = t.accounts
7575+let primary_accounts t = t.primary_accounts
7676+let username t = t.username
7777+let api_url t = t.api_url
7878+let download_url t = t.download_url
7979+let upload_url t = t.upload_url
8080+let event_source_url t = t.event_source_url
8181+let state t = t.state
8282+8383+(** Constructor *)
8484+let v ~capabilities ~accounts ~primary_accounts ~username ~api_url ~download_url ~upload_url ~event_source_url ~state =
8585+ { capabilities; accounts; primary_accounts; username; api_url; download_url; upload_url; event_source_url; state }
8686+8787+(** Parser submodule *)
8888+module Parser = struct
8989+ (** Parse session from JSON.
9090+ Test files: test/data/core/session.json
9191+9292+ Expected structure:
9393+ {
9494+ "capabilities": {
9595+ "urn:ietf:params:jmap:core": {...},
9696+ "urn:ietf:params:jmap:mail": {...},
9797+ ...
9898+ },
9999+ "accounts": {
100100+ "account-id": {
101101+ "name": "user@example.com",
102102+ "isPersonal": true,
103103+ "isReadOnly": false,
104104+ "accountCapabilities": {...}
105105+ },
106106+ ...
107107+ },
108108+ "primaryAccounts": {
109109+ "urn:ietf:params:jmap:mail": "account-id",
110110+ ...
111111+ },
112112+ "username": "user@example.com",
113113+ "apiUrl": "https://jmap.example.com/api/",
114114+ "downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}",
115115+ "uploadUrl": "https://jmap.example.com/upload/{accountId}/",
116116+ "eventSourceUrl": "https://jmap.example.com/eventsource/",
117117+ "state": "cyrus-0"
118118+ }
119119+ *)
120120+ let of_json json =
121121+ match json with
122122+ | `O fields ->
123123+ let get_string name =
124124+ match List.assoc_opt name fields with
125125+ | Some (`String s) -> s
126126+ | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
127127+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name))
128128+ in
129129+ let require_field name =
130130+ match List.assoc_opt name fields with
131131+ | Some v -> v
132132+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing field: %s" name))
133133+ in
134134+135135+ (* Parse capabilities *)
136136+ let capabilities =
137137+ match require_field "capabilities" with
138138+ | `O caps -> caps
139139+ | _ -> raise (Jmap_error.Parse_error "capabilities must be an object")
140140+ in
141141+142142+ (* Parse accounts *)
143143+ let accounts =
144144+ match require_field "accounts" with
145145+ | `O accts ->
146146+ List.map (fun (id, acct_json) ->
147147+ (Jmap_id.of_string id, Account.of_json acct_json)
148148+ ) accts
149149+ | _ -> raise (Jmap_error.Parse_error "accounts must be an object")
150150+ in
151151+152152+ (* Parse primaryAccounts *)
153153+ let primary_accounts =
154154+ match require_field "primaryAccounts" with
155155+ | `O prim ->
156156+ List.map (fun (cap, id_json) ->
157157+ match id_json with
158158+ | `String id -> (cap, Jmap_id.of_string id)
159159+ | _ -> raise (Jmap_error.Parse_error "primaryAccounts values must be strings")
160160+ ) prim
161161+ | _ -> raise (Jmap_error.Parse_error "primaryAccounts must be an object")
162162+ in
163163+164164+ let username = get_string "username" in
165165+ let api_url = get_string "apiUrl" in
166166+ let download_url = get_string "downloadUrl" in
167167+ let upload_url = get_string "uploadUrl" in
168168+ let event_source_url = get_string "eventSourceUrl" in
169169+ let state = get_string "state" in
170170+171171+ { capabilities; accounts; primary_accounts; username; api_url;
172172+ download_url; upload_url; event_source_url; state }
173173+ | _ -> raise (Jmap_error.Parse_error "Session must be a JSON object")
174174+175175+ let of_string s =
176176+ try
177177+ of_json (Ezjsonm.from_string s)
178178+ with
179179+ | Ezjsonm.Parse_error (_, msg) ->
180180+ raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg))
181181+182182+ let of_channel ic =
183183+ try
184184+ of_json (Ezjsonm.from_channel ic)
185185+ with
186186+ | Ezjsonm.Parse_error (_, msg) ->
187187+ raise (Jmap_error.Parse_error ("Invalid JSON: " ^ msg))
188188+end
+75
jmap/jmap-core/jmap_session.mli
···11+(** JMAP Session and Account Types *)
22+33+(** Account object *)
44+module Account : sig
55+ type t = {
66+ name : string;
77+ is_personal : bool;
88+ is_read_only : bool;
99+ account_capabilities : (string * Ezjsonm.value) list;
1010+ }
1111+1212+ (** Accessors *)
1313+ val name : t -> string
1414+ val is_personal : t -> bool
1515+ val is_read_only : t -> bool
1616+ val account_capabilities : t -> (string * Ezjsonm.value) list
1717+1818+ (** Constructor *)
1919+ val v :
2020+ name:string ->
2121+ is_personal:bool ->
2222+ is_read_only:bool ->
2323+ account_capabilities:(string * Ezjsonm.value) list ->
2424+ t
2525+2626+ (** Parse from JSON *)
2727+ val of_json : Ezjsonm.value -> t
2828+end
2929+3030+(** Session object *)
3131+type t = {
3232+ capabilities : (string * Ezjsonm.value) list;
3333+ accounts : (Jmap_id.t * Account.t) list;
3434+ primary_accounts : (string * Jmap_id.t) list;
3535+ username : string;
3636+ api_url : string;
3737+ download_url : string;
3838+ upload_url : string;
3939+ event_source_url : string;
4040+ state : string;
4141+}
4242+4343+(** Accessors *)
4444+val capabilities : t -> (string * Ezjsonm.value) list
4545+val accounts : t -> (Jmap_id.t * Account.t) list
4646+val primary_accounts : t -> (string * Jmap_id.t) list
4747+val username : t -> string
4848+val api_url : t -> string
4949+val download_url : t -> string
5050+val upload_url : t -> string
5151+val event_source_url : t -> string
5252+val state : t -> string
5353+5454+(** Constructor *)
5555+val v :
5656+ capabilities:(string * Ezjsonm.value) list ->
5757+ accounts:(Jmap_id.t * Account.t) list ->
5858+ primary_accounts:(string * Jmap_id.t) list ->
5959+ username:string ->
6060+ api_url:string ->
6161+ download_url:string ->
6262+ upload_url:string ->
6363+ event_source_url:string ->
6464+ state:string ->
6565+ t
6666+6767+(** Parser submodule *)
6868+module Parser : sig
6969+ (** Parse session from JSON *)
7070+ val of_json : Ezjsonm.value -> t
7171+7272+ val of_string : string -> t
7373+7474+ val of_channel : in_channel -> t
7575+end
+646
jmap/jmap-core/jmap_standard_methods.ml
···11+(** JMAP Standard Method Types
22+33+ This module defines the request and response types for all standard
44+ JMAP methods that work across different object types.
55+66+ These types are polymorphic over the object type 'a.
77+88+ Reference: RFC 8620 Sections 5.1-5.6
99+*)
1010+1111+(** Local helper functions to avoid circular dependency with Jmap_parser *)
1212+module Helpers = struct
1313+ let expect_object = function
1414+ | `O fields -> fields
1515+ | _ -> raise (Jmap_error.Parse_error "Expected JSON object")
1616+1717+ let expect_string = function
1818+ | `String s -> s
1919+ | _ -> raise (Jmap_error.Parse_error "Expected JSON string")
2020+2121+ let find_field name fields = List.assoc_opt name fields
2222+2323+ let require_field name fields =
2424+ match find_field name fields with
2525+ | Some v -> v
2626+ | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing required field: %s" name))
2727+2828+ let get_string name fields =
2929+ match require_field name fields with
3030+ | `String s -> s
3131+ | _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
3232+3333+ let get_string_opt name fields =
3434+ match find_field name fields with
3535+ | Some (`String s) -> Some s
3636+ | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name))
3737+ | None -> None
3838+3939+ let get_bool name fields =
4040+ match require_field name fields with
4141+ | `Bool b -> b
4242+ | _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a boolean" name))
4343+4444+ let parse_array parse_elem = function
4545+ | `A items -> List.map parse_elem items
4646+ | `Null -> []
4747+ | _ -> raise (Jmap_error.Parse_error "Expected JSON array")
4848+end
4949+5050+(** Standard /get method (RFC 8620 Section 5.1) *)
5151+module Get = struct
5252+ type 'a request = {
5353+ account_id : Jmap_id.t;
5454+ ids : Jmap_id.t list option; (** null = fetch all *)
5555+ properties : string list option; (** null = fetch all properties *)
5656+ }
5757+5858+ type 'a response = {
5959+ account_id : Jmap_id.t;
6060+ state : string;
6161+ list : 'a list;
6262+ not_found : Jmap_id.t list;
6363+ }
6464+6565+ (** Accessors for request *)
6666+ let account_id (r : 'a request) = r.account_id
6767+ let ids (r : 'a request) = r.ids
6868+ let properties (r : 'a request) = r.properties
6969+7070+ (** Constructor for request *)
7171+ let v ~account_id ?ids ?properties () =
7272+ { account_id; ids; properties }
7373+7474+ (** Accessors for response *)
7575+ let response_account_id (r : 'a response) = r.account_id
7676+ let state (r : 'a response) = r.state
7777+ let list (r : 'a response) = r.list
7878+ let not_found (r : 'a response) = r.not_found
7979+8080+ (** Constructor for response *)
8181+ let response_v ~account_id ~state ~list ~not_found =
8282+ { account_id; state; list; not_found }
8383+8484+ (** Parse request from JSON.
8585+ Test files: test/data/core/request_get.json *)
8686+ let request_of_json parse_obj json =
8787+ ignore parse_obj;
8888+ let open Helpers in
8989+ let fields = expect_object json in
9090+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
9191+ let ids = match find_field "ids" fields with
9292+ | Some `Null | None -> None
9393+ | Some v -> Some (parse_array Jmap_id.of_json v)
9494+ in
9595+ let properties = match find_field "properties" fields with
9696+ | Some `Null | None -> None
9797+ | Some v -> Some (parse_array expect_string v)
9898+ in
9999+ { account_id; ids; properties }
100100+101101+ (** Convert request to JSON *)
102102+ let request_to_json (req : 'a request) =
103103+ let fields = [
104104+ ("accountId", Jmap_id.to_json req.account_id);
105105+ ] in
106106+ let fields = match req.ids with
107107+ | Some ids -> ("ids", `A (List.map Jmap_id.to_json ids)) :: fields
108108+ | None -> fields
109109+ in
110110+ let fields = match req.properties with
111111+ | Some props -> ("properties", `A (List.map (fun s -> `String s) props)) :: fields
112112+ | None -> fields
113113+ in
114114+ `O fields
115115+116116+ (** Parse response from JSON.
117117+ Test files: test/data/core/response_get.json *)
118118+ let response_of_json parse_obj json =
119119+ let open Helpers in
120120+ let fields = expect_object json in
121121+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
122122+ let state = get_string "state" fields in
123123+ let list = parse_array parse_obj (require_field "list" fields) in
124124+ let not_found = match find_field "notFound" fields with
125125+ | Some v -> parse_array Jmap_id.of_json v
126126+ | None -> []
127127+ in
128128+ { account_id; state; list; not_found }
129129+end
130130+131131+(** Standard /changes method (RFC 8620 Section 5.2) *)
132132+module Changes = struct
133133+ type request = {
134134+ account_id : Jmap_id.t;
135135+ since_state : string;
136136+ max_changes : Jmap_primitives.UnsignedInt.t option;
137137+ }
138138+139139+ type response = {
140140+ account_id : Jmap_id.t;
141141+ old_state : string;
142142+ new_state : string;
143143+ has_more_changes : bool;
144144+ created : Jmap_id.t list;
145145+ updated : Jmap_id.t list;
146146+ destroyed : Jmap_id.t list;
147147+ }
148148+149149+ (** Accessors for request *)
150150+ let account_id (r : request) = r.account_id
151151+ let since_state (r : request) = r.since_state
152152+ let max_changes (r : request) = r.max_changes
153153+154154+ (** Constructor for request *)
155155+ let v ~account_id ~since_state ?max_changes () =
156156+ { account_id; since_state; max_changes }
157157+158158+ (** Accessors for response *)
159159+ let response_account_id (r : response) = r.account_id
160160+ let old_state (r : response) = r.old_state
161161+ let new_state (r : response) = r.new_state
162162+ let has_more_changes (r : response) = r.has_more_changes
163163+ let created (r : response) = r.created
164164+ let updated (r : response) = r.updated
165165+ let destroyed (r : response) = r.destroyed
166166+167167+ (** Constructor for response *)
168168+ let response_v ~account_id ~old_state ~new_state ~has_more_changes ~created ~updated ~destroyed =
169169+ { account_id; old_state; new_state; has_more_changes; created; updated; destroyed }
170170+171171+ (** Parse request from JSON.
172172+ Test files: test/data/core/request_changes.json *)
173173+ let request_of_json json =
174174+ let open Helpers in
175175+ let fields = expect_object json in
176176+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
177177+ let since_state = get_string "sinceState" fields in
178178+ let max_changes = match find_field "maxChanges" fields with
179179+ | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
180180+ | None -> None
181181+ in
182182+ { account_id; since_state; max_changes }
183183+184184+ (** Parse response from JSON.
185185+ Test files: test/data/core/response_changes.json *)
186186+ let response_of_json json =
187187+ let open Helpers in
188188+ let fields = expect_object json in
189189+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
190190+ let old_state = get_string "oldState" fields in
191191+ let new_state = get_string "newState" fields in
192192+ let has_more_changes = get_bool "hasMoreChanges" fields in
193193+ let created = parse_array Jmap_id.of_json (require_field "created" fields) in
194194+ let updated = parse_array Jmap_id.of_json (require_field "updated" fields) in
195195+ let destroyed = parse_array Jmap_id.of_json (require_field "destroyed" fields) in
196196+ { account_id; old_state; new_state; has_more_changes; created; updated; destroyed }
197197+end
198198+199199+(** Standard /set method (RFC 8620 Section 5.3) *)
200200+module Set = struct
201201+ (** PatchObject - JSON Pointer paths to values *)
202202+ type patch_object = (string * Ezjsonm.value option) list
203203+204204+ type 'a request = {
205205+ account_id : Jmap_id.t;
206206+ if_in_state : string option;
207207+ create : (Jmap_id.t * 'a) list option;
208208+ update : (Jmap_id.t * patch_object) list option;
209209+ destroy : Jmap_id.t list option;
210210+ }
211211+212212+ type 'a response = {
213213+ account_id : Jmap_id.t;
214214+ old_state : string option;
215215+ new_state : string;
216216+ created : (Jmap_id.t * 'a) list option;
217217+ updated : (Jmap_id.t * 'a option) list option;
218218+ destroyed : Jmap_id.t list option;
219219+ not_created : (Jmap_id.t * Jmap_error.set_error_detail) list option;
220220+ not_updated : (Jmap_id.t * Jmap_error.set_error_detail) list option;
221221+ not_destroyed : (Jmap_id.t * Jmap_error.set_error_detail) list option;
222222+ }
223223+224224+ (** Accessors for request *)
225225+ let account_id (r : 'a request) = r.account_id
226226+ let if_in_state (r : 'a request) = r.if_in_state
227227+ let create (r : 'a request) = r.create
228228+ let update (r : 'a request) = r.update
229229+ let destroy (r : 'a request) = r.destroy
230230+231231+ (** Constructor for request *)
232232+ let v ~account_id ?if_in_state ?create ?update ?destroy () =
233233+ { account_id; if_in_state; create; update; destroy }
234234+235235+ (** Accessors for response *)
236236+ let response_account_id (r : 'a response) = r.account_id
237237+ let old_state (r : 'a response) = r.old_state
238238+ let new_state (r : 'a response) = r.new_state
239239+ let created (r : 'a response) = r.created
240240+ let updated (r : 'a response) = r.updated
241241+ let destroyed (r : 'a response) = r.destroyed
242242+ let not_created (r : 'a response) = r.not_created
243243+ let not_updated (r : 'a response) = r.not_updated
244244+ let not_destroyed (r : 'a response) = r.not_destroyed
245245+246246+ (** Constructor for response *)
247247+ let response_v ~account_id ?old_state ~new_state ?created ?updated ?destroyed ?not_created ?not_updated ?not_destroyed () =
248248+ { account_id; old_state; new_state; created; updated; destroyed; not_created; not_updated; not_destroyed }
249249+250250+ (** Parse request from JSON.
251251+ Test files:
252252+ - test/data/core/request_set_create.json
253253+ - test/data/core/request_set_update.json
254254+ - test/data/core/request_set_destroy.json
255255+ *)
256256+ let request_of_json parse_obj json =
257257+ let open Helpers in
258258+ let fields = expect_object json in
259259+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
260260+ let if_in_state = get_string_opt "ifInState" fields in
261261+ let create = match find_field "create" fields with
262262+ | Some `Null | None -> None
263263+ | Some (`O pairs) ->
264264+ Some (List.map (fun (k, v) -> (Jmap_id.of_string k, parse_obj v)) pairs)
265265+ | Some _ -> raise (Jmap_error.Parse_error "create must be an object")
266266+ in
267267+ let update = match find_field "update" fields with
268268+ | Some `Null | None -> None
269269+ | Some (`O pairs) ->
270270+ Some (List.map (fun (k, v) ->
271271+ let id = Jmap_id.of_string k in
272272+ let patch = match v with
273273+ | `O patch_fields ->
274274+ List.map (fun (pk, pv) ->
275275+ match pv with
276276+ | `Null -> (pk, None)
277277+ | _ -> (pk, Some pv)
278278+ ) patch_fields
279279+ | _ -> raise (Jmap_error.Parse_error "update value must be an object")
280280+ in
281281+ (id, patch)
282282+ ) pairs)
283283+ | Some _ -> raise (Jmap_error.Parse_error "update must be an object")
284284+ in
285285+ let destroy = match find_field "destroy" fields with
286286+ | Some `Null | None -> None
287287+ | Some v -> Some (parse_array Jmap_id.of_json v)
288288+ in
289289+ { account_id; if_in_state; create; update; destroy }
290290+291291+ (** Parse response from JSON.
292292+ Test files:
293293+ - test/data/core/response_set_create.json
294294+ - test/data/core/response_set_update.json
295295+ - test/data/core/response_set_destroy.json
296296+ *)
297297+ let response_of_json parse_obj json =
298298+ let open Helpers in
299299+ let fields = expect_object json in
300300+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
301301+ let old_state = get_string_opt "oldState" fields in
302302+ let new_state = get_string "newState" fields in
303303+ let created = match find_field "created" fields with
304304+ | Some `Null | None -> None
305305+ | Some (`O pairs) ->
306306+ Some (List.map (fun (k, v) -> (Jmap_id.of_string k, parse_obj v)) pairs)
307307+ | Some _ -> raise (Jmap_error.Parse_error "created must be an object")
308308+ in
309309+ let updated = match find_field "updated" fields with
310310+ | Some `Null | None -> None
311311+ | Some (`O pairs) ->
312312+ Some (List.map (fun (k, v) ->
313313+ let id = Jmap_id.of_string k in
314314+ match v with
315315+ | `Null -> (id, None)
316316+ | _ -> (id, Some (parse_obj v))
317317+ ) pairs)
318318+ | Some _ -> raise (Jmap_error.Parse_error "updated must be an object")
319319+ in
320320+ let destroyed = match find_field "destroyed" fields with
321321+ | Some `Null | None -> None
322322+ | Some v -> Some (parse_array Jmap_id.of_json v)
323323+ in
324324+ let not_created = match find_field "notCreated" fields with
325325+ | Some `Null | None -> None
326326+ | Some (`O pairs) ->
327327+ Some (List.map (fun (k, v) ->
328328+ (Jmap_id.of_string k, Jmap_error.parse_set_error_detail v)
329329+ ) pairs)
330330+ | Some _ -> raise (Jmap_error.Parse_error "notCreated must be an object")
331331+ in
332332+ let not_updated = match find_field "notUpdated" fields with
333333+ | Some `Null | None -> None
334334+ | Some (`O pairs) ->
335335+ Some (List.map (fun (k, v) ->
336336+ (Jmap_id.of_string k, Jmap_error.parse_set_error_detail v)
337337+ ) pairs)
338338+ | Some _ -> raise (Jmap_error.Parse_error "notUpdated must be an object")
339339+ in
340340+ let not_destroyed = match find_field "notDestroyed" fields with
341341+ | Some `Null | None -> None
342342+ | Some (`O pairs) ->
343343+ Some (List.map (fun (k, v) ->
344344+ (Jmap_id.of_string k, Jmap_error.parse_set_error_detail v)
345345+ ) pairs)
346346+ | Some _ -> raise (Jmap_error.Parse_error "notDestroyed must be an object")
347347+ in
348348+ { account_id; old_state; new_state; created; updated; destroyed;
349349+ not_created; not_updated; not_destroyed }
350350+end
351351+352352+(** Standard /copy method (RFC 8620 Section 5.4) *)
353353+module Copy = struct
354354+ type 'a request = {
355355+ from_account_id : Jmap_id.t;
356356+ if_from_in_state : string option;
357357+ account_id : Jmap_id.t;
358358+ if_in_state : string option;
359359+ create : (Jmap_id.t * 'a) list; (** Each object must include source id *)
360360+ on_success_destroy_original : bool option;
361361+ destroy_from_if_in_state : string option;
362362+ }
363363+364364+ type 'a response = {
365365+ from_account_id : Jmap_id.t;
366366+ account_id : Jmap_id.t;
367367+ old_state : string option;
368368+ new_state : string;
369369+ created : (Jmap_id.t * 'a) list option;
370370+ not_created : (Jmap_id.t * Jmap_error.set_error_detail) list option;
371371+ }
372372+373373+ (** Accessors for request *)
374374+ let from_account_id (r : 'a request) = r.from_account_id
375375+ let if_from_in_state (r : 'a request) = r.if_from_in_state
376376+ let account_id (r : 'a request) = r.account_id
377377+ let if_in_state (r : 'a request) = r.if_in_state
378378+ let create (r : 'a request) = r.create
379379+ let on_success_destroy_original (r : 'a request) = r.on_success_destroy_original
380380+ let destroy_from_if_in_state (r : 'a request) = r.destroy_from_if_in_state
381381+382382+ (** Constructor for request *)
383383+ let v ~from_account_id ?if_from_in_state ~account_id ?if_in_state ~create ?on_success_destroy_original ?destroy_from_if_in_state () =
384384+ { from_account_id; if_from_in_state; account_id; if_in_state; create; on_success_destroy_original; destroy_from_if_in_state }
385385+386386+ (** Accessors for response *)
387387+ let response_from_account_id (r : 'a response) = r.from_account_id
388388+ let response_account_id (r : 'a response) = r.account_id
389389+ let old_state (r : 'a response) = r.old_state
390390+ let new_state (r : 'a response) = r.new_state
391391+ let created (r : 'a response) = r.created
392392+ let not_created (r : 'a response) = r.not_created
393393+394394+ (** Constructor for response *)
395395+ let response_v ~from_account_id ~account_id ?old_state ~new_state ?created ?not_created () =
396396+ { from_account_id; account_id; old_state; new_state; created; not_created }
397397+398398+ (** Parse request from JSON.
399399+ Test files: test/data/core/request_copy.json *)
400400+ let request_of_json _parse_obj _json =
401401+ (* TODO: Implement JSON parsing *)
402402+ raise (Jmap_error.Parse_error "Copy.request_of_json not yet implemented")
403403+404404+ (** Parse response from JSON.
405405+ Test files: test/data/core/response_copy.json *)
406406+ let response_of_json _parse_obj _json =
407407+ (* TODO: Implement JSON parsing *)
408408+ raise (Jmap_error.Parse_error "Copy.response_of_json not yet implemented")
409409+end
410410+411411+(** Standard /query method (RFC 8620 Section 5.5) *)
412412+module Query = struct
413413+ type 'filter request = {
414414+ account_id : Jmap_id.t;
415415+ filter : 'filter Jmap_filter.t option;
416416+ sort : Jmap_comparator.t list option;
417417+ position : Jmap_primitives.Int53.t option;
418418+ anchor : Jmap_id.t option;
419419+ anchor_offset : Jmap_primitives.Int53.t option;
420420+ limit : Jmap_primitives.UnsignedInt.t option;
421421+ calculate_total : bool option;
422422+ }
423423+424424+ type response = {
425425+ account_id : Jmap_id.t;
426426+ query_state : string;
427427+ can_calculate_changes : bool;
428428+ position : Jmap_primitives.UnsignedInt.t;
429429+ ids : Jmap_id.t list;
430430+ total : Jmap_primitives.UnsignedInt.t option; (** Only if calculateTotal=true *)
431431+ limit : Jmap_primitives.UnsignedInt.t option; (** If server limited results *)
432432+ }
433433+434434+ (** Accessors for request *)
435435+ let account_id (r : 'f request) = r.account_id
436436+ let filter (r : 'f request) = r.filter
437437+ let sort (r : 'f request) = r.sort
438438+ let position (r : 'f request) = r.position
439439+ let anchor (r : 'f request) = r.anchor
440440+ let anchor_offset (r : 'f request) = r.anchor_offset
441441+ let limit (r : 'f request) = r.limit
442442+ let calculate_total (r : 'f request) = r.calculate_total
443443+444444+ (** Constructor for request *)
445445+ let v ~account_id ?filter ?sort ?position ?anchor ?anchor_offset ?limit ?calculate_total () =
446446+ { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
447447+448448+ (** Accessors for response *)
449449+ let response_account_id (r : response) = r.account_id
450450+ let query_state (r : response) = r.query_state
451451+ let can_calculate_changes (r : response) = r.can_calculate_changes
452452+ let response_position (r : response) = r.position
453453+ let ids (r : response) = r.ids
454454+ let total (r : response) = r.total
455455+ let response_limit (r : response) = r.limit
456456+457457+ (** Constructor for response *)
458458+ let response_v ~account_id ~query_state ~can_calculate_changes ~position ~ids ?total ?limit () =
459459+ { account_id; query_state; can_calculate_changes; position; ids; total; limit }
460460+461461+ (** Parse request from JSON.
462462+ Test files: test/data/core/request_query.json *)
463463+ let request_of_json parse_filter json =
464464+ let open Helpers in
465465+ let fields = expect_object json in
466466+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
467467+ let filter = match find_field "filter" fields with
468468+ | Some v -> Some (Jmap_filter.of_json parse_filter v)
469469+ | None -> None
470470+ in
471471+ let sort = match find_field "sort" fields with
472472+ | Some v -> Some (parse_array Jmap_comparator.of_json v)
473473+ | None -> None
474474+ in
475475+ let position = match find_field "position" fields with
476476+ | Some v -> Some (Jmap_primitives.Int53.of_json v)
477477+ | None -> None
478478+ in
479479+ let anchor = match find_field "anchor" fields with
480480+ | Some v -> Some (Jmap_id.of_json v)
481481+ | None -> None
482482+ in
483483+ let anchor_offset = match find_field "anchorOffset" fields with
484484+ | Some v -> Some (Jmap_primitives.Int53.of_json v)
485485+ | None -> None
486486+ in
487487+ let limit = match find_field "limit" fields with
488488+ | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
489489+ | None -> None
490490+ in
491491+ let calculate_total = match find_field "calculateTotal" fields with
492492+ | Some (`Bool b) -> Some b
493493+ | Some _ -> raise (Jmap_error.Parse_error "calculateTotal must be a boolean")
494494+ | None -> None
495495+ in
496496+ { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total }
497497+498498+ (** Parse response from JSON.
499499+ Test files: test/data/core/response_query.json *)
500500+ let response_of_json json =
501501+ let open Helpers in
502502+ let fields = expect_object json in
503503+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
504504+ let query_state = get_string "queryState" fields in
505505+ let can_calculate_changes = get_bool "canCalculateChanges" fields in
506506+ let position = Jmap_primitives.UnsignedInt.of_json (require_field "position" fields) in
507507+ let ids = parse_array Jmap_id.of_json (require_field "ids" fields) in
508508+ let total = match find_field "total" fields with
509509+ | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
510510+ | None -> None
511511+ in
512512+ let limit = match find_field "limit" fields with
513513+ | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
514514+ | None -> None
515515+ in
516516+ { account_id; query_state; can_calculate_changes; position; ids; total; limit }
517517+end
518518+519519+(** Standard /queryChanges method (RFC 8620 Section 5.6) *)
520520+module QueryChanges = struct
521521+ (** Item added to query results *)
522522+ type added_item = {
523523+ id : Jmap_id.t;
524524+ index : Jmap_primitives.UnsignedInt.t;
525525+ }
526526+527527+ type 'filter request = {
528528+ account_id : Jmap_id.t;
529529+ filter : 'filter Jmap_filter.t option;
530530+ sort : Jmap_comparator.t list option;
531531+ since_query_state : string;
532532+ max_changes : Jmap_primitives.UnsignedInt.t option;
533533+ up_to_id : Jmap_id.t option;
534534+ calculate_total : bool option;
535535+ }
536536+537537+ type response = {
538538+ account_id : Jmap_id.t;
539539+ old_query_state : string;
540540+ new_query_state : string;
541541+ total : Jmap_primitives.UnsignedInt.t option;
542542+ removed : Jmap_id.t list;
543543+ added : added_item list;
544544+ }
545545+546546+ (** Accessors for added_item *)
547547+ let added_item_id a = a.id
548548+ let added_item_index a = a.index
549549+550550+ (** Constructor for added_item *)
551551+ let added_item_v ~id ~index = { id; index }
552552+553553+ (** Accessors for request *)
554554+ let account_id (r : 'f request) = r.account_id
555555+ let filter (r : 'f request) = r.filter
556556+ let sort (r : 'f request) = r.sort
557557+ let since_query_state (r : 'f request) = r.since_query_state
558558+ let max_changes (r : 'f request) = r.max_changes
559559+ let up_to_id (r : 'f request) = r.up_to_id
560560+ let calculate_total (r : 'f request) = r.calculate_total
561561+562562+ (** Constructor for request *)
563563+ let v ~account_id ?filter ?sort ~since_query_state ?max_changes ?up_to_id ?calculate_total () =
564564+ { account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total }
565565+566566+ (** Accessors for response *)
567567+ let response_account_id (r : response) = r.account_id
568568+ let old_query_state (r : response) = r.old_query_state
569569+ let new_query_state (r : response) = r.new_query_state
570570+ let total (r : response) = r.total
571571+ let removed (r : response) = r.removed
572572+ let added (r : response) = r.added
573573+574574+ (** Constructor for response *)
575575+ let response_v ~account_id ~old_query_state ~new_query_state ?total ~removed ~added () =
576576+ { account_id; old_query_state; new_query_state; total; removed; added }
577577+578578+ (** Parse request from JSON.
579579+ Test files: test/data/core/request_query_changes.json *)
580580+ let request_of_json parse_filter json =
581581+ let open Helpers in
582582+ let fields = expect_object json in
583583+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
584584+ let filter = match find_field "filter" fields with
585585+ | Some v -> Some (Jmap_filter.of_json parse_filter v)
586586+ | None -> None
587587+ in
588588+ let sort = match find_field "sort" fields with
589589+ | Some v -> Some (parse_array Jmap_comparator.of_json v)
590590+ | None -> None
591591+ in
592592+ let since_query_state = get_string "sinceQueryState" fields in
593593+ let max_changes = match find_field "maxChanges" fields with
594594+ | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
595595+ | None -> None
596596+ in
597597+ let up_to_id = match find_field "upToId" fields with
598598+ | Some v -> Some (Jmap_id.of_json v)
599599+ | None -> None
600600+ in
601601+ let calculate_total = match find_field "calculateTotal" fields with
602602+ | Some (`Bool b) -> Some b
603603+ | Some _ -> raise (Jmap_error.Parse_error "calculateTotal must be a boolean")
604604+ | None -> None
605605+ in
606606+ { account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total }
607607+608608+ (** Parse response from JSON.
609609+ Test files: test/data/core/response_query_changes.json *)
610610+ let response_of_json json =
611611+ let open Helpers in
612612+ let fields = expect_object json in
613613+ let account_id = Jmap_id.of_json (require_field "accountId" fields) in
614614+ let old_query_state = get_string "oldQueryState" fields in
615615+ let new_query_state = get_string "newQueryState" fields in
616616+ let total = match find_field "total" fields with
617617+ | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v)
618618+ | None -> None
619619+ in
620620+ let removed = parse_array Jmap_id.of_json (require_field "removed" fields) in
621621+ let added = match require_field "added" fields with
622622+ | `A items ->
623623+ List.map (fun item ->
624624+ match item with
625625+ | `O item_fields ->
626626+ let id = Jmap_id.of_json (require_field "id" item_fields) in
627627+ let index = Jmap_primitives.UnsignedInt.of_json (require_field "index" item_fields) in
628628+ { id; index }
629629+ | _ -> raise (Jmap_error.Parse_error "Added item must be an object")
630630+ ) items
631631+ | _ -> raise (Jmap_error.Parse_error "added must be an array")
632632+ in
633633+ { account_id; old_query_state; new_query_state; total; removed; added }
634634+end
635635+636636+(** Core/echo method (RFC 8620 Section 7.3) *)
637637+module Echo = struct
638638+ (** Echo simply returns the arguments unchanged *)
639639+ type t = Ezjsonm.value
640640+641641+ (** Test files:
642642+ - test/data/core/request_echo.json
643643+ - test/data/core/response_echo.json *)
644644+ let of_json json = json
645645+ let to_json t = t
646646+end
+402
jmap/jmap-core/jmap_standard_methods.mli
···11+(** JMAP Standard Method Types *)
22+33+(** Standard /get method (RFC 8620 Section 5.1) *)
44+module Get : sig
55+ type 'a request = {
66+ account_id : Jmap_id.t;
77+ ids : Jmap_id.t list option;
88+ properties : string list option;
99+ }
1010+1111+ type 'a response = {
1212+ account_id : Jmap_id.t;
1313+ state : string;
1414+ list : 'a list;
1515+ not_found : Jmap_id.t list;
1616+ }
1717+1818+ (** Accessors for request *)
1919+ val account_id : 'a request -> Jmap_id.t
2020+ val ids : 'a request -> Jmap_id.t list option
2121+ val properties : 'a request -> string list option
2222+2323+ (** Constructor for request *)
2424+ val v : account_id:Jmap_id.t -> ?ids:Jmap_id.t list -> ?properties:string list -> unit -> 'a request
2525+2626+ (** Accessors for response *)
2727+ val response_account_id : 'a response -> Jmap_id.t
2828+ val state : 'a response -> string
2929+ val list : 'a response -> 'a list
3030+ val not_found : 'a response -> Jmap_id.t list
3131+3232+ (** Constructor for response *)
3333+ val response_v : account_id:Jmap_id.t -> state:string -> list:'a list -> not_found:Jmap_id.t list -> 'a response
3434+3535+ (** Convert request to JSON *)
3636+ val request_to_json : 'a request -> Ezjsonm.value
3737+3838+ (** Parse request from JSON *)
3939+ val request_of_json : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a request
4040+4141+ (** Parse response from JSON *)
4242+ val response_of_json : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a response
4343+end
4444+4545+(** Standard /changes method (RFC 8620 Section 5.2) *)
4646+module Changes : sig
4747+ type request = {
4848+ account_id : Jmap_id.t;
4949+ since_state : string;
5050+ max_changes : Jmap_primitives.UnsignedInt.t option;
5151+ }
5252+5353+ type response = {
5454+ account_id : Jmap_id.t;
5555+ old_state : string;
5656+ new_state : string;
5757+ has_more_changes : bool;
5858+ created : Jmap_id.t list;
5959+ updated : Jmap_id.t list;
6060+ destroyed : Jmap_id.t list;
6161+ }
6262+6363+ (** Accessors for request *)
6464+ val account_id : request -> Jmap_id.t
6565+ val since_state : request -> string
6666+ val max_changes : request -> Jmap_primitives.UnsignedInt.t option
6767+6868+ (** Constructor for request *)
6969+ val v : account_id:Jmap_id.t -> since_state:string -> ?max_changes:Jmap_primitives.UnsignedInt.t -> unit -> request
7070+7171+ (** Accessors for response *)
7272+ val response_account_id : response -> Jmap_id.t
7373+ val old_state : response -> string
7474+ val new_state : response -> string
7575+ val has_more_changes : response -> bool
7676+ val created : response -> Jmap_id.t list
7777+ val updated : response -> Jmap_id.t list
7878+ val destroyed : response -> Jmap_id.t list
7979+8080+ (** Constructor for response *)
8181+ val response_v :
8282+ account_id:Jmap_id.t ->
8383+ old_state:string ->
8484+ new_state:string ->
8585+ has_more_changes:bool ->
8686+ created:Jmap_id.t list ->
8787+ updated:Jmap_id.t list ->
8888+ destroyed:Jmap_id.t list ->
8989+ response
9090+9191+ (** Parse request from JSON *)
9292+ val request_of_json : Ezjsonm.value -> request
9393+9494+ (** Parse response from JSON *)
9595+ val response_of_json : Ezjsonm.value -> response
9696+end
9797+9898+(** Standard /set method (RFC 8620 Section 5.3) *)
9999+module Set : sig
100100+ (** PatchObject - JSON Pointer paths to values *)
101101+ type patch_object = (string * Ezjsonm.value option) list
102102+103103+ type 'a request = {
104104+ account_id : Jmap_id.t;
105105+ if_in_state : string option;
106106+ create : (Jmap_id.t * 'a) list option;
107107+ update : (Jmap_id.t * patch_object) list option;
108108+ destroy : Jmap_id.t list option;
109109+ }
110110+111111+ type 'a response = {
112112+ account_id : Jmap_id.t;
113113+ old_state : string option;
114114+ new_state : string;
115115+ created : (Jmap_id.t * 'a) list option;
116116+ updated : (Jmap_id.t * 'a option) list option;
117117+ destroyed : Jmap_id.t list option;
118118+ not_created : (Jmap_id.t * Jmap_error.set_error_detail) list option;
119119+ not_updated : (Jmap_id.t * Jmap_error.set_error_detail) list option;
120120+ not_destroyed : (Jmap_id.t * Jmap_error.set_error_detail) list option;
121121+ }
122122+123123+ (** Accessors for request *)
124124+ val account_id : 'a request -> Jmap_id.t
125125+ val if_in_state : 'a request -> string option
126126+ val create : 'a request -> (Jmap_id.t * 'a) list option
127127+ val update : 'a request -> (Jmap_id.t * patch_object) list option
128128+ val destroy : 'a request -> Jmap_id.t list option
129129+130130+ (** Constructor for request *)
131131+ val v :
132132+ account_id:Jmap_id.t ->
133133+ ?if_in_state:string ->
134134+ ?create:(Jmap_id.t * 'a) list ->
135135+ ?update:(Jmap_id.t * patch_object) list ->
136136+ ?destroy:Jmap_id.t list ->
137137+ unit ->
138138+ 'a request
139139+140140+ (** Accessors for response *)
141141+ val response_account_id : 'a response -> Jmap_id.t
142142+ val old_state : 'a response -> string option
143143+ val new_state : 'a response -> string
144144+ val created : 'a response -> (Jmap_id.t * 'a) list option
145145+ val updated : 'a response -> (Jmap_id.t * 'a option) list option
146146+ val destroyed : 'a response -> Jmap_id.t list option
147147+ val not_created : 'a response -> (Jmap_id.t * Jmap_error.set_error_detail) list option
148148+ val not_updated : 'a response -> (Jmap_id.t * Jmap_error.set_error_detail) list option
149149+ val not_destroyed : 'a response -> (Jmap_id.t * Jmap_error.set_error_detail) list option
150150+151151+ (** Constructor for response *)
152152+ val response_v :
153153+ account_id:Jmap_id.t ->
154154+ ?old_state:string ->
155155+ new_state:string ->
156156+ ?created:(Jmap_id.t * 'a) list ->
157157+ ?updated:(Jmap_id.t * 'a option) list ->
158158+ ?destroyed:Jmap_id.t list ->
159159+ ?not_created:(Jmap_id.t * Jmap_error.set_error_detail) list ->
160160+ ?not_updated:(Jmap_id.t * Jmap_error.set_error_detail) list ->
161161+ ?not_destroyed:(Jmap_id.t * Jmap_error.set_error_detail) list ->
162162+ unit ->
163163+ 'a response
164164+165165+ (** Parse request from JSON *)
166166+ val request_of_json : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a request
167167+168168+ (** Parse response from JSON *)
169169+ val response_of_json : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a response
170170+end
171171+172172+(** Standard /copy method (RFC 8620 Section 5.4) *)
173173+module Copy : sig
174174+ type 'a request = {
175175+ from_account_id : Jmap_id.t;
176176+ if_from_in_state : string option;
177177+ account_id : Jmap_id.t;
178178+ if_in_state : string option;
179179+ create : (Jmap_id.t * 'a) list;
180180+ on_success_destroy_original : bool option;
181181+ destroy_from_if_in_state : string option;
182182+ }
183183+184184+ type 'a response = {
185185+ from_account_id : Jmap_id.t;
186186+ account_id : Jmap_id.t;
187187+ old_state : string option;
188188+ new_state : string;
189189+ created : (Jmap_id.t * 'a) list option;
190190+ not_created : (Jmap_id.t * Jmap_error.set_error_detail) list option;
191191+ }
192192+193193+ (** Accessors for request *)
194194+ val from_account_id : 'a request -> Jmap_id.t
195195+ val if_from_in_state : 'a request -> string option
196196+ val account_id : 'a request -> Jmap_id.t
197197+ val if_in_state : 'a request -> string option
198198+ val create : 'a request -> (Jmap_id.t * 'a) list
199199+ val on_success_destroy_original : 'a request -> bool option
200200+ val destroy_from_if_in_state : 'a request -> string option
201201+202202+ (** Constructor for request *)
203203+ val v :
204204+ from_account_id:Jmap_id.t ->
205205+ ?if_from_in_state:string ->
206206+ account_id:Jmap_id.t ->
207207+ ?if_in_state:string ->
208208+ create:(Jmap_id.t * 'a) list ->
209209+ ?on_success_destroy_original:bool ->
210210+ ?destroy_from_if_in_state:string ->
211211+ unit ->
212212+ 'a request
213213+214214+ (** Accessors for response *)
215215+ val response_from_account_id : 'a response -> Jmap_id.t
216216+ val response_account_id : 'a response -> Jmap_id.t
217217+ val old_state : 'a response -> string option
218218+ val new_state : 'a response -> string
219219+ val created : 'a response -> (Jmap_id.t * 'a) list option
220220+ val not_created : 'a response -> (Jmap_id.t * Jmap_error.set_error_detail) list option
221221+222222+ (** Constructor for response *)
223223+ val response_v :
224224+ from_account_id:Jmap_id.t ->
225225+ account_id:Jmap_id.t ->
226226+ ?old_state:string ->
227227+ new_state:string ->
228228+ ?created:(Jmap_id.t * 'a) list ->
229229+ ?not_created:(Jmap_id.t * Jmap_error.set_error_detail) list ->
230230+ unit ->
231231+ 'a response
232232+233233+ (** Parse request from JSON *)
234234+ val request_of_json : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a request
235235+236236+ (** Parse response from JSON *)
237237+ val response_of_json : (Ezjsonm.value -> 'a) -> Ezjsonm.value -> 'a response
238238+end
239239+240240+(** Standard /query method (RFC 8620 Section 5.5) *)
241241+module Query : sig
242242+ type 'filter request = {
243243+ account_id : Jmap_id.t;
244244+ filter : 'filter Jmap_filter.t option;
245245+ sort : Jmap_comparator.t list option;
246246+ position : Jmap_primitives.Int53.t option;
247247+ anchor : Jmap_id.t option;
248248+ anchor_offset : Jmap_primitives.Int53.t option;
249249+ limit : Jmap_primitives.UnsignedInt.t option;
250250+ calculate_total : bool option;
251251+ }
252252+253253+ type response = {
254254+ account_id : Jmap_id.t;
255255+ query_state : string;
256256+ can_calculate_changes : bool;
257257+ position : Jmap_primitives.UnsignedInt.t;
258258+ ids : Jmap_id.t list;
259259+ total : Jmap_primitives.UnsignedInt.t option;
260260+ limit : Jmap_primitives.UnsignedInt.t option;
261261+ }
262262+263263+ (** Accessors for request *)
264264+ val account_id : 'filter request -> Jmap_id.t
265265+ val filter : 'filter request -> 'filter Jmap_filter.t option
266266+ val sort : 'filter request -> Jmap_comparator.t list option
267267+ val position : 'filter request -> Jmap_primitives.Int53.t option
268268+ val anchor : 'filter request -> Jmap_id.t option
269269+ val anchor_offset : 'filter request -> Jmap_primitives.Int53.t option
270270+ val limit : 'filter request -> Jmap_primitives.UnsignedInt.t option
271271+ val calculate_total : 'filter request -> bool option
272272+273273+ (** Constructor for request *)
274274+ val v :
275275+ account_id:Jmap_id.t ->
276276+ ?filter:'filter Jmap_filter.t ->
277277+ ?sort:Jmap_comparator.t list ->
278278+ ?position:Jmap_primitives.Int53.t ->
279279+ ?anchor:Jmap_id.t ->
280280+ ?anchor_offset:Jmap_primitives.Int53.t ->
281281+ ?limit:Jmap_primitives.UnsignedInt.t ->
282282+ ?calculate_total:bool ->
283283+ unit ->
284284+ 'filter request
285285+286286+ (** Accessors for response *)
287287+ val response_account_id : response -> Jmap_id.t
288288+ val query_state : response -> string
289289+ val can_calculate_changes : response -> bool
290290+ val response_position : response -> Jmap_primitives.UnsignedInt.t
291291+ val ids : response -> Jmap_id.t list
292292+ val total : response -> Jmap_primitives.UnsignedInt.t option
293293+ val response_limit : response -> Jmap_primitives.UnsignedInt.t option
294294+295295+ (** Constructor for response *)
296296+ val response_v :
297297+ account_id:Jmap_id.t ->
298298+ query_state:string ->
299299+ can_calculate_changes:bool ->
300300+ position:Jmap_primitives.UnsignedInt.t ->
301301+ ids:Jmap_id.t list ->
302302+ ?total:Jmap_primitives.UnsignedInt.t ->
303303+ ?limit:Jmap_primitives.UnsignedInt.t ->
304304+ unit ->
305305+ response
306306+307307+ (** Parse request from JSON *)
308308+ val request_of_json : (Ezjsonm.value -> 'filter) -> Ezjsonm.value -> 'filter request
309309+310310+ (** Parse response from JSON *)
311311+ val response_of_json : Ezjsonm.value -> response
312312+end
313313+314314+(** Standard /queryChanges method (RFC 8620 Section 5.6) *)
315315+module QueryChanges : sig
316316+ (** Item added to query results *)
317317+ type added_item = {
318318+ id : Jmap_id.t;
319319+ index : Jmap_primitives.UnsignedInt.t;
320320+ }
321321+322322+ type 'filter request = {
323323+ account_id : Jmap_id.t;
324324+ filter : 'filter Jmap_filter.t option;
325325+ sort : Jmap_comparator.t list option;
326326+ since_query_state : string;
327327+ max_changes : Jmap_primitives.UnsignedInt.t option;
328328+ up_to_id : Jmap_id.t option;
329329+ calculate_total : bool option;
330330+ }
331331+332332+ type response = {
333333+ account_id : Jmap_id.t;
334334+ old_query_state : string;
335335+ new_query_state : string;
336336+ total : Jmap_primitives.UnsignedInt.t option;
337337+ removed : Jmap_id.t list;
338338+ added : added_item list;
339339+ }
340340+341341+ (** Accessors for added_item *)
342342+ val added_item_id : added_item -> Jmap_id.t
343343+ val added_item_index : added_item -> Jmap_primitives.UnsignedInt.t
344344+345345+ (** Constructor for added_item *)
346346+ val added_item_v : id:Jmap_id.t -> index:Jmap_primitives.UnsignedInt.t -> added_item
347347+348348+ (** Accessors for request *)
349349+ val account_id : 'filter request -> Jmap_id.t
350350+ val filter : 'filter request -> 'filter Jmap_filter.t option
351351+ val sort : 'filter request -> Jmap_comparator.t list option
352352+ val since_query_state : 'filter request -> string
353353+ val max_changes : 'filter request -> Jmap_primitives.UnsignedInt.t option
354354+ val up_to_id : 'filter request -> Jmap_id.t option
355355+ val calculate_total : 'filter request -> bool option
356356+357357+ (** Constructor for request *)
358358+ val v :
359359+ account_id:Jmap_id.t ->
360360+ ?filter:'filter Jmap_filter.t ->
361361+ ?sort:Jmap_comparator.t list ->
362362+ since_query_state:string ->
363363+ ?max_changes:Jmap_primitives.UnsignedInt.t ->
364364+ ?up_to_id:Jmap_id.t ->
365365+ ?calculate_total:bool ->
366366+ unit ->
367367+ 'filter request
368368+369369+ (** Accessors for response *)
370370+ val response_account_id : response -> Jmap_id.t
371371+ val old_query_state : response -> string
372372+ val new_query_state : response -> string
373373+ val total : response -> Jmap_primitives.UnsignedInt.t option
374374+ val removed : response -> Jmap_id.t list
375375+ val added : response -> added_item list
376376+377377+ (** Constructor for response *)
378378+ val response_v :
379379+ account_id:Jmap_id.t ->
380380+ old_query_state:string ->
381381+ new_query_state:string ->
382382+ ?total:Jmap_primitives.UnsignedInt.t ->
383383+ removed:Jmap_id.t list ->
384384+ added:added_item list ->
385385+ unit ->
386386+ response
387387+388388+ (** Parse request from JSON *)
389389+ val request_of_json : (Ezjsonm.value -> 'filter) -> Ezjsonm.value -> 'filter request
390390+391391+ (** Parse response from JSON *)
392392+ val response_of_json : Ezjsonm.value -> response
393393+end
394394+395395+(** Core/echo method (RFC 8620 Section 7.3) *)
396396+module Echo : sig
397397+ (** Echo simply returns the arguments unchanged *)
398398+ type t = Ezjsonm.value
399399+400400+ val of_json : Ezjsonm.value -> t
401401+ val to_json : t -> Ezjsonm.value
402402+end
+33
jmap/jmap-mail.opam
···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+version: "0.1.0"
44+synopsis: "JMAP Mail Protocol (RFC 8621) implementation in OCaml"
55+description:
66+ "JMAP Mail extension with Mailbox, Email, Thread, and related types"
77+maintainer: ["your.email@example.com"]
88+authors: ["Your Name"]
99+license: "MIT"
1010+homepage: "https://github.com/yourusername/jmap"
1111+bug-reports: "https://github.com/yourusername/jmap/issues"
1212+depends: [
1313+ "ocaml" {>= "4.14"}
1414+ "dune" {>= "3.0" & >= "3.0"}
1515+ "jmap-core" {= version}
1616+ "ezjsonm" {>= "1.3.0"}
1717+ "odoc" {with-doc}
1818+]
1919+build: [
2020+ ["dune" "subst"] {dev}
2121+ [
2222+ "dune"
2323+ "build"
2424+ "-p"
2525+ name
2626+ "-j"
2727+ jobs
2828+ "@install"
2929+ "@runtest" {with-test}
3030+ "@doc" {with-doc}
3131+ ]
3232+]
3333+dev-repo: "git+https://github.com/yourusername/jmap.git"
···11+(** JMAP Email Type
22+33+ An Email represents an immutable RFC 5322 message. All metadata extracted
44+ from the message (headers, MIME structure, etc.) is exposed through
55+ structured properties.
66+77+open Jmap_core
88+99+ Reference: RFC 8621 Section 4 (Emails)
1010+ Test files:
1111+ - test/data/mail/email_get_request.json
1212+ - test/data/mail/email_get_response.json
1313+ - test/data/mail/email_get_full_request.json
1414+ - test/data/mail/email_get_full_response.json
1515+ - test/data/mail/email_query_request.json
1616+ - test/data/mail/email_query_response.json
1717+ - test/data/mail/email_set_request.json
1818+ - test/data/mail/email_set_response.json
1919+ - test/data/mail/email_import_request.json
2020+ - test/data/mail/email_import_response.json
2121+ - test/data/mail/email_parse_request.json
2222+ - test/data/mail/email_parse_response.json
2323+*)
2424+2525+(** Email address type (RFC 8621 Section 4.1.2.2) *)
2626+module EmailAddress = struct
2727+ type t = {
2828+ name : string option; (** Display name (e.g., "John Doe") *)
2929+ email : string; (** Email address (e.g., "john@example.com") *)
3030+ }
3131+3232+ (** Parse EmailAddress from JSON.
3333+ Test files: test/data/mail/email_get_response.json (from, to, cc, etc.)
3434+3535+ Expected structure:
3636+ {
3737+ "name": "Bob Smith",
3838+ "email": "bob@example.com"
3939+ }
4040+ *)
4141+ let of_json json =
4242+ let open Jmap_core.Parser.Helpers in
4343+ let fields = expect_object json in
4444+ let name = get_string_opt "name" fields in
4545+ let email = get_string "email" fields in
4646+ { name; email }
4747+4848+ let to_json t =
4949+ let fields = [("email", `String t.email)] in
5050+ let fields = match t.name with
5151+ | Some n -> ("name", `String n) :: fields
5252+ | None -> fields
5353+ in
5454+ `O fields
5555+5656+ (* Accessors *)
5757+ let name t = t.name
5858+ let email t = t.email
5959+6060+ (* Constructor *)
6161+ let v ?name ~email () =
6262+ { name; email }
6363+end
6464+6565+(** Email header field (RFC 8621 Section 4.1.4) *)
6666+module EmailHeader = struct
6767+ type t = {
6868+ name : string; (** Header field name (case-insensitive) *)
6969+ value : string; (** Header field value (decoded) *)
7070+ }
7171+7272+ let of_json json =
7373+ let open Jmap_core.Parser.Helpers in
7474+ let fields = expect_object json in
7575+ let name = get_string "name" fields in
7676+ let value = get_string "value" fields in
7777+ { name; value }
7878+7979+ let to_json t =
8080+ `O [
8181+ ("name", `String t.name);
8282+ ("value", `String t.value);
8383+ ]
8484+8585+ (* Accessors *)
8686+ let name t = t.name
8787+ let value t = t.value
8888+8989+ (* Constructor *)
9090+ let v ~name ~value =
9191+ { name; value }
9292+end
9393+9494+(** MIME body part structure (RFC 8621 Section 4.1.4) *)
9595+module BodyPart = struct
9696+ type t = {
9797+ part_id : string option; (** Part ID for referencing this part *)
9898+ blob_id : Jmap_core.Id.t option; (** Blob ID for fetching raw content *)
9999+ size : Jmap_core.Primitives.UnsignedInt.t; (** Size in octets *)
100100+ headers : EmailHeader.t list; (** All header fields *)
101101+ name : string option; (** Name from Content-Disposition or Content-Type *)
102102+ type_ : string; (** Content-Type value (e.g., "text/plain") *)
103103+ charset : string option; (** Charset parameter from Content-Type *)
104104+ disposition : string option; (** Content-Disposition value (e.g., "attachment") *)
105105+ cid : string option; (** Content-ID value (without angle brackets) *)
106106+ language : string list option; (** Content-Language values *)
107107+ location : string option; (** Content-Location value *)
108108+ sub_parts : t list option; (** Sub-parts for multipart/* types *)
109109+ }
110110+111111+ (** Parse BodyPart from JSON.
112112+ Test files: test/data/mail/email_get_full_response.json (bodyStructure, textBody, etc.)
113113+114114+ Expected structure (leaf part):
115115+ {
116116+ "partId": "1",
117117+ "blobId": "Gb5f13e2d7b8a9c0d1e2f3a4b5c6d7e8",
118118+ "size": 2134,
119119+ "headers": [...],
120120+ "type": "text/plain",
121121+ "charset": "utf-8",
122122+ "disposition": null,
123123+ "cid": null,
124124+ "language": null,
125125+ "location": null
126126+ }
127127+128128+ Or multipart:
129129+ {
130130+ "type": "multipart/mixed",
131131+ "subParts": [...]
132132+ }
133133+ *)
134134+ let rec of_json json =
135135+ let open Jmap_core.Parser.Helpers in
136136+ let fields = expect_object json in
137137+ let part_id = get_string_opt "partId" fields in
138138+ let blob_id = match find_field "blobId" fields with
139139+ | Some (`String s) -> Some (Jmap_core.Id.of_string s)
140140+ | Some `Null | None -> None
141141+ | Some _ -> raise (Jmap_core.Error.Parse_error "blobId must be a string")
142142+ in
143143+ let size = match find_field "size" fields with
144144+ | Some s -> Jmap_core.Primitives.UnsignedInt.of_json s
145145+ | None -> Jmap_core.Primitives.UnsignedInt.of_int 0
146146+ in
147147+ let headers = match find_field "headers" fields with
148148+ | Some (`A items) -> List.map EmailHeader.of_json items
149149+ | Some `Null | None -> []
150150+ | Some _ -> raise (Jmap_core.Error.Parse_error "headers must be an array")
151151+ in
152152+ let name = get_string_opt "name" fields in
153153+ let type_ = get_string "type" fields in
154154+ let charset = get_string_opt "charset" fields in
155155+ let disposition = get_string_opt "disposition" fields in
156156+ let cid = get_string_opt "cid" fields in
157157+ let language = match find_field "language" fields with
158158+ | Some (`A items) -> Some (List.map expect_string items)
159159+ | Some `Null | None -> None
160160+ | Some _ -> raise (Jmap_core.Error.Parse_error "language must be an array")
161161+ in
162162+ let location = get_string_opt "location" fields in
163163+ let sub_parts = match find_field "subParts" fields with
164164+ | Some (`A items) -> Some (List.map of_json items)
165165+ | Some `Null | None -> None
166166+ | Some _ -> raise (Jmap_core.Error.Parse_error "subParts must be an array")
167167+ in
168168+ { part_id; blob_id; size; headers; name; type_; charset;
169169+ disposition; cid; language; location; sub_parts }
170170+171171+ let rec to_json t =
172172+ let fields = [("type", `String t.type_)] in
173173+ let fields = match t.part_id with
174174+ | Some id -> ("partId", `String id) :: fields
175175+ | None -> fields
176176+ in
177177+ let fields = match t.blob_id with
178178+ | Some id -> ("blobId", Jmap_core.Id.to_json id) :: fields
179179+ | None -> fields
180180+ in
181181+ let fields = ("size", Jmap_core.Primitives.UnsignedInt.to_json t.size) :: fields in
182182+ let fields = if t.headers <> [] then
183183+ ("headers", `A (List.map EmailHeader.to_json t.headers)) :: fields
184184+ else
185185+ fields
186186+ in
187187+ let fields = match t.name with
188188+ | Some n -> ("name", `String n) :: fields
189189+ | None -> fields
190190+ in
191191+ let fields = match t.charset with
192192+ | Some c -> ("charset", `String c) :: fields
193193+ | None -> fields
194194+ in
195195+ let fields = match t.disposition with
196196+ | Some d -> ("disposition", `String d) :: fields
197197+ | None -> fields
198198+ in
199199+ let fields = match t.cid with
200200+ | Some c -> ("cid", `String c) :: fields
201201+ | None -> fields
202202+ in
203203+ let fields = match t.language with
204204+ | Some l -> ("language", `A (List.map (fun s -> `String s) l)) :: fields
205205+ | None -> fields
206206+ in
207207+ let fields = match t.location with
208208+ | Some l -> ("location", `String l) :: fields
209209+ | None -> fields
210210+ in
211211+ let fields = match t.sub_parts with
212212+ | Some parts -> ("subParts", `A (List.map to_json parts)) :: fields
213213+ | None -> fields
214214+ in
215215+ `O fields
216216+217217+ (* Accessors *)
218218+ let part_id t = t.part_id
219219+ let blob_id t = t.blob_id
220220+ let size t = t.size
221221+ let headers t = t.headers
222222+ let name t = t.name
223223+ let type_ t = t.type_
224224+ let charset t = t.charset
225225+ let disposition t = t.disposition
226226+ let cid t = t.cid
227227+ let language t = t.language
228228+ let location t = t.location
229229+ let sub_parts t = t.sub_parts
230230+231231+ (* Constructor *)
232232+ let v ?part_id ?blob_id ~size ~headers ?name ~type_ ?charset
233233+ ?disposition ?cid ?language ?location ?sub_parts () =
234234+ { part_id; blob_id; size; headers; name; type_; charset;
235235+ disposition; cid; language; location; sub_parts }
236236+end
237237+238238+(** Body value content (RFC 8621 Section 4.1.4.3) *)
239239+module BodyValue = struct
240240+ type t = {
241241+ value : string; (** Decoded body part content *)
242242+ is_encoding_problem : bool; (** True if charset decoding failed *)
243243+ is_truncated : bool; (** True if value was truncated due to size limits *)
244244+ }
245245+246246+ (** Parse BodyValue from JSON.
247247+ Test files: test/data/mail/email_get_full_response.json (bodyValues field)
248248+249249+ Expected structure:
250250+ {
251251+ "value": "Hi Alice,\n\nHere's the latest update...",
252252+ "isEncodingProblem": false,
253253+ "isTruncated": false
254254+ }
255255+ *)
256256+ let of_json json =
257257+ let open Jmap_core.Parser.Helpers in
258258+ let fields = expect_object json in
259259+ let value = get_string "value" fields in
260260+ let is_encoding_problem = get_bool_opt "isEncodingProblem" fields false in
261261+ let is_truncated = get_bool_opt "isTruncated" fields false in
262262+ { value; is_encoding_problem; is_truncated }
263263+264264+ let to_json t =
265265+ `O [
266266+ ("value", `String t.value);
267267+ ("isEncodingProblem", `Bool t.is_encoding_problem);
268268+ ("isTruncated", `Bool t.is_truncated);
269269+ ]
270270+271271+ (* Accessors *)
272272+ let value t = t.value
273273+ let is_encoding_problem t = t.is_encoding_problem
274274+ let is_truncated t = t.is_truncated
275275+276276+ (* Constructor *)
277277+ let v ~value ~is_encoding_problem ~is_truncated =
278278+ { value; is_encoding_problem; is_truncated }
279279+end
280280+281281+(** Email object type (RFC 8621 Section 4.1) *)
282282+type t = {
283283+ (* Metadata properties *)
284284+ id : Jmap_core.Id.t; (** Immutable server-assigned id *)
285285+ blob_id : Jmap_core.Id.t; (** Blob ID for downloading raw message *)
286286+ thread_id : Jmap_core.Id.t; (** Thread ID this email belongs to *)
287287+ mailbox_ids : (Jmap_core.Id.t * bool) list; (** Map of mailbox IDs to true *)
288288+ keywords : (string * bool) list; (** Map of keywords to true (e.g., "$seen") *)
289289+ size : Jmap_core.Primitives.UnsignedInt.t; (** Size in octets *)
290290+ received_at : Jmap_core.Primitives.UTCDate.t; (** Date message was received *)
291291+292292+ (* Header properties - commonly used headers *)
293293+ message_id : string list option; (** Message-ID header field values *)
294294+ in_reply_to : string list option; (** In-Reply-To header field values *)
295295+ references : string list option; (** References header field values *)
296296+ sender : EmailAddress.t list option; (** Sender header *)
297297+ from : EmailAddress.t list option; (** From header *)
298298+ to_ : EmailAddress.t list option; (** To header *)
299299+ cc : EmailAddress.t list option; (** Cc header *)
300300+ bcc : EmailAddress.t list option; (** Bcc header *)
301301+ reply_to : EmailAddress.t list option; (** Reply-To header *)
302302+ subject : string option; (** Subject header *)
303303+ sent_at : Jmap_core.Primitives.Date.t option; (** Date header *)
304304+305305+ (* Body properties *)
306306+ body_structure : BodyPart.t option; (** Full MIME structure *)
307307+ body_values : (string * BodyValue.t) list option; (** Map of partId to decoded content *)
308308+ text_body : BodyPart.t list option; (** Text/plain parts for rendering *)
309309+ html_body : BodyPart.t list option; (** Text/html parts for rendering *)
310310+ attachments : BodyPart.t list option; (** All attachment parts *)
311311+ has_attachment : bool; (** True if email has attachments *)
312312+ preview : string; (** Short plaintext preview (up to 256 chars) *)
313313+}
314314+315315+(** Accessors *)
316316+let id t = t.id
317317+let blob_id t = t.blob_id
318318+let thread_id t = t.thread_id
319319+let mailbox_ids t = t.mailbox_ids
320320+let keywords t = t.keywords
321321+let size t = t.size
322322+let received_at t = t.received_at
323323+let message_id t = t.message_id
324324+let in_reply_to t = t.in_reply_to
325325+let references t = t.references
326326+let sender t = t.sender
327327+let from t = t.from
328328+let to_ t = t.to_
329329+let cc t = t.cc
330330+let bcc t = t.bcc
331331+let reply_to t = t.reply_to
332332+let subject t = t.subject
333333+let sent_at t = t.sent_at
334334+let body_structure t = t.body_structure
335335+let body_values t = t.body_values
336336+let text_body t = t.text_body
337337+let html_body t = t.html_body
338338+let attachments t = t.attachments
339339+let has_attachment t = t.has_attachment
340340+let preview t = t.preview
341341+342342+(** Constructor *)
343343+let v ~id ~blob_id ~thread_id ~mailbox_ids ~keywords ~size ~received_at
344344+ ?message_id ?in_reply_to ?references ?sender ?from ?to_ ?cc ?bcc
345345+ ?reply_to ?subject ?sent_at ?body_structure ?body_values ?text_body
346346+ ?html_body ?attachments ~has_attachment ~preview () =
347347+ { id; blob_id; thread_id; mailbox_ids; keywords; size; received_at;
348348+ message_id; in_reply_to; references; sender; from; to_; cc; bcc;
349349+ reply_to; subject; sent_at; body_structure; body_values; text_body;
350350+ html_body; attachments; has_attachment; preview }
351351+352352+(** Parse Email from JSON.
353353+ Test files: test/data/mail/email_get_response.json (list field)
354354+355355+ Expected structure:
356356+ {
357357+ "id": "e001",
358358+ "blobId": "Ge5f13e2d7b8a9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8",
359359+ "threadId": "t001",
360360+ "mailboxIds": { "mb001": true },
361361+ "keywords": { "$seen": true },
362362+ "size": 15234,
363363+ "receivedAt": "2025-10-05T09:15:30Z",
364364+ ...
365365+ }
366366+*)
367367+let of_json json =
368368+ let open Jmap_core.Parser.Helpers in
369369+ let fields = expect_object json in
370370+371371+ (* Required fields *)
372372+ let id = Jmap_core.Id.of_json (require_field "id" fields) in
373373+ let blob_id = Jmap_core.Id.of_json (require_field "blobId" fields) in
374374+ let thread_id = Jmap_core.Id.of_json (require_field "threadId" fields) in
375375+376376+ (* mailboxIds - map of id -> bool *)
377377+ let mailbox_ids = match require_field "mailboxIds" fields with
378378+ | `O map_fields ->
379379+ List.map (fun (k, v) ->
380380+ (Jmap_core.Id.of_string k, expect_bool v)
381381+ ) map_fields
382382+ | _ -> raise (Jmap_core.Error.Parse_error "mailboxIds must be an object")
383383+ in
384384+385385+ (* keywords - map of string -> bool *)
386386+ let keywords = match require_field "keywords" fields with
387387+ | `O map_fields ->
388388+ List.map (fun (k, v) -> (k, expect_bool v)) map_fields
389389+ | _ -> raise (Jmap_core.Error.Parse_error "keywords must be an object")
390390+ in
391391+392392+ let size = Jmap_core.Primitives.UnsignedInt.of_json (require_field "size" fields) in
393393+ let received_at = Jmap_core.Primitives.UTCDate.of_json (require_field "receivedAt" fields) in
394394+395395+ (* Optional header fields *)
396396+ let message_id = match find_field "messageId" fields with
397397+ | Some (`A items) -> Some (List.map expect_string items)
398398+ | Some `Null | None -> None
399399+ | Some _ -> raise (Jmap_core.Error.Parse_error "messageId must be an array")
400400+ in
401401+ let in_reply_to = match find_field "inReplyTo" fields with
402402+ | Some (`A items) -> Some (List.map expect_string items)
403403+ | Some `Null | None -> None
404404+ | Some _ -> raise (Jmap_core.Error.Parse_error "inReplyTo must be an array")
405405+ in
406406+ let references = match find_field "references" fields with
407407+ | Some (`A items) -> Some (List.map expect_string items)
408408+ | Some `Null | None -> None
409409+ | Some _ -> raise (Jmap_core.Error.Parse_error "references must be an array")
410410+ in
411411+ let sender = match find_field "sender" fields with
412412+ | Some (`A items) -> Some (List.map EmailAddress.of_json items)
413413+ | Some `Null | None -> None
414414+ | Some _ -> raise (Jmap_core.Error.Parse_error "sender must be an array")
415415+ in
416416+ let from = match find_field "from" fields with
417417+ | Some (`A items) -> Some (List.map EmailAddress.of_json items)
418418+ | Some `Null | None -> None
419419+ | Some _ -> raise (Jmap_core.Error.Parse_error "from must be an array")
420420+ in
421421+ let to_ = match find_field "to" fields with
422422+ | Some (`A items) -> Some (List.map EmailAddress.of_json items)
423423+ | Some `Null | None -> None
424424+ | Some _ -> raise (Jmap_core.Error.Parse_error "to must be an array")
425425+ in
426426+ let cc = match find_field "cc" fields with
427427+ | Some (`A items) -> Some (List.map EmailAddress.of_json items)
428428+ | Some `Null | None -> None
429429+ | Some _ -> raise (Jmap_core.Error.Parse_error "cc must be an array")
430430+ in
431431+ let bcc = match find_field "bcc" fields with
432432+ | Some (`A items) -> Some (List.map EmailAddress.of_json items)
433433+ | Some `Null | None -> None
434434+ | Some _ -> raise (Jmap_core.Error.Parse_error "bcc must be an array")
435435+ in
436436+ let reply_to = match find_field "replyTo" fields with
437437+ | Some (`A items) -> Some (List.map EmailAddress.of_json items)
438438+ | Some `Null | None -> None
439439+ | Some _ -> raise (Jmap_core.Error.Parse_error "replyTo must be an array")
440440+ in
441441+ let subject = get_string_opt "subject" fields in
442442+ let sent_at = match find_field "sentAt" fields with
443443+ | Some (`String s) -> Some (Jmap_core.Primitives.Date.of_string s)
444444+ | Some `Null | None -> None
445445+ | Some _ -> raise (Jmap_core.Error.Parse_error "sentAt must be a string")
446446+ in
447447+448448+ (* Body properties *)
449449+ let body_structure = match find_field "bodyStructure" fields with
450450+ | Some ((`O _) as json) -> Some (BodyPart.of_json json)
451451+ | Some `Null | None -> None
452452+ | Some _ -> raise (Jmap_core.Error.Parse_error "bodyStructure must be an object")
453453+ in
454454+455455+ (* bodyValues - map of partId -> BodyValue *)
456456+ let body_values = match find_field "bodyValues" fields with
457457+ | Some (`O map_fields) ->
458458+ Some (List.map (fun (k, v) -> (k, BodyValue.of_json v)) map_fields)
459459+ | Some `Null | None -> None
460460+ | Some _ -> raise (Jmap_core.Error.Parse_error "bodyValues must be an object")
461461+ in
462462+463463+ let text_body = match find_field "textBody" fields with
464464+ | Some (`A items) -> Some (List.map BodyPart.of_json items)
465465+ | Some `Null | None -> None
466466+ | Some _ -> raise (Jmap_core.Error.Parse_error "textBody must be an array")
467467+ in
468468+ let html_body = match find_field "htmlBody" fields with
469469+ | Some (`A items) -> Some (List.map BodyPart.of_json items)
470470+ | Some `Null | None -> None
471471+ | Some _ -> raise (Jmap_core.Error.Parse_error "htmlBody must be an array")
472472+ in
473473+ let attachments = match find_field "attachments" fields with
474474+ | Some (`A items) -> Some (List.map BodyPart.of_json items)
475475+ | Some `Null | None -> None
476476+ | Some _ -> raise (Jmap_core.Error.Parse_error "attachments must be an array")
477477+ in
478478+479479+ let has_attachment = get_bool_opt "hasAttachment" fields false in
480480+ let preview = get_string "preview" fields in
481481+482482+ { id; blob_id; thread_id; mailbox_ids; keywords; size; received_at;
483483+ message_id; in_reply_to; references; sender; from; to_; cc; bcc;
484484+ reply_to; subject; sent_at; body_structure; body_values; text_body;
485485+ html_body; attachments; has_attachment; preview }
486486+487487+let to_json t =
488488+ let fields = [
489489+ ("id", Jmap_core.Id.to_json t.id);
490490+ ("blobId", Jmap_core.Id.to_json t.blob_id);
491491+ ("threadId", Jmap_core.Id.to_json t.thread_id);
492492+ ("mailboxIds", `O (List.map (fun (id, b) ->
493493+ (Jmap_core.Id.to_string id, `Bool b)) t.mailbox_ids));
494494+ ("keywords", `O (List.map (fun (k, b) -> (k, `Bool b)) t.keywords));
495495+ ("size", Jmap_core.Primitives.UnsignedInt.to_json t.size);
496496+ ("receivedAt", Jmap_core.Primitives.UTCDate.to_json t.received_at);
497497+ ("hasAttachment", `Bool t.has_attachment);
498498+ ("preview", `String t.preview);
499499+ ] in
500500+501501+ (* Add optional fields *)
502502+ let fields = match t.message_id with
503503+ | Some ids -> ("messageId", `A (List.map (fun s -> `String s) ids)) :: fields
504504+ | None -> fields
505505+ in
506506+ let fields = match t.in_reply_to with
507507+ | Some ids -> ("inReplyTo", `A (List.map (fun s -> `String s) ids)) :: fields
508508+ | None -> fields
509509+ in
510510+ let fields = match t.references with
511511+ | Some ids -> ("references", `A (List.map (fun s -> `String s) ids)) :: fields
512512+ | None -> fields
513513+ in
514514+ let fields = match t.sender with
515515+ | Some addrs -> ("sender", `A (List.map EmailAddress.to_json addrs)) :: fields
516516+ | None -> fields
517517+ in
518518+ let fields = match t.from with
519519+ | Some addrs -> ("from", `A (List.map EmailAddress.to_json addrs)) :: fields
520520+ | None -> fields
521521+ in
522522+ let fields = match t.to_ with
523523+ | Some addrs -> ("to", `A (List.map EmailAddress.to_json addrs)) :: fields
524524+ | None -> fields
525525+ in
526526+ let fields = match t.cc with
527527+ | Some addrs -> ("cc", `A (List.map EmailAddress.to_json addrs)) :: fields
528528+ | None -> fields
529529+ in
530530+ let fields = match t.bcc with
531531+ | Some addrs -> ("bcc", `A (List.map EmailAddress.to_json addrs)) :: fields
532532+ | None -> fields
533533+ in
534534+ let fields = match t.reply_to with
535535+ | Some addrs -> ("replyTo", `A (List.map EmailAddress.to_json addrs)) :: fields
536536+ | None -> fields
537537+ in
538538+ let fields = match t.subject with
539539+ | Some s -> ("subject", `String s) :: fields
540540+ | None -> fields
541541+ in
542542+ let fields = match t.sent_at with
543543+ | Some d -> ("sentAt", Jmap_core.Primitives.Date.to_json d) :: fields
544544+ | None -> fields
545545+ in
546546+ let fields = match t.body_structure with
547547+ | Some bs -> ("bodyStructure", BodyPart.to_json bs) :: fields
548548+ | None -> fields
549549+ in
550550+ let fields = match t.body_values with
551551+ | Some bv -> ("bodyValues", `O (List.map (fun (k, v) ->
552552+ (k, BodyValue.to_json v)) bv)) :: fields
553553+ | None -> fields
554554+ in
555555+ let fields = match t.text_body with
556556+ | Some tb -> ("textBody", `A (List.map BodyPart.to_json tb)) :: fields
557557+ | None -> fields
558558+ in
559559+ let fields = match t.html_body with
560560+ | Some hb -> ("htmlBody", `A (List.map BodyPart.to_json hb)) :: fields
561561+ | None -> fields
562562+ in
563563+ let fields = match t.attachments with
564564+ | Some att -> ("attachments", `A (List.map BodyPart.to_json att)) :: fields
565565+ | None -> fields
566566+ in
567567+ `O fields
568568+569569+(** Email-specific filter for /query (RFC 8621 Section 4.4) *)
570570+module Filter = struct
571571+ type t = {
572572+ in_mailbox : Jmap_core.Id.t option; (** Email is in this mailbox *)
573573+ in_mailbox_other_than : Jmap_core.Id.t list option; (** Email is in a mailbox other than these *)
574574+ before : Jmap_core.Primitives.UTCDate.t option; (** receivedAt < this date *)
575575+ after : Jmap_core.Primitives.UTCDate.t option; (** receivedAt >= this date *)
576576+ min_size : Jmap_core.Primitives.UnsignedInt.t option; (** size >= this value *)
577577+ max_size : Jmap_core.Primitives.UnsignedInt.t option; (** size < this value *)
578578+ all_in_thread_have_keyword : string option; (** All emails in thread have this keyword *)
579579+ some_in_thread_have_keyword : string option; (** Some email in thread has this keyword *)
580580+ none_in_thread_have_keyword : string option; (** No email in thread has this keyword *)
581581+ has_keyword : string option; (** Email has this keyword *)
582582+ not_keyword : string option; (** Email does not have this keyword *)
583583+ has_attachment : bool option; (** hasAttachment equals this *)
584584+ text : string option; (** Text appears in subject/body/addresses *)
585585+ from : string option; (** From header contains this *)
586586+ to_ : string option; (** To header contains this *)
587587+ cc : string option; (** Cc header contains this *)
588588+ bcc : string option; (** Bcc header contains this *)
589589+ subject : string option; (** Subject header contains this *)
590590+ body : string option; (** Body contains this text *)
591591+ header : (string * string) list option; (** Header name contains value *)
592592+ }
593593+594594+ let of_json json =
595595+ let open Jmap_core.Parser.Helpers in
596596+ let fields = expect_object json in
597597+ let in_mailbox = match find_field "inMailbox" fields with
598598+ | Some (`String s) -> Some (Jmap_core.Id.of_string s)
599599+ | Some `Null | None -> None
600600+ | Some _ -> raise (Jmap_core.Error.Parse_error "inMailbox must be a string")
601601+ in
602602+ let in_mailbox_other_than = match find_field "inMailboxOtherThan" fields with
603603+ | Some (`A items) -> Some (List.map (fun s -> Jmap_core.Id.of_json s) items)
604604+ | Some `Null | None -> None
605605+ | Some _ -> raise (Jmap_core.Error.Parse_error "inMailboxOtherThan must be an array")
606606+ in
607607+ let before = match find_field "before" fields with
608608+ | Some (`String s) -> Some (Jmap_core.Primitives.UTCDate.of_string s)
609609+ | Some `Null | None -> None
610610+ | Some _ -> raise (Jmap_core.Error.Parse_error "before must be a string")
611611+ in
612612+ let after = match find_field "after" fields with
613613+ | Some (`String s) -> Some (Jmap_core.Primitives.UTCDate.of_string s)
614614+ | Some `Null | None -> None
615615+ | Some _ -> raise (Jmap_core.Error.Parse_error "after must be a string")
616616+ in
617617+ let min_size = match find_field "minSize" fields with
618618+ | Some s -> Some (Jmap_core.Primitives.UnsignedInt.of_json s)
619619+ | None -> None
620620+ in
621621+ let max_size = match find_field "maxSize" fields with
622622+ | Some s -> Some (Jmap_core.Primitives.UnsignedInt.of_json s)
623623+ | None -> None
624624+ in
625625+ let all_in_thread_have_keyword = get_string_opt "allInThreadHaveKeyword" fields in
626626+ let some_in_thread_have_keyword = get_string_opt "someInThreadHaveKeyword" fields in
627627+ let none_in_thread_have_keyword = get_string_opt "noneInThreadHaveKeyword" fields in
628628+ let has_keyword = get_string_opt "hasKeyword" fields in
629629+ let not_keyword = get_string_opt "notKeyword" fields in
630630+ let has_attachment = match find_field "hasAttachment" fields with
631631+ | Some (`Bool b) -> Some b
632632+ | Some `Null | None -> None
633633+ | Some _ -> raise (Jmap_core.Error.Parse_error "hasAttachment must be a boolean")
634634+ in
635635+ let text = get_string_opt "text" fields in
636636+ let from = get_string_opt "from" fields in
637637+ let to_ = get_string_opt "to" fields in
638638+ let cc = get_string_opt "cc" fields in
639639+ let bcc = get_string_opt "bcc" fields in
640640+ let subject = get_string_opt "subject" fields in
641641+ let body = get_string_opt "body" fields in
642642+ let header = match find_field "header" fields with
643643+ | Some (`A items) ->
644644+ Some (List.map (fun item ->
645645+ let hdr_fields = expect_object item in
646646+ let name = get_string "name" hdr_fields in
647647+ let value = get_string "value" hdr_fields in
648648+ (name, value)
649649+ ) items)
650650+ | Some `Null | None -> None
651651+ | Some _ -> raise (Jmap_core.Error.Parse_error "header must be an array")
652652+ in
653653+ { in_mailbox; in_mailbox_other_than; before; after; min_size; max_size;
654654+ all_in_thread_have_keyword; some_in_thread_have_keyword;
655655+ none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment;
656656+ text; from; to_; cc; bcc; subject; body; header }
657657+658658+ (* Accessors *)
659659+ let in_mailbox t = t.in_mailbox
660660+ let in_mailbox_other_than t = t.in_mailbox_other_than
661661+ let before t = t.before
662662+ let after t = t.after
663663+ let min_size t = t.min_size
664664+ let max_size t = t.max_size
665665+ let all_in_thread_have_keyword t = t.all_in_thread_have_keyword
666666+ let some_in_thread_have_keyword t = t.some_in_thread_have_keyword
667667+ let none_in_thread_have_keyword t = t.none_in_thread_have_keyword
668668+ let has_keyword t = t.has_keyword
669669+ let not_keyword t = t.not_keyword
670670+ let has_attachment t = t.has_attachment
671671+ let text t = t.text
672672+ let from t = t.from
673673+ let to_ t = t.to_
674674+ let cc t = t.cc
675675+ let bcc t = t.bcc
676676+ let subject t = t.subject
677677+ let body t = t.body
678678+ let header t = t.header
679679+680680+ (* Constructor *)
681681+ let v ?in_mailbox ?in_mailbox_other_than ?before ?after ?min_size ?max_size
682682+ ?all_in_thread_have_keyword ?some_in_thread_have_keyword
683683+ ?none_in_thread_have_keyword ?has_keyword ?not_keyword ?has_attachment
684684+ ?text ?from ?to_ ?cc ?bcc ?subject ?body ?header () =
685685+ { in_mailbox; in_mailbox_other_than; before; after; min_size; max_size;
686686+ all_in_thread_have_keyword; some_in_thread_have_keyword;
687687+ none_in_thread_have_keyword; has_keyword; not_keyword; has_attachment;
688688+ text; from; to_; cc; bcc; subject; body; header }
689689+690690+ (* Convert to JSON *)
691691+ let to_json t =
692692+ let fields = [] in
693693+ let fields = match t.in_mailbox with
694694+ | Some id -> ("inMailbox", Jmap_core.Id.to_json id) :: fields
695695+ | None -> fields
696696+ in
697697+ let fields = match t.in_mailbox_other_than with
698698+ | Some ids -> ("inMailboxOtherThan", `A (List.map Jmap_core.Id.to_json ids)) :: fields
699699+ | None -> fields
700700+ in
701701+ let fields = match t.before with
702702+ | Some d -> ("before", `String (Jmap_core.Primitives.UTCDate.to_string d)) :: fields
703703+ | None -> fields
704704+ in
705705+ let fields = match t.after with
706706+ | Some d -> ("after", `String (Jmap_core.Primitives.UTCDate.to_string d)) :: fields
707707+ | None -> fields
708708+ in
709709+ let fields = match t.min_size with
710710+ | Some s -> ("minSize", Jmap_core.Primitives.UnsignedInt.to_json s) :: fields
711711+ | None -> fields
712712+ in
713713+ let fields = match t.max_size with
714714+ | Some s -> ("maxSize", Jmap_core.Primitives.UnsignedInt.to_json s) :: fields
715715+ | None -> fields
716716+ in
717717+ let fields = match t.all_in_thread_have_keyword with
718718+ | Some k -> ("allInThreadHaveKeyword", `String k) :: fields
719719+ | None -> fields
720720+ in
721721+ let fields = match t.some_in_thread_have_keyword with
722722+ | Some k -> ("someInThreadHaveKeyword", `String k) :: fields
723723+ | None -> fields
724724+ in
725725+ let fields = match t.none_in_thread_have_keyword with
726726+ | Some k -> ("noneInThreadHaveKeyword", `String k) :: fields
727727+ | None -> fields
728728+ in
729729+ let fields = match t.has_keyword with
730730+ | Some k -> ("hasKeyword", `String k) :: fields
731731+ | None -> fields
732732+ in
733733+ let fields = match t.not_keyword with
734734+ | Some k -> ("notKeyword", `String k) :: fields
735735+ | None -> fields
736736+ in
737737+ let fields = match t.has_attachment with
738738+ | Some b -> ("hasAttachment", `Bool b) :: fields
739739+ | None -> fields
740740+ in
741741+ let fields = match t.text with
742742+ | Some s -> ("text", `String s) :: fields
743743+ | None -> fields
744744+ in
745745+ let fields = match t.from with
746746+ | Some s -> ("from", `String s) :: fields
747747+ | None -> fields
748748+ in
749749+ let fields = match t.to_ with
750750+ | Some s -> ("to", `String s) :: fields
751751+ | None -> fields
752752+ in
753753+ let fields = match t.cc with
754754+ | Some s -> ("cc", `String s) :: fields
755755+ | None -> fields
756756+ in
757757+ let fields = match t.bcc with
758758+ | Some s -> ("bcc", `String s) :: fields
759759+ | None -> fields
760760+ in
761761+ let fields = match t.subject with
762762+ | Some s -> ("subject", `String s) :: fields
763763+ | None -> fields
764764+ in
765765+ let fields = match t.body with
766766+ | Some s -> ("body", `String s) :: fields
767767+ | None -> fields
768768+ in
769769+ let fields = match t.header with
770770+ | Some hdrs ->
771771+ let hdr_arr = List.map (fun (name, value) ->
772772+ `O [("name", `String name); ("value", `String value)]
773773+ ) hdrs in
774774+ ("header", `A hdr_arr) :: fields
775775+ | None -> fields
776776+ in
777777+ `O fields
778778+end
779779+780780+(** Standard /get method (RFC 8621 Section 4.2) *)
781781+module Get = struct
782782+ type request = {
783783+ account_id : Jmap_core.Id.t;
784784+ ids : Jmap_core.Id.t list option;
785785+ properties : string list option;
786786+ (* Email-specific get arguments *)
787787+ body_properties : string list option; (** Properties to fetch for bodyStructure parts *)
788788+ fetch_text_body_values : bool option; (** Fetch bodyValues for textBody parts *)
789789+ fetch_html_body_values : bool option; (** Fetch bodyValues for htmlBody parts *)
790790+ fetch_all_body_values : bool option; (** Fetch bodyValues for all parts *)
791791+ max_body_value_bytes : Jmap_core.Primitives.UnsignedInt.t option; (** Truncate large body values *)
792792+ }
793793+794794+ type response = t Jmap_core.Standard_methods.Get.response
795795+796796+ (* Accessors for request *)
797797+ let account_id req = req.account_id
798798+ let ids req = req.ids
799799+ let properties req = req.properties
800800+ let body_properties req = req.body_properties
801801+ let fetch_text_body_values req = req.fetch_text_body_values
802802+ let fetch_html_body_values req = req.fetch_html_body_values
803803+ let fetch_all_body_values req = req.fetch_all_body_values
804804+ let max_body_value_bytes req = req.max_body_value_bytes
805805+806806+ (* Constructor for request *)
807807+ let request_v ~account_id ?ids ?properties ?body_properties
808808+ ?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values
809809+ ?max_body_value_bytes () =
810810+ { account_id; ids; properties; body_properties; fetch_text_body_values;
811811+ fetch_html_body_values; fetch_all_body_values; max_body_value_bytes }
812812+813813+ (** Parse get request from JSON.
814814+ Test files:
815815+ - test/data/mail/email_get_request.json
816816+ - test/data/mail/email_get_full_request.json
817817+ *)
818818+ let request_of_json json =
819819+ let open Jmap_core.Parser.Helpers in
820820+ let fields = expect_object json in
821821+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
822822+ let ids = match find_field "ids" fields with
823823+ | Some (`A items) -> Some (List.map Jmap_core.Id.of_json items)
824824+ | Some `Null | None -> None
825825+ | Some _ -> raise (Jmap_core.Error.Parse_error "ids must be an array")
826826+ in
827827+ let properties = match find_field "properties" fields with
828828+ | Some (`A items) -> Some (List.map expect_string items)
829829+ | Some `Null | None -> None
830830+ | Some _ -> raise (Jmap_core.Error.Parse_error "properties must be an array")
831831+ in
832832+ let body_properties = match find_field "bodyProperties" fields with
833833+ | Some (`A items) -> Some (List.map expect_string items)
834834+ | Some `Null | None -> None
835835+ | Some _ -> raise (Jmap_core.Error.Parse_error "bodyProperties must be an array")
836836+ in
837837+ let fetch_text_body_values = match find_field "fetchTextBodyValues" fields with
838838+ | Some (`Bool b) -> Some b
839839+ | Some `Null | None -> None
840840+ | Some _ -> raise (Jmap_core.Error.Parse_error "fetchTextBodyValues must be a boolean")
841841+ in
842842+ let fetch_html_body_values = match find_field "fetchHTMLBodyValues" fields with
843843+ | Some (`Bool b) -> Some b
844844+ | Some `Null | None -> None
845845+ | Some _ -> raise (Jmap_core.Error.Parse_error "fetchHTMLBodyValues must be a boolean")
846846+ in
847847+ let fetch_all_body_values = match find_field "fetchAllBodyValues" fields with
848848+ | Some (`Bool b) -> Some b
849849+ | Some `Null | None -> None
850850+ | Some _ -> raise (Jmap_core.Error.Parse_error "fetchAllBodyValues must be a boolean")
851851+ in
852852+ let max_body_value_bytes = match find_field "maxBodyValueBytes" fields with
853853+ | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v)
854854+ | None -> None
855855+ in
856856+ { account_id; ids; properties; body_properties; fetch_text_body_values;
857857+ fetch_html_body_values; fetch_all_body_values; max_body_value_bytes }
858858+859859+ (** Parse get response from JSON.
860860+ Test files:
861861+ - test/data/mail/email_get_response.json
862862+ - test/data/mail/email_get_full_response.json
863863+ *)
864864+ let response_of_json json =
865865+ Jmap_core.Standard_methods.Get.response_of_json of_json json
866866+867867+ (** Convert get request to JSON *)
868868+ let request_to_json req =
869869+ let fields = [
870870+ ("accountId", Jmap_core.Id.to_json req.account_id);
871871+ ] in
872872+ let fields = match req.ids with
873873+ | Some ids -> ("ids", `A (List.map Jmap_core.Id.to_json ids)) :: fields
874874+ | None -> fields
875875+ in
876876+ let fields = match req.properties with
877877+ | Some props -> ("properties", `A (List.map (fun s -> `String s) props)) :: fields
878878+ | None -> fields
879879+ in
880880+ let fields = match req.body_properties with
881881+ | Some bp -> ("bodyProperties", `A (List.map (fun s -> `String s) bp)) :: fields
882882+ | None -> fields
883883+ in
884884+ let fields = match req.fetch_text_body_values with
885885+ | Some ftbv -> ("fetchTextBodyValues", `Bool ftbv) :: fields
886886+ | None -> fields
887887+ in
888888+ let fields = match req.fetch_html_body_values with
889889+ | Some fhbv -> ("fetchHTMLBodyValues", `Bool fhbv) :: fields
890890+ | None -> fields
891891+ in
892892+ let fields = match req.fetch_all_body_values with
893893+ | Some fabv -> ("fetchAllBodyValues", `Bool fabv) :: fields
894894+ | None -> fields
895895+ in
896896+ let fields = match req.max_body_value_bytes with
897897+ | Some mbvb -> ("maxBodyValueBytes", Jmap_core.Primitives.UnsignedInt.to_json mbvb) :: fields
898898+ | None -> fields
899899+ in
900900+ `O fields
901901+end
902902+903903+(** Standard /changes method (RFC 8621 Section 4.3) *)
904904+module Changes = struct
905905+ type request = Jmap_core.Standard_methods.Changes.request
906906+ type response = Jmap_core.Standard_methods.Changes.response
907907+908908+ let request_of_json json =
909909+ Jmap_core.Standard_methods.Changes.request_of_json json
910910+911911+ let response_of_json json =
912912+ Jmap_core.Standard_methods.Changes.response_of_json json
913913+end
914914+915915+(** Standard /query method (RFC 8621 Section 4.4) *)
916916+module Query = struct
917917+ type request = {
918918+ account_id : Jmap_core.Id.t;
919919+ filter : Filter.t Jmap_core.Filter.t option;
920920+ sort : Jmap_core.Comparator.t list option;
921921+ position : Jmap_core.Primitives.Int53.t option;
922922+ anchor : Jmap_core.Id.t option;
923923+ anchor_offset : Jmap_core.Primitives.Int53.t option;
924924+ limit : Jmap_core.Primitives.UnsignedInt.t option;
925925+ calculate_total : bool option;
926926+ (* Email-specific query arguments *)
927927+ collapse_threads : bool option; (** Return only one email per thread *)
928928+ }
929929+930930+ type response = Jmap_core.Standard_methods.Query.response
931931+932932+ (* Accessors for request *)
933933+ let account_id req = req.account_id
934934+ let filter req = req.filter
935935+ let sort req = req.sort
936936+ let position req = req.position
937937+ let anchor req = req.anchor
938938+ let anchor_offset req = req.anchor_offset
939939+ let limit req = req.limit
940940+ let calculate_total req = req.calculate_total
941941+ let collapse_threads req = req.collapse_threads
942942+943943+ (* Constructor for request *)
944944+ let request_v ~account_id ?filter ?sort ?position ?anchor ?anchor_offset
945945+ ?limit ?calculate_total ?collapse_threads () =
946946+ { account_id; filter; sort; position; anchor; anchor_offset;
947947+ limit; calculate_total; collapse_threads }
948948+949949+ (** Parse query request from JSON.
950950+ Test files: test/data/mail/email_query_request.json *)
951951+ let request_of_json json =
952952+ let open Jmap_core.Parser.Helpers in
953953+ let fields = expect_object json in
954954+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
955955+ let filter = match find_field "filter" fields with
956956+ | Some v -> Some (Jmap_core.Filter.of_json Filter.of_json v)
957957+ | None -> None
958958+ in
959959+ let sort = match find_field "sort" fields with
960960+ | Some (`A items) -> Some (List.map Jmap_core.Comparator.of_json items)
961961+ | Some `Null | None -> None
962962+ | Some _ -> raise (Jmap_core.Error.Parse_error "sort must be an array")
963963+ in
964964+ let position = match find_field "position" fields with
965965+ | Some v -> Some (Jmap_core.Primitives.Int53.of_json v)
966966+ | None -> None
967967+ in
968968+ let anchor = match find_field "anchor" fields with
969969+ | Some (`String s) -> Some (Jmap_core.Id.of_string s)
970970+ | Some `Null | None -> None
971971+ | Some _ -> raise (Jmap_core.Error.Parse_error "anchor must be a string")
972972+ in
973973+ let anchor_offset = match find_field "anchorOffset" fields with
974974+ | Some v -> Some (Jmap_core.Primitives.Int53.of_json v)
975975+ | None -> None
976976+ in
977977+ let limit = match find_field "limit" fields with
978978+ | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v)
979979+ | None -> None
980980+ in
981981+ let calculate_total = match find_field "calculateTotal" fields with
982982+ | Some (`Bool b) -> Some b
983983+ | Some `Null | None -> None
984984+ | Some _ -> raise (Jmap_core.Error.Parse_error "calculateTotal must be a boolean")
985985+ in
986986+ let collapse_threads = match find_field "collapseThreads" fields with
987987+ | Some (`Bool b) -> Some b
988988+ | Some `Null | None -> None
989989+ | Some _ -> raise (Jmap_core.Error.Parse_error "collapseThreads must be a boolean")
990990+ in
991991+ { account_id; filter; sort; position; anchor; anchor_offset;
992992+ limit; calculate_total; collapse_threads }
993993+994994+ (** Parse query response from JSON.
995995+ Test files: test/data/mail/email_query_response.json *)
996996+ let response_of_json json =
997997+ Jmap_core.Standard_methods.Query.response_of_json json
998998+999999+ (** Convert query request to JSON *)
10001000+ let request_to_json req =
10011001+ let fields = [
10021002+ ("accountId", Jmap_core.Id.to_json req.account_id);
10031003+ ] in
10041004+ let fields = match req.filter with
10051005+ | Some f -> ("filter", Jmap_core.Filter.to_json Filter.to_json f) :: fields
10061006+ | None -> fields
10071007+ in
10081008+ let fields = match req.sort with
10091009+ | Some s -> ("sort", `A (List.map Jmap_core.Comparator.to_json s)) :: fields
10101010+ | None -> fields
10111011+ in
10121012+ let fields = match req.position with
10131013+ | Some p -> ("position", Jmap_core.Primitives.Int53.to_json p) :: fields
10141014+ | None -> fields
10151015+ in
10161016+ let fields = match req.anchor with
10171017+ | Some a -> ("anchor", Jmap_core.Id.to_json a) :: fields
10181018+ | None -> fields
10191019+ in
10201020+ let fields = match req.anchor_offset with
10211021+ | Some ao -> ("anchorOffset", Jmap_core.Primitives.Int53.to_json ao) :: fields
10221022+ | None -> fields
10231023+ in
10241024+ let fields = match req.limit with
10251025+ | Some l -> ("limit", Jmap_core.Primitives.UnsignedInt.to_json l) :: fields
10261026+ | None -> fields
10271027+ in
10281028+ let fields = match req.calculate_total with
10291029+ | Some ct -> ("calculateTotal", `Bool ct) :: fields
10301030+ | None -> fields
10311031+ in
10321032+ let fields = match req.collapse_threads with
10331033+ | Some ct -> ("collapseThreads", `Bool ct) :: fields
10341034+ | None -> fields
10351035+ in
10361036+ `O fields
10371037+end
10381038+10391039+(** Standard /queryChanges method (RFC 8621 Section 4.5) *)
10401040+module QueryChanges = struct
10411041+ type request = {
10421042+ account_id : Jmap_core.Id.t;
10431043+ filter : Filter.t Jmap_core.Filter.t option;
10441044+ sort : Jmap_core.Comparator.t list option;
10451045+ since_query_state : string;
10461046+ max_changes : Jmap_core.Primitives.UnsignedInt.t option;
10471047+ up_to_id : Jmap_core.Id.t option;
10481048+ calculate_total : bool option;
10491049+ (* Email-specific *)
10501050+ collapse_threads : bool option;
10511051+ }
10521052+10531053+ type response = Jmap_core.Standard_methods.QueryChanges.response
10541054+10551055+ (* Accessors for request *)
10561056+ let account_id req = req.account_id
10571057+ let filter req = req.filter
10581058+ let sort req = req.sort
10591059+ let since_query_state req = req.since_query_state
10601060+ let max_changes req = req.max_changes
10611061+ let up_to_id req = req.up_to_id
10621062+ let calculate_total req = req.calculate_total
10631063+ let collapse_threads req = req.collapse_threads
10641064+10651065+ (* Constructor for request *)
10661066+ let request_v ~account_id ?filter ?sort ~since_query_state ?max_changes
10671067+ ?up_to_id ?calculate_total ?collapse_threads () =
10681068+ { account_id; filter; sort; since_query_state; max_changes;
10691069+ up_to_id; calculate_total; collapse_threads }
10701070+10711071+ let request_of_json json =
10721072+ let open Jmap_core.Parser.Helpers in
10731073+ let fields = expect_object json in
10741074+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
10751075+ let filter = match find_field "filter" fields with
10761076+ | Some v -> Some (Jmap_core.Filter.of_json Filter.of_json v)
10771077+ | None -> None
10781078+ in
10791079+ let sort = match find_field "sort" fields with
10801080+ | Some (`A items) -> Some (List.map Jmap_core.Comparator.of_json items)
10811081+ | Some `Null | None -> None
10821082+ | Some _ -> raise (Jmap_core.Error.Parse_error "sort must be an array")
10831083+ in
10841084+ let since_query_state = get_string "sinceQueryState" fields in
10851085+ let max_changes = match find_field "maxChanges" fields with
10861086+ | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v)
10871087+ | None -> None
10881088+ in
10891089+ let up_to_id = match find_field "upToId" fields with
10901090+ | Some (`String s) -> Some (Jmap_core.Id.of_string s)
10911091+ | Some `Null | None -> None
10921092+ | Some _ -> raise (Jmap_core.Error.Parse_error "upToId must be a string")
10931093+ in
10941094+ let calculate_total = match find_field "calculateTotal" fields with
10951095+ | Some (`Bool b) -> Some b
10961096+ | Some `Null | None -> None
10971097+ | Some _ -> raise (Jmap_core.Error.Parse_error "calculateTotal must be a boolean")
10981098+ in
10991099+ let collapse_threads = match find_field "collapseThreads" fields with
11001100+ | Some (`Bool b) -> Some b
11011101+ | Some `Null | None -> None
11021102+ | Some _ -> raise (Jmap_core.Error.Parse_error "collapseThreads must be a boolean")
11031103+ in
11041104+ { account_id; filter; sort; since_query_state; max_changes;
11051105+ up_to_id; calculate_total; collapse_threads }
11061106+11071107+ let response_of_json json =
11081108+ Jmap_core.Standard_methods.QueryChanges.response_of_json json
11091109+end
11101110+11111111+(** Standard /set method (RFC 8621 Section 4.6) *)
11121112+module Set = struct
11131113+ type request = t Jmap_core.Standard_methods.Set.request
11141114+ type response = t Jmap_core.Standard_methods.Set.response
11151115+11161116+ (** Parse set request from JSON.
11171117+ Test files: test/data/mail/email_set_request.json *)
11181118+ let request_of_json json =
11191119+ Jmap_core.Standard_methods.Set.request_of_json of_json json
11201120+11211121+ (** Parse set response from JSON.
11221122+ Test files: test/data/mail/email_set_response.json *)
11231123+ let response_of_json json =
11241124+ Jmap_core.Standard_methods.Set.response_of_json of_json json
11251125+end
11261126+11271127+(** Standard /copy method (RFC 8621 Section 4.7) *)
11281128+module Copy = struct
11291129+ type request = t Jmap_core.Standard_methods.Copy.request
11301130+ type response = t Jmap_core.Standard_methods.Copy.response
11311131+11321132+ let request_of_json json =
11331133+ Jmap_core.Standard_methods.Copy.request_of_json of_json json
11341134+11351135+ let response_of_json json =
11361136+ Jmap_core.Standard_methods.Copy.response_of_json of_json json
11371137+end
11381138+11391139+(** Email/import method (RFC 8621 Section 4.8) *)
11401140+module Import = struct
11411141+ (** Email import request object *)
11421142+ type import_email = {
11431143+ blob_id : Jmap_core.Id.t; (** Blob ID containing raw RFC 5322 message *)
11441144+ mailbox_ids : (Jmap_core.Id.t * bool) list; (** Mailboxes to add email to *)
11451145+ keywords : (string * bool) list; (** Keywords to set *)
11461146+ received_at : Jmap_core.Primitives.UTCDate.t option; (** Override received date *)
11471147+ }
11481148+11491149+ type request = {
11501150+ account_id : Jmap_core.Id.t;
11511151+ if_in_state : string option;
11521152+ emails : (Jmap_core.Id.t * import_email) list; (** Map of creation id to import object *)
11531153+ }
11541154+11551155+ type response = {
11561156+ account_id : Jmap_core.Id.t;
11571157+ old_state : string option;
11581158+ new_state : string;
11591159+ created : (Jmap_core.Id.t * t) list option;
11601160+ not_created : (Jmap_core.Id.t * Jmap_core.Error.set_error_detail) list option;
11611161+ }
11621162+11631163+ (* Accessors for import_email *)
11641164+ let import_blob_id ie = ie.blob_id
11651165+ let import_mailbox_ids ie = ie.mailbox_ids
11661166+ let import_keywords ie = ie.keywords
11671167+ let import_received_at ie = ie.received_at
11681168+11691169+ (* Constructor for import_email *)
11701170+ let import_email_v ~blob_id ~mailbox_ids ~keywords ?received_at () =
11711171+ { blob_id; mailbox_ids; keywords; received_at }
11721172+11731173+ (* Accessors for request *)
11741174+ let account_id (r : request) = r.account_id
11751175+ let if_in_state (r : request) = r.if_in_state
11761176+ let emails (r : request) = r.emails
11771177+11781178+ (* Constructor for request *)
11791179+ let request_v ~account_id ?if_in_state ~emails () =
11801180+ { account_id; if_in_state; emails }
11811181+11821182+ (* Accessors for response *)
11831183+ let response_account_id (r : response) = r.account_id
11841184+ let old_state (r : response) = r.old_state
11851185+ let new_state (r : response) = r.new_state
11861186+ let created (r : response) = r.created
11871187+ let not_created (r : response) = r.not_created
11881188+11891189+ (* Constructor for response *)
11901190+ let response_v ~account_id ?old_state ~new_state ?created ?not_created () =
11911191+ { account_id; old_state; new_state; created; not_created }
11921192+11931193+ (** Parse import request from JSON.
11941194+ Test files: test/data/mail/email_import_request.json *)
11951195+ let request_of_json json =
11961196+ let open Jmap_core.Parser.Helpers in
11971197+ let fields = expect_object json in
11981198+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
11991199+ let if_in_state = get_string_opt "ifInState" fields in
12001200+ let emails = match require_field "emails" fields with
12011201+ | `O pairs ->
12021202+ List.map (fun (k, v) ->
12031203+ let ie_fields = expect_object v in
12041204+ let blob_id = Jmap_core.Id.of_json (require_field "blobId" ie_fields) in
12051205+ let mailbox_ids = match require_field "mailboxIds" ie_fields with
12061206+ | `O map_fields ->
12071207+ List.map (fun (mid, b) ->
12081208+ (Jmap_core.Id.of_string mid, expect_bool b)
12091209+ ) map_fields
12101210+ | _ -> raise (Jmap_core.Error.Parse_error "mailboxIds must be an object")
12111211+ in
12121212+ let keywords = match require_field "keywords" ie_fields with
12131213+ | `O map_fields ->
12141214+ List.map (fun (kw, b) -> (kw, expect_bool b)) map_fields
12151215+ | _ -> raise (Jmap_core.Error.Parse_error "keywords must be an object")
12161216+ in
12171217+ let received_at = match find_field "receivedAt" ie_fields with
12181218+ | Some (`String s) -> Some (Jmap_core.Primitives.UTCDate.of_string s)
12191219+ | Some `Null | None -> None
12201220+ | Some _ -> raise (Jmap_core.Error.Parse_error "receivedAt must be a string")
12211221+ in
12221222+ let import_email = { blob_id; mailbox_ids; keywords; received_at } in
12231223+ (Jmap_core.Id.of_string k, import_email)
12241224+ ) pairs
12251225+ | _ -> raise (Jmap_core.Error.Parse_error "emails must be an object")
12261226+ in
12271227+ { account_id; if_in_state; emails }
12281228+12291229+ (** Parse import response from JSON.
12301230+ Test files: test/data/mail/email_import_response.json *)
12311231+ let response_of_json json =
12321232+ let open Jmap_core.Parser.Helpers in
12331233+ let fields = expect_object json in
12341234+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
12351235+ let old_state = get_string_opt "oldState" fields in
12361236+ let new_state = get_string "newState" fields in
12371237+ let created = match find_field "created" fields with
12381238+ | Some `Null | None -> None
12391239+ | Some (`O pairs) ->
12401240+ Some (List.map (fun (k, v) ->
12411241+ (Jmap_core.Id.of_string k, of_json v)
12421242+ ) pairs)
12431243+ | Some _ -> raise (Jmap_core.Error.Parse_error "created must be an object")
12441244+ in
12451245+ let not_created = match find_field "notCreated" fields with
12461246+ | Some `Null | None -> None
12471247+ | Some (`O pairs) ->
12481248+ Some (List.map (fun (k, v) ->
12491249+ (Jmap_core.Id.of_string k, Jmap_core.Error.parse_set_error_detail v)
12501250+ ) pairs)
12511251+ | Some _ -> raise (Jmap_core.Error.Parse_error "notCreated must be an object")
12521252+ in
12531253+ { account_id; old_state; new_state; created; not_created }
12541254+end
12551255+12561256+(** Email/parse method (RFC 8621 Section 4.9) *)
12571257+module Parse = struct
12581258+ type request = {
12591259+ account_id : Jmap_core.Id.t;
12601260+ blob_ids : Jmap_core.Id.t list; (** Blob IDs to parse *)
12611261+ properties : string list option; (** Email properties to return *)
12621262+ body_properties : string list option; (** BodyPart properties to return *)
12631263+ fetch_text_body_values : bool option;
12641264+ fetch_html_body_values : bool option;
12651265+ fetch_all_body_values : bool option;
12661266+ max_body_value_bytes : Jmap_core.Primitives.UnsignedInt.t option;
12671267+ }
12681268+12691269+ type response = {
12701270+ account_id : Jmap_core.Id.t;
12711271+ parsed : (Jmap_core.Id.t * t) list option; (** Map of blob ID to parsed email *)
12721272+ not_parsable : Jmap_core.Id.t list option; (** Blob IDs that couldn't be parsed *)
12731273+ not_found : Jmap_core.Id.t list option; (** Blob IDs that don't exist *)
12741274+ }
12751275+12761276+ (* Accessors for request *)
12771277+ let account_id (r : request) = r.account_id
12781278+ let blob_ids (r : request) = r.blob_ids
12791279+ let properties (r : request) = r.properties
12801280+ let body_properties (r : request) = r.body_properties
12811281+ let fetch_text_body_values (r : request) = r.fetch_text_body_values
12821282+ let fetch_html_body_values (r : request) = r.fetch_html_body_values
12831283+ let fetch_all_body_values (r : request) = r.fetch_all_body_values
12841284+ let max_body_value_bytes (r : request) = r.max_body_value_bytes
12851285+12861286+ (* Constructor for request *)
12871287+ let request_v ~account_id ~blob_ids ?properties ?body_properties
12881288+ ?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values
12891289+ ?max_body_value_bytes () =
12901290+ { account_id; blob_ids; properties; body_properties; fetch_text_body_values;
12911291+ fetch_html_body_values; fetch_all_body_values; max_body_value_bytes }
12921292+12931293+ (* Accessors for response *)
12941294+ let response_account_id (r : response) = r.account_id
12951295+ let parsed (r : response) = r.parsed
12961296+ let not_parsable (r : response) = r.not_parsable
12971297+ let not_found (r : response) = r.not_found
12981298+12991299+ (* Constructor for response *)
13001300+ let response_v ~account_id ?parsed ?not_parsable ?not_found () =
13011301+ { account_id; parsed; not_parsable; not_found }
13021302+13031303+ (** Parse parse request from JSON.
13041304+ Test files: test/data/mail/email_parse_request.json *)
13051305+ let request_of_json json =
13061306+ let open Jmap_core.Parser.Helpers in
13071307+ let fields = expect_object json in
13081308+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
13091309+ let blob_ids = match require_field "blobIds" fields with
13101310+ | `A items -> List.map Jmap_core.Id.of_json items
13111311+ | _ -> raise (Jmap_core.Error.Parse_error "blobIds must be an array")
13121312+ in
13131313+ let properties = match find_field "properties" fields with
13141314+ | Some (`A items) -> Some (List.map expect_string items)
13151315+ | Some `Null | None -> None
13161316+ | Some _ -> raise (Jmap_core.Error.Parse_error "properties must be an array")
13171317+ in
13181318+ let body_properties = match find_field "bodyProperties" fields with
13191319+ | Some (`A items) -> Some (List.map expect_string items)
13201320+ | Some `Null | None -> None
13211321+ | Some _ -> raise (Jmap_core.Error.Parse_error "bodyProperties must be an array")
13221322+ in
13231323+ let fetch_text_body_values = match find_field "fetchTextBodyValues" fields with
13241324+ | Some (`Bool b) -> Some b
13251325+ | Some `Null | None -> None
13261326+ | Some _ -> raise (Jmap_core.Error.Parse_error "fetchTextBodyValues must be a boolean")
13271327+ in
13281328+ let fetch_html_body_values = match find_field "fetchHTMLBodyValues" fields with
13291329+ | Some (`Bool b) -> Some b
13301330+ | Some `Null | None -> None
13311331+ | Some _ -> raise (Jmap_core.Error.Parse_error "fetchHTMLBodyValues must be a boolean")
13321332+ in
13331333+ let fetch_all_body_values = match find_field "fetchAllBodyValues" fields with
13341334+ | Some (`Bool b) -> Some b
13351335+ | Some `Null | None -> None
13361336+ | Some _ -> raise (Jmap_core.Error.Parse_error "fetchAllBodyValues must be a boolean")
13371337+ in
13381338+ let max_body_value_bytes = match find_field "maxBodyValueBytes" fields with
13391339+ | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v)
13401340+ | None -> None
13411341+ in
13421342+ { account_id; blob_ids; properties; body_properties; fetch_text_body_values;
13431343+ fetch_html_body_values; fetch_all_body_values; max_body_value_bytes }
13441344+13451345+ (** Parse parse response from JSON.
13461346+ Test files: test/data/mail/email_parse_response.json *)
13471347+ let response_of_json json =
13481348+ let open Jmap_core.Parser.Helpers in
13491349+ let fields = expect_object json in
13501350+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
13511351+ let parsed = match find_field "parsed" fields with
13521352+ | Some `Null | None -> None
13531353+ | Some (`O pairs) ->
13541354+ Some (List.map (fun (k, v) ->
13551355+ (Jmap_core.Id.of_string k, of_json v)
13561356+ ) pairs)
13571357+ | Some _ -> raise (Jmap_core.Error.Parse_error "parsed must be an object")
13581358+ in
13591359+ let not_parsable = match find_field "notParsable" fields with
13601360+ | Some (`A items) -> Some (List.map Jmap_core.Id.of_json items)
13611361+ | Some `Null | None -> None
13621362+ | Some _ -> raise (Jmap_core.Error.Parse_error "notParsable must be an array")
13631363+ in
13641364+ let not_found = match find_field "notFound" fields with
13651365+ | Some (`A items) -> Some (List.map Jmap_core.Id.of_json items)
13661366+ | Some `Null | None -> None
13671367+ | Some _ -> raise (Jmap_core.Error.Parse_error "notFound must be an array")
13681368+ in
13691369+ { account_id; parsed; not_parsable; not_found }
13701370+end
13711371+13721372+13731373+(** Standard email keywords (RFC 8621 Section 4.1.1) *)
13741374+module Keyword = struct
13751375+ let seen = "$seen" (* Message has been read *)
13761376+ let draft = "$draft" (* Message is a draft *)
13771377+ let flagged = "$flagged" (* Message is flagged for urgent/special attention *)
13781378+ let answered = "$answered" (* Message has been replied to *)
13791379+ let forwarded = "$forwarded" (* Message has been forwarded (non-standard but common) *)
13801380+ let phishing = "$phishing" (* Message is suspected phishing *)
13811381+ let junk = "$junk" (* Message is junk/spam *)
13821382+ let notjunk = "$notjunk" (* Message is definitely not junk *)
13831383+end
13841384+13851385+(** Parser submodule *)
13861386+module Parser = struct
13871387+ let of_json = of_json
13881388+ let to_json = to_json
13891389+end
+584
jmap/jmap-mail/jmap_email.mli
···11+(** JMAP Email Type *)
22+33+open Jmap_core
44+55+(** Email address type (RFC 8621 Section 4.1.2.2) *)
66+module EmailAddress : sig
77+ type t = {
88+ name : string option;
99+ email : string;
1010+ }
1111+1212+ (** Accessors *)
1313+ val name : t -> string option
1414+ val email : t -> string
1515+1616+ (** Constructor *)
1717+ val v : ?name:string -> email:string -> unit -> t
1818+1919+ val of_json : Ezjsonm.value -> t
2020+ val to_json : t -> Ezjsonm.value
2121+end
2222+2323+(** Email header field (RFC 8621 Section 4.1.4) *)
2424+module EmailHeader : sig
2525+ type t = {
2626+ name : string;
2727+ value : string;
2828+ }
2929+3030+ (** Accessors *)
3131+ val name : t -> string
3232+ val value : t -> string
3333+3434+ (** Constructor *)
3535+ val v : name:string -> value:string -> t
3636+3737+ val of_json : Ezjsonm.value -> t
3838+ val to_json : t -> Ezjsonm.value
3939+end
4040+4141+(** MIME body part structure (RFC 8621 Section 4.1.4) *)
4242+module BodyPart : sig
4343+ type t = {
4444+ part_id : string option;
4545+ blob_id : Id.t option;
4646+ size : Primitives.UnsignedInt.t;
4747+ headers : EmailHeader.t list;
4848+ name : string option;
4949+ type_ : string;
5050+ charset : string option;
5151+ disposition : string option;
5252+ cid : string option;
5353+ language : string list option;
5454+ location : string option;
5555+ sub_parts : t list option;
5656+ }
5757+5858+ (** Accessors *)
5959+ val part_id : t -> string option
6060+ val blob_id : t -> Id.t option
6161+ val size : t -> Primitives.UnsignedInt.t
6262+ val headers : t -> EmailHeader.t list
6363+ val name : t -> string option
6464+ val type_ : t -> string
6565+ val charset : t -> string option
6666+ val disposition : t -> string option
6767+ val cid : t -> string option
6868+ val language : t -> string list option
6969+ val location : t -> string option
7070+ val sub_parts : t -> t list option
7171+7272+ (** Constructor *)
7373+ val v :
7474+ ?part_id:string ->
7575+ ?blob_id:Id.t ->
7676+ size:Primitives.UnsignedInt.t ->
7777+ headers:EmailHeader.t list ->
7878+ ?name:string ->
7979+ type_:string ->
8080+ ?charset:string ->
8181+ ?disposition:string ->
8282+ ?cid:string ->
8383+ ?language:string list ->
8484+ ?location:string ->
8585+ ?sub_parts:t list ->
8686+ unit ->
8787+ t
8888+8989+ val of_json : Ezjsonm.value -> t
9090+ val to_json : t -> Ezjsonm.value
9191+end
9292+9393+(** Body value content (RFC 8621 Section 4.1.4.3) *)
9494+module BodyValue : sig
9595+ type t = {
9696+ value : string;
9797+ is_encoding_problem : bool;
9898+ is_truncated : bool;
9999+ }
100100+101101+ (** Accessors *)
102102+ val value : t -> string
103103+ val is_encoding_problem : t -> bool
104104+ val is_truncated : t -> bool
105105+106106+ (** Constructor *)
107107+ val v : value:string -> is_encoding_problem:bool -> is_truncated:bool -> t
108108+109109+ val of_json : Ezjsonm.value -> t
110110+ val to_json : t -> Ezjsonm.value
111111+end
112112+113113+(** Email object type (RFC 8621 Section 4.1) *)
114114+type t = {
115115+ id : Id.t;
116116+ blob_id : Id.t;
117117+ thread_id : Id.t;
118118+ mailbox_ids : (Id.t * bool) list;
119119+ keywords : (string * bool) list;
120120+ size : Primitives.UnsignedInt.t;
121121+ received_at : Primitives.UTCDate.t;
122122+ message_id : string list option;
123123+ in_reply_to : string list option;
124124+ references : string list option;
125125+ sender : EmailAddress.t list option;
126126+ from : EmailAddress.t list option;
127127+ to_ : EmailAddress.t list option;
128128+ cc : EmailAddress.t list option;
129129+ bcc : EmailAddress.t list option;
130130+ reply_to : EmailAddress.t list option;
131131+ subject : string option;
132132+ sent_at : Primitives.Date.t option;
133133+ body_structure : BodyPart.t option;
134134+ body_values : (string * BodyValue.t) list option;
135135+ text_body : BodyPart.t list option;
136136+ html_body : BodyPart.t list option;
137137+ attachments : BodyPart.t list option;
138138+ has_attachment : bool;
139139+ preview : string;
140140+}
141141+142142+(** Accessors *)
143143+val id : t -> Id.t
144144+val blob_id : t -> Id.t
145145+val thread_id : t -> Id.t
146146+val mailbox_ids : t -> (Id.t * bool) list
147147+val keywords : t -> (string * bool) list
148148+val size : t -> Primitives.UnsignedInt.t
149149+val received_at : t -> Primitives.UTCDate.t
150150+val message_id : t -> string list option
151151+val in_reply_to : t -> string list option
152152+val references : t -> string list option
153153+val sender : t -> EmailAddress.t list option
154154+val from : t -> EmailAddress.t list option
155155+val to_ : t -> EmailAddress.t list option
156156+val cc : t -> EmailAddress.t list option
157157+val bcc : t -> EmailAddress.t list option
158158+val reply_to : t -> EmailAddress.t list option
159159+val subject : t -> string option
160160+val sent_at : t -> Primitives.Date.t option
161161+val body_structure : t -> BodyPart.t option
162162+val body_values : t -> (string * BodyValue.t) list option
163163+val text_body : t -> BodyPart.t list option
164164+val html_body : t -> BodyPart.t list option
165165+val attachments : t -> BodyPart.t list option
166166+val has_attachment : t -> bool
167167+val preview : t -> string
168168+169169+(** Constructor *)
170170+val v :
171171+ id:Id.t ->
172172+ blob_id:Id.t ->
173173+ thread_id:Id.t ->
174174+ mailbox_ids:(Id.t * bool) list ->
175175+ keywords:(string * bool) list ->
176176+ size:Primitives.UnsignedInt.t ->
177177+ received_at:Primitives.UTCDate.t ->
178178+ ?message_id:string list ->
179179+ ?in_reply_to:string list ->
180180+ ?references:string list ->
181181+ ?sender:EmailAddress.t list ->
182182+ ?from:EmailAddress.t list ->
183183+ ?to_:EmailAddress.t list ->
184184+ ?cc:EmailAddress.t list ->
185185+ ?bcc:EmailAddress.t list ->
186186+ ?reply_to:EmailAddress.t list ->
187187+ ?subject:string ->
188188+ ?sent_at:Primitives.Date.t ->
189189+ ?body_structure:BodyPart.t ->
190190+ ?body_values:(string * BodyValue.t) list ->
191191+ ?text_body:BodyPart.t list ->
192192+ ?html_body:BodyPart.t list ->
193193+ ?attachments:BodyPart.t list ->
194194+ has_attachment:bool ->
195195+ preview:string ->
196196+ unit ->
197197+ t
198198+199199+(** Email-specific filter for /query *)
200200+module Filter : sig
201201+ type t = {
202202+ in_mailbox : Id.t option;
203203+ in_mailbox_other_than : Id.t list option;
204204+ before : Primitives.UTCDate.t option;
205205+ after : Primitives.UTCDate.t option;
206206+ min_size : Primitives.UnsignedInt.t option;
207207+ max_size : Primitives.UnsignedInt.t option;
208208+ all_in_thread_have_keyword : string option;
209209+ some_in_thread_have_keyword : string option;
210210+ none_in_thread_have_keyword : string option;
211211+ has_keyword : string option;
212212+ not_keyword : string option;
213213+ has_attachment : bool option;
214214+ text : string option;
215215+ from : string option;
216216+ to_ : string option;
217217+ cc : string option;
218218+ bcc : string option;
219219+ subject : string option;
220220+ body : string option;
221221+ header : (string * string) list option;
222222+ }
223223+224224+ (** Accessors *)
225225+ val in_mailbox : t -> Id.t option
226226+ val in_mailbox_other_than : t -> Id.t list option
227227+ val before : t -> Primitives.UTCDate.t option
228228+ val after : t -> Primitives.UTCDate.t option
229229+ val min_size : t -> Primitives.UnsignedInt.t option
230230+ val max_size : t -> Primitives.UnsignedInt.t option
231231+ val all_in_thread_have_keyword : t -> string option
232232+ val some_in_thread_have_keyword : t -> string option
233233+ val none_in_thread_have_keyword : t -> string option
234234+ val has_keyword : t -> string option
235235+ val not_keyword : t -> string option
236236+ val has_attachment : t -> bool option
237237+ val text : t -> string option
238238+ val from : t -> string option
239239+ val to_ : t -> string option
240240+ val cc : t -> string option
241241+ val bcc : t -> string option
242242+ val subject : t -> string option
243243+ val body : t -> string option
244244+ val header : t -> (string * string) list option
245245+246246+ (** Constructor *)
247247+ val v :
248248+ ?in_mailbox:Id.t ->
249249+ ?in_mailbox_other_than:Id.t list ->
250250+ ?before:Primitives.UTCDate.t ->
251251+ ?after:Primitives.UTCDate.t ->
252252+ ?min_size:Primitives.UnsignedInt.t ->
253253+ ?max_size:Primitives.UnsignedInt.t ->
254254+ ?all_in_thread_have_keyword:string ->
255255+ ?some_in_thread_have_keyword:string ->
256256+ ?none_in_thread_have_keyword:string ->
257257+ ?has_keyword:string ->
258258+ ?not_keyword:string ->
259259+ ?has_attachment:bool ->
260260+ ?text:string ->
261261+ ?from:string ->
262262+ ?to_:string ->
263263+ ?cc:string ->
264264+ ?bcc:string ->
265265+ ?subject:string ->
266266+ ?body:string ->
267267+ ?header:(string * string) list ->
268268+ unit ->
269269+ t
270270+271271+ val of_json : Ezjsonm.value -> t
272272+ val to_json : t -> Ezjsonm.value
273273+end
274274+275275+(** Standard /get method *)
276276+module Get : sig
277277+ type request = {
278278+ account_id : Id.t;
279279+ ids : Id.t list option;
280280+ properties : string list option;
281281+ body_properties : string list option;
282282+ fetch_text_body_values : bool option;
283283+ fetch_html_body_values : bool option;
284284+ fetch_all_body_values : bool option;
285285+ max_body_value_bytes : Primitives.UnsignedInt.t option;
286286+ }
287287+288288+ type response = t Standard_methods.Get.response
289289+290290+ (** Accessors for request *)
291291+ val account_id : request -> Id.t
292292+ val ids : request -> Id.t list option
293293+ val properties : request -> string list option
294294+ val body_properties : request -> string list option
295295+ val fetch_text_body_values : request -> bool option
296296+ val fetch_html_body_values : request -> bool option
297297+ val fetch_all_body_values : request -> bool option
298298+ val max_body_value_bytes : request -> Primitives.UnsignedInt.t option
299299+300300+ (** Constructor for request *)
301301+ val request_v :
302302+ account_id:Id.t ->
303303+ ?ids:Id.t list ->
304304+ ?properties:string list ->
305305+ ?body_properties:string list ->
306306+ ?fetch_text_body_values:bool ->
307307+ ?fetch_html_body_values:bool ->
308308+ ?fetch_all_body_values:bool ->
309309+ ?max_body_value_bytes:Primitives.UnsignedInt.t ->
310310+ unit ->
311311+ request
312312+313313+ val request_of_json : Ezjsonm.value -> request
314314+ val request_to_json : request -> Ezjsonm.value
315315+ val response_of_json : Ezjsonm.value -> response
316316+end
317317+318318+(** Standard /changes method *)
319319+module Changes : sig
320320+ type request = Standard_methods.Changes.request
321321+ type response = Standard_methods.Changes.response
322322+323323+ val request_of_json : Ezjsonm.value -> request
324324+ val response_of_json : Ezjsonm.value -> response
325325+end
326326+327327+(** Standard /query method *)
328328+module Query : sig
329329+ type request = {
330330+ account_id : Id.t;
331331+ filter : Filter.t Jmap_core.Filter.t option;
332332+ sort : Comparator.t list option;
333333+ position : Primitives.Int53.t option;
334334+ anchor : Id.t option;
335335+ anchor_offset : Primitives.Int53.t option;
336336+ limit : Primitives.UnsignedInt.t option;
337337+ calculate_total : bool option;
338338+ collapse_threads : bool option;
339339+ }
340340+341341+ type response = Standard_methods.Query.response
342342+343343+ (** Accessors for request *)
344344+ val account_id : request -> Id.t
345345+ val filter : request -> Filter.t Jmap_core.Filter.t option
346346+ val sort : request -> Comparator.t list option
347347+ val position : request -> Primitives.Int53.t option
348348+ val anchor : request -> Id.t option
349349+ val anchor_offset : request -> Primitives.Int53.t option
350350+ val limit : request -> Primitives.UnsignedInt.t option
351351+ val calculate_total : request -> bool option
352352+ val collapse_threads : request -> bool option
353353+354354+ (** Constructor for request *)
355355+ val request_v :
356356+ account_id:Id.t ->
357357+ ?filter:Filter.t Jmap_core.Filter.t ->
358358+ ?sort:Comparator.t list ->
359359+ ?position:Primitives.Int53.t ->
360360+ ?anchor:Id.t ->
361361+ ?anchor_offset:Primitives.Int53.t ->
362362+ ?limit:Primitives.UnsignedInt.t ->
363363+ ?calculate_total:bool ->
364364+ ?collapse_threads:bool ->
365365+ unit ->
366366+ request
367367+368368+ val request_of_json : Ezjsonm.value -> request
369369+ val request_to_json : request -> Ezjsonm.value
370370+ val response_of_json : Ezjsonm.value -> response
371371+end
372372+373373+(** Standard /queryChanges method *)
374374+module QueryChanges : sig
375375+ type request = {
376376+ account_id : Id.t;
377377+ filter : Filter.t Jmap_core.Filter.t option;
378378+ sort : Comparator.t list option;
379379+ since_query_state : string;
380380+ max_changes : Primitives.UnsignedInt.t option;
381381+ up_to_id : Id.t option;
382382+ calculate_total : bool option;
383383+ collapse_threads : bool option;
384384+ }
385385+386386+ type response = Standard_methods.QueryChanges.response
387387+388388+ (** Accessors for request *)
389389+ val account_id : request -> Id.t
390390+ val filter : request -> Filter.t Jmap_core.Filter.t option
391391+ val sort : request -> Comparator.t list option
392392+ val since_query_state : request -> string
393393+ val max_changes : request -> Primitives.UnsignedInt.t option
394394+ val up_to_id : request -> Id.t option
395395+ val calculate_total : request -> bool option
396396+ val collapse_threads : request -> bool option
397397+398398+ (** Constructor for request *)
399399+ val request_v :
400400+ account_id:Id.t ->
401401+ ?filter:Filter.t Jmap_core.Filter.t ->
402402+ ?sort:Comparator.t list ->
403403+ since_query_state:string ->
404404+ ?max_changes:Primitives.UnsignedInt.t ->
405405+ ?up_to_id:Id.t ->
406406+ ?calculate_total:bool ->
407407+ ?collapse_threads:bool ->
408408+ unit ->
409409+ request
410410+411411+ val request_of_json : Ezjsonm.value -> request
412412+ val response_of_json : Ezjsonm.value -> response
413413+end
414414+415415+(** Standard /set method *)
416416+module Set : sig
417417+ type request = t Standard_methods.Set.request
418418+ type response = t Standard_methods.Set.response
419419+420420+ val request_of_json : Ezjsonm.value -> request
421421+ val response_of_json : Ezjsonm.value -> response
422422+end
423423+424424+(** Standard /copy method *)
425425+module Copy : sig
426426+ type request = t Standard_methods.Copy.request
427427+ type response = t Standard_methods.Copy.response
428428+429429+ val request_of_json : Ezjsonm.value -> request
430430+ val response_of_json : Ezjsonm.value -> response
431431+end
432432+433433+(** Email/import method *)
434434+module Import : sig
435435+ (** Email import request object *)
436436+ type import_email = {
437437+ blob_id : Id.t;
438438+ mailbox_ids : (Id.t * bool) list;
439439+ keywords : (string * bool) list;
440440+ received_at : Primitives.UTCDate.t option;
441441+ }
442442+443443+ type request = {
444444+ account_id : Id.t;
445445+ if_in_state : string option;
446446+ emails : (Id.t * import_email) list;
447447+ }
448448+449449+ type response = {
450450+ account_id : Id.t;
451451+ old_state : string option;
452452+ new_state : string;
453453+ created : (Id.t * t) list option;
454454+ not_created : (Id.t * Error.set_error_detail) list option;
455455+ }
456456+457457+ (** Accessors for import_email *)
458458+ val import_blob_id : import_email -> Id.t
459459+ val import_mailbox_ids : import_email -> (Id.t * bool) list
460460+ val import_keywords : import_email -> (string * bool) list
461461+ val import_received_at : import_email -> Primitives.UTCDate.t option
462462+463463+ (** Constructor for import_email *)
464464+ val import_email_v :
465465+ blob_id:Id.t ->
466466+ mailbox_ids:(Id.t * bool) list ->
467467+ keywords:(string * bool) list ->
468468+ ?received_at:Primitives.UTCDate.t ->
469469+ unit ->
470470+ import_email
471471+472472+ (** Accessors for request *)
473473+ val account_id : request -> Id.t
474474+ val if_in_state : request -> string option
475475+ val emails : request -> (Id.t * import_email) list
476476+477477+ (** Constructor for request *)
478478+ val request_v :
479479+ account_id:Id.t ->
480480+ ?if_in_state:string ->
481481+ emails:(Id.t * import_email) list ->
482482+ unit ->
483483+ request
484484+485485+ (** Accessors for response *)
486486+ val response_account_id : response -> Id.t
487487+ val old_state : response -> string option
488488+ val new_state : response -> string
489489+ val created : response -> (Id.t * t) list option
490490+ val not_created : response -> (Id.t * Error.set_error_detail) list option
491491+492492+ (** Constructor for response *)
493493+ val response_v :
494494+ account_id:Id.t ->
495495+ ?old_state:string ->
496496+ new_state:string ->
497497+ ?created:(Id.t * t) list ->
498498+ ?not_created:(Id.t * Error.set_error_detail) list ->
499499+ unit ->
500500+ response
501501+502502+ val request_of_json : Ezjsonm.value -> request
503503+ val response_of_json : Ezjsonm.value -> response
504504+end
505505+506506+(** Email/parse method *)
507507+module Parse : sig
508508+ type request = {
509509+ account_id : Id.t;
510510+ blob_ids : Id.t list;
511511+ properties : string list option;
512512+ body_properties : string list option;
513513+ fetch_text_body_values : bool option;
514514+ fetch_html_body_values : bool option;
515515+ fetch_all_body_values : bool option;
516516+ max_body_value_bytes : Primitives.UnsignedInt.t option;
517517+ }
518518+519519+ type response = {
520520+ account_id : Id.t;
521521+ parsed : (Id.t * t) list option;
522522+ not_parsable : Id.t list option;
523523+ not_found : Id.t list option;
524524+ }
525525+526526+ (** Accessors for request *)
527527+ val account_id : request -> Id.t
528528+ val blob_ids : request -> Id.t list
529529+ val properties : request -> string list option
530530+ val body_properties : request -> string list option
531531+ val fetch_text_body_values : request -> bool option
532532+ val fetch_html_body_values : request -> bool option
533533+ val fetch_all_body_values : request -> bool option
534534+ val max_body_value_bytes : request -> Primitives.UnsignedInt.t option
535535+536536+ (** Constructor for request *)
537537+ val request_v :
538538+ account_id:Id.t ->
539539+ blob_ids:Id.t list ->
540540+ ?properties:string list ->
541541+ ?body_properties:string list ->
542542+ ?fetch_text_body_values:bool ->
543543+ ?fetch_html_body_values:bool ->
544544+ ?fetch_all_body_values:bool ->
545545+ ?max_body_value_bytes:Primitives.UnsignedInt.t ->
546546+ unit ->
547547+ request
548548+549549+ (** Accessors for response *)
550550+ val response_account_id : response -> Id.t
551551+ val parsed : response -> (Id.t * t) list option
552552+ val not_parsable : response -> Id.t list option
553553+ val not_found : response -> Id.t list option
554554+555555+ (** Constructor for response *)
556556+ val response_v :
557557+ account_id:Id.t ->
558558+ ?parsed:(Id.t * t) list ->
559559+ ?not_parsable:Id.t list ->
560560+ ?not_found:Id.t list ->
561561+ unit ->
562562+ response
563563+564564+ val request_of_json : Ezjsonm.value -> request
565565+ val response_of_json : Ezjsonm.value -> response
566566+end
567567+568568+(** Parser submodule *)
569569+module Parser : sig
570570+ val of_json : Ezjsonm.value -> t
571571+ val to_json : t -> Ezjsonm.value
572572+end
573573+574574+(** Standard email keywords (RFC 8621 Section 4.1.1) *)
575575+module Keyword : sig
576576+ val seen : string
577577+ val draft : string
578578+ val flagged : string
579579+ val answered : string
580580+ val forwarded : string
581581+ val phishing : string
582582+ val junk : string
583583+ val notjunk : string
584584+end
+396
jmap/jmap-mail/jmap_email_submission.ml
···11+(** JMAP EmailSubmission Type
22+33+ An EmailSubmission represents the submission of an Email for delivery
44+ to one or more recipients. It tracks the delivery status and allows
55+ for features like delayed sending and undo.
66+77+open Jmap_core
88+99+ Reference: RFC 8621 Section 7 (Email Submission)
1010+ Test files:
1111+ - test/data/mail/email_submission_get_request.json
1212+ - test/data/mail/email_submission_get_response.json
1313+*)
1414+1515+(** SMTP Address with parameters (RFC 8621 Section 7.1.1) *)
1616+module Address = struct
1717+ type t = {
1818+ email : string; (** Email address *)
1919+ parameters : (string * string) list option; (** SMTP extension parameters *)
2020+ }
2121+2222+ (** Accessors *)
2323+ let email t = t.email
2424+ let parameters t = t.parameters
2525+2626+ (** Constructor *)
2727+ let v ~email ?parameters () = { email; parameters }
2828+2929+ (** Parse Address from JSON.
3030+ Test files: test/data/mail/email_submission_get_response.json (envelope field)
3131+3232+ Expected structure:
3333+ {
3434+ "email": "alice@example.com",
3535+ "parameters": null
3636+ }
3737+ *)
3838+ let of_json _json =
3939+ raise (Jmap_core.Error.Parse_error "Address.of_json not yet implemented")
4040+4141+ let to_json _t =
4242+ raise (Jmap_core.Error.Parse_error "Address.to_json not yet implemented")
4343+end
4444+4545+(** SMTP Envelope (RFC 8621 Section 7.1.1) *)
4646+module Envelope = struct
4747+ type t = {
4848+ mail_from : Address.t; (** MAIL FROM address *)
4949+ rcpt_to : Address.t list; (** RCPT TO addresses *)
5050+ }
5151+5252+ (** Accessors *)
5353+ let mail_from t = t.mail_from
5454+ let rcpt_to t = t.rcpt_to
5555+5656+ (** Constructor *)
5757+ let v ~mail_from ~rcpt_to = { mail_from; rcpt_to }
5858+5959+ (** Parse Envelope from JSON.
6060+ Test files: test/data/mail/email_submission_get_response.json (envelope field)
6161+6262+ Expected structure:
6363+ {
6464+ "mailFrom": {
6565+ "email": "alice@example.com",
6666+ "parameters": null
6767+ },
6868+ "rcptTo": [
6969+ {
7070+ "email": "bob@example.com",
7171+ "parameters": null
7272+ }
7373+ ]
7474+ }
7575+ *)
7676+ let of_json _json =
7777+ raise (Jmap_core.Error.Parse_error "Envelope.of_json not yet implemented")
7878+7979+ let to_json _t =
8080+ raise (Jmap_core.Error.Parse_error "Envelope.to_json not yet implemented")
8181+end
8282+8383+(** Delivery status for a single recipient (RFC 8621 Section 7.1.4) *)
8484+module DeliveryStatus = struct
8585+ (** Whether message was delivered *)
8686+ type delivered =
8787+ | Queued (** Message queued for delivery *)
8888+ | Yes (** Message delivered *)
8989+ | No (** Message not delivered (permanent failure) *)
9090+ | Unknown (** Delivery status unknown *)
9191+9292+ (** Whether message was displayed (MDN) *)
9393+ type displayed =
9494+ | Unknown (** No MDN received *)
9595+ | Yes (** Positive MDN received *)
9696+9797+ type t = {
9898+ smtp_reply : string; (** SMTP response string from server *)
9999+ delivered : delivered; (** Delivery status *)
100100+ displayed : displayed; (** Display status (from MDN) *)
101101+ }
102102+103103+ (** Accessors *)
104104+ let smtp_reply t = t.smtp_reply
105105+ let delivered t = t.delivered
106106+ let displayed t = t.displayed
107107+108108+ (** Constructor *)
109109+ let v ~smtp_reply ~delivered ~displayed = { smtp_reply; delivered; displayed }
110110+111111+ (** Parse DeliveryStatus from JSON.
112112+ Test files: test/data/mail/email_submission_get_response.json (deliveryStatus field)
113113+114114+ Expected structure:
115115+ {
116116+ "smtpReply": "250 2.0.0 OK",
117117+ "delivered": "yes",
118118+ "displayed": "unknown"
119119+ }
120120+ *)
121121+ let of_json _json =
122122+ raise (Jmap_core.Error.Parse_error "DeliveryStatus.of_json not yet implemented")
123123+124124+ let to_json _t =
125125+ raise (Jmap_core.Error.Parse_error "DeliveryStatus.to_json not yet implemented")
126126+127127+ let delivered_of_string = function
128128+ | "queued" -> Queued
129129+ | "yes" -> Yes
130130+ | "no" -> No
131131+ | "unknown" -> Unknown
132132+ | s -> raise (Invalid_argument ("Unknown delivered status: " ^ s))
133133+134134+ let delivered_to_string = function
135135+ | Queued -> "queued"
136136+ | Yes -> "yes"
137137+ | No -> "no"
138138+ | Unknown -> "unknown"
139139+140140+ let displayed_of_string = function
141141+ | "unknown" -> Unknown
142142+ | "yes" -> Yes
143143+ | s -> raise (Invalid_argument ("Unknown displayed status: " ^ s))
144144+145145+ let displayed_to_string = function
146146+ | Unknown -> "unknown"
147147+ | Yes -> "yes"
148148+end
149149+150150+(** Undo status (RFC 8621 Section 7.1.3) *)
151151+type undo_status =
152152+ | Pending (** Message can still be cancelled *)
153153+ | Final (** Message has been sent, cannot be cancelled *)
154154+ | Canceled (** Message was cancelled *)
155155+156156+(** EmailSubmission object type (RFC 8621 Section 7.1) *)
157157+type t = {
158158+ id : Jmap_core.Id.t; (** Immutable server-assigned id *)
159159+ identity_id : Jmap_core.Id.t; (** Identity to send from *)
160160+ email_id : Jmap_core.Id.t; (** Email to send *)
161161+ thread_id : Jmap_core.Id.t; (** Thread ID of email *)
162162+ envelope : Envelope.t option; (** SMTP envelope (null = derive from headers) *)
163163+ send_at : Jmap_core.Primitives.UTCDate.t; (** When to send (may be in future) *)
164164+ undo_status : undo_status; (** Whether message can be cancelled *)
165165+ delivery_status : (string * DeliveryStatus.t) list option; (** Map of email to delivery status *)
166166+ dsn_blob_ids : Jmap_core.Id.t list; (** Blob IDs of received DSN messages *)
167167+ mdn_blob_ids : Jmap_core.Id.t list; (** Blob IDs of received MDN messages *)
168168+}
169169+170170+(** Accessors *)
171171+let id t = t.id
172172+let identity_id t = t.identity_id
173173+let email_id t = t.email_id
174174+let thread_id t = t.thread_id
175175+let envelope t = t.envelope
176176+let send_at t = t.send_at
177177+let undo_status t = t.undo_status
178178+let delivery_status t = t.delivery_status
179179+let dsn_blob_ids t = t.dsn_blob_ids
180180+let mdn_blob_ids t = t.mdn_blob_ids
181181+182182+(** Constructor *)
183183+let v ~id ~identity_id ~email_id ~thread_id ?envelope ~send_at ~undo_status ?delivery_status ~dsn_blob_ids ~mdn_blob_ids () =
184184+ { id; identity_id; email_id; thread_id; envelope; send_at; undo_status; delivery_status; dsn_blob_ids; mdn_blob_ids }
185185+186186+(** Standard /get method (RFC 8621 Section 7.2) *)
187187+module Get = struct
188188+ type request = t Jmap_core.Standard_methods.Get.request
189189+ type response = t Jmap_core.Standard_methods.Get.response
190190+191191+ (** Parse get request from JSON.
192192+ Test files: test/data/mail/email_submission_get_request.json
193193+194194+ Expected structure:
195195+ {
196196+ "accountId": "u123456",
197197+ "ids": ["es001", "es002"]
198198+ }
199199+ *)
200200+ let request_of_json _json =
201201+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Get.request_of_json not yet implemented")
202202+203203+ (** Parse get response from JSON.
204204+ Test files: test/data/mail/email_submission_get_response.json
205205+206206+ Expected structure:
207207+ {
208208+ "accountId": "u123456",
209209+ "state": "es42:100",
210210+ "list": [
211211+ {
212212+ "id": "es001",
213213+ "identityId": "id001",
214214+ "emailId": "e050",
215215+ "threadId": "t025",
216216+ "envelope": { ... },
217217+ "sendAt": "2025-10-07T09:30:00Z",
218218+ "undoStatus": "final",
219219+ "deliveryStatus": { ... },
220220+ "dsnBlobIds": [],
221221+ "mdnBlobIds": []
222222+ }
223223+ ],
224224+ "notFound": []
225225+ }
226226+ *)
227227+ let response_of_json _json =
228228+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Get.response_of_json not yet implemented")
229229+end
230230+231231+(** Standard /changes method (RFC 8621 Section 7.3) *)
232232+module Changes = struct
233233+ type request = Jmap_core.Standard_methods.Changes.request
234234+ type response = Jmap_core.Standard_methods.Changes.response
235235+236236+ let request_of_json _json =
237237+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Changes.request_of_json not yet implemented")
238238+239239+ let response_of_json _json =
240240+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Changes.response_of_json not yet implemented")
241241+end
242242+243243+(** EmailSubmission-specific filter for /query (RFC 8621 Section 7.5) *)
244244+module Filter = struct
245245+ type t = {
246246+ identity_ids : Jmap_core.Id.t list option; (** Submission uses one of these identities *)
247247+ email_ids : Jmap_core.Id.t list option; (** Submission is for one of these emails *)
248248+ thread_ids : Jmap_core.Id.t list option; (** Submission is for email in one of these threads *)
249249+ undo_status : undo_status option; (** undoStatus equals this *)
250250+ before : Jmap_core.Primitives.UTCDate.t option; (** sendAt < this *)
251251+ after : Jmap_core.Primitives.UTCDate.t option; (** sendAt >= this *)
252252+ }
253253+254254+ (** Accessors *)
255255+ let identity_ids t = t.identity_ids
256256+ let email_ids t = t.email_ids
257257+ let thread_ids t = t.thread_ids
258258+ let undo_status t = t.undo_status
259259+ let before t = t.before
260260+ let after t = t.after
261261+262262+ (** Constructor *)
263263+ let v ?identity_ids ?email_ids ?thread_ids ?undo_status ?before ?after () =
264264+ { identity_ids; email_ids; thread_ids; undo_status; before; after }
265265+266266+ let of_json _json =
267267+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Filter.of_json not yet implemented")
268268+end
269269+270270+(** Standard /query method (RFC 8621 Section 7.5) *)
271271+module Query = struct
272272+ type request = Filter.t Jmap_core.Standard_methods.Query.request
273273+ type response = Jmap_core.Standard_methods.Query.response
274274+275275+ let request_of_json _json =
276276+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Query.request_of_json not yet implemented")
277277+278278+ let response_of_json _json =
279279+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Query.response_of_json not yet implemented")
280280+end
281281+282282+(** Standard /queryChanges method (RFC 8621 Section 7.6) *)
283283+module QueryChanges = struct
284284+ type request = Filter.t Jmap_core.Standard_methods.QueryChanges.request
285285+ type response = Jmap_core.Standard_methods.QueryChanges.response
286286+287287+ let request_of_json _json =
288288+ raise (Jmap_core.Error.Parse_error "EmailSubmission.QueryChanges.request_of_json not yet implemented")
289289+290290+ let response_of_json _json =
291291+ raise (Jmap_core.Error.Parse_error "EmailSubmission.QueryChanges.response_of_json not yet implemented")
292292+end
293293+294294+(** Standard /set method (RFC 8621 Section 7.4)
295295+296296+ EmailSubmission/set is used to:
297297+ - Create new submissions (send email)
298298+ - Update existing submissions (e.g., cancel pending send)
299299+ - Destroy submissions (for cleanup only - cannot unsend)
300300+*)
301301+module Set = struct
302302+ (** On success action for EmailSubmission/set create *)
303303+ type on_success = {
304304+ set_email_keywords : (Jmap_core.Id.t * (string * bool) list) option; (** Set keywords on sent email *)
305305+ }
306306+307307+ type request = {
308308+ account_id : Jmap_core.Id.t;
309309+ if_in_state : string option;
310310+ create : (Jmap_core.Id.t * t) list option;
311311+ update : (Jmap_core.Id.t * Jmap_core.Standard_methods.Set.patch_object) list option;
312312+ destroy : Jmap_core.Id.t list option;
313313+ (* EmailSubmission-specific *)
314314+ on_success_update_email : (Jmap_core.Id.t * on_success) list option; (** Actions to perform on success *)
315315+ on_success_destroy_email : Jmap_core.Id.t list option; (** Email IDs to destroy on success *)
316316+ }
317317+318318+ type response = t Jmap_core.Standard_methods.Set.response
319319+320320+ (** Accessors for on_success *)
321321+ let on_success_set_email_keywords os = os.set_email_keywords
322322+323323+ (** Constructor for on_success *)
324324+ let on_success_v ?set_email_keywords () =
325325+ { set_email_keywords }
326326+327327+ (** Accessors for request *)
328328+ let account_id req = req.account_id
329329+ let if_in_state req = req.if_in_state
330330+ let create req = req.create
331331+ let update req = req.update
332332+ let destroy req = req.destroy
333333+ let on_success_update_email req = req.on_success_update_email
334334+ let on_success_destroy_email req = req.on_success_destroy_email
335335+336336+ (** Constructor for request *)
337337+ let request_v ~account_id ?if_in_state ?create ?update ?destroy
338338+ ?on_success_update_email ?on_success_destroy_email () =
339339+ { account_id; if_in_state; create; update; destroy;
340340+ on_success_update_email; on_success_destroy_email }
341341+342342+ let request_of_json _json =
343343+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Set.request_of_json not yet implemented")
344344+345345+ let response_of_json _json =
346346+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Set.response_of_json not yet implemented")
347347+end
348348+349349+(** Parser submodule *)
350350+module Parser = struct
351351+ (** Parse EmailSubmission from JSON.
352352+ Test files: test/data/mail/email_submission_get_response.json (list field)
353353+354354+ Expected structure:
355355+ {
356356+ "id": "es001",
357357+ "identityId": "id001",
358358+ "emailId": "e050",
359359+ "threadId": "t025",
360360+ "envelope": {
361361+ "mailFrom": { "email": "alice@example.com", "parameters": null },
362362+ "rcptTo": [{ "email": "bob@example.com", "parameters": null }]
363363+ },
364364+ "sendAt": "2025-10-07T09:30:00Z",
365365+ "undoStatus": "final",
366366+ "deliveryStatus": {
367367+ "bob@example.com": {
368368+ "smtpReply": "250 2.0.0 OK",
369369+ "delivered": "yes",
370370+ "displayed": "unknown"
371371+ }
372372+ },
373373+ "dsnBlobIds": [],
374374+ "mdnBlobIds": []
375375+ }
376376+ *)
377377+ let of_json _json =
378378+ (* TODO: Implement JSON parsing *)
379379+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Parser.of_json not yet implemented")
380380+381381+ let to_json _t =
382382+ (* TODO: Implement JSON serialization *)
383383+ raise (Jmap_core.Error.Parse_error "EmailSubmission.Parser.to_json not yet implemented")
384384+end
385385+386386+(** Helper functions for undo_status *)
387387+let undo_status_of_string = function
388388+ | "pending" -> Pending
389389+ | "final" -> Final
390390+ | "canceled" -> Canceled
391391+ | s -> raise (Invalid_argument ("Unknown undo status: " ^ s))
392392+393393+let undo_status_to_string = function
394394+ | Pending -> "pending"
395395+ | Final -> "final"
396396+ | Canceled -> "canceled"
+255
jmap/jmap-mail/jmap_email_submission.mli
···11+(** JMAP EmailSubmission Type *)
22+33+open Jmap_core
44+55+(** SMTP Address with parameters (RFC 8621 Section 7.1.1) *)
66+module Address : sig
77+ type t = {
88+ email : string;
99+ parameters : (string * string) list option;
1010+ }
1111+1212+ (** Accessors *)
1313+ val email : t -> string
1414+ val parameters : t -> (string * string) list option
1515+1616+ (** Constructor *)
1717+ val v : email:string -> ?parameters:(string * string) list -> unit -> t
1818+1919+ val of_json : Ezjsonm.value -> t
2020+ val to_json : t -> Ezjsonm.value
2121+end
2222+2323+(** SMTP Envelope (RFC 8621 Section 7.1.1) *)
2424+module Envelope : sig
2525+ type t = {
2626+ mail_from : Address.t;
2727+ rcpt_to : Address.t list;
2828+ }
2929+3030+ (** Accessors *)
3131+ val mail_from : t -> Address.t
3232+ val rcpt_to : t -> Address.t list
3333+3434+ (** Constructor *)
3535+ val v : mail_from:Address.t -> rcpt_to:Address.t list -> t
3636+3737+ val of_json : Ezjsonm.value -> t
3838+ val to_json : t -> Ezjsonm.value
3939+end
4040+4141+(** Delivery status for a single recipient (RFC 8621 Section 7.1.4) *)
4242+module DeliveryStatus : sig
4343+ (** Whether message was delivered *)
4444+ type delivered =
4545+ | Queued
4646+ | Yes
4747+ | No
4848+ | Unknown
4949+5050+ (** Whether message was displayed (MDN) *)
5151+ type displayed =
5252+ | Unknown
5353+ | Yes
5454+5555+ type t = {
5656+ smtp_reply : string;
5757+ delivered : delivered;
5858+ displayed : displayed;
5959+ }
6060+6161+ (** Accessors *)
6262+ val smtp_reply : t -> string
6363+ val delivered : t -> delivered
6464+ val displayed : t -> displayed
6565+6666+ (** Constructor *)
6767+ val v : smtp_reply:string -> delivered:delivered -> displayed:displayed -> t
6868+6969+ val of_json : Ezjsonm.value -> t
7070+ val to_json : t -> Ezjsonm.value
7171+7272+ val delivered_of_string : string -> delivered
7373+ val delivered_to_string : delivered -> string
7474+ val displayed_of_string : string -> displayed
7575+ val displayed_to_string : displayed -> string
7676+end
7777+7878+(** Undo status (RFC 8621 Section 7.1.3) *)
7979+type undo_status =
8080+ | Pending
8181+ | Final
8282+ | Canceled
8383+8484+(** EmailSubmission object type (RFC 8621 Section 7.1) *)
8585+type t = {
8686+ id : Id.t;
8787+ identity_id : Id.t;
8888+ email_id : Id.t;
8989+ thread_id : Id.t;
9090+ envelope : Envelope.t option;
9191+ send_at : Primitives.UTCDate.t;
9292+ undo_status : undo_status;
9393+ delivery_status : (string * DeliveryStatus.t) list option;
9494+ dsn_blob_ids : Id.t list;
9595+ mdn_blob_ids : Id.t list;
9696+}
9797+9898+(** Accessors *)
9999+val id : t -> Id.t
100100+val identity_id : t -> Id.t
101101+val email_id : t -> Id.t
102102+val thread_id : t -> Id.t
103103+val envelope : t -> Envelope.t option
104104+val send_at : t -> Primitives.UTCDate.t
105105+val undo_status : t -> undo_status
106106+val delivery_status : t -> (string * DeliveryStatus.t) list option
107107+val dsn_blob_ids : t -> Id.t list
108108+val mdn_blob_ids : t -> Id.t list
109109+110110+(** Constructor *)
111111+val v :
112112+ id:Id.t ->
113113+ identity_id:Id.t ->
114114+ email_id:Id.t ->
115115+ thread_id:Id.t ->
116116+ ?envelope:Envelope.t ->
117117+ send_at:Primitives.UTCDate.t ->
118118+ undo_status:undo_status ->
119119+ ?delivery_status:(string * DeliveryStatus.t) list ->
120120+ dsn_blob_ids:Id.t list ->
121121+ mdn_blob_ids:Id.t list ->
122122+ unit ->
123123+ t
124124+125125+(** Standard /get method *)
126126+module Get : sig
127127+ type request = t Standard_methods.Get.request
128128+ type response = t Standard_methods.Get.response
129129+130130+ val request_of_json : Ezjsonm.value -> request
131131+ val response_of_json : Ezjsonm.value -> response
132132+end
133133+134134+(** Standard /changes method *)
135135+module Changes : sig
136136+ type request = Standard_methods.Changes.request
137137+ type response = Standard_methods.Changes.response
138138+139139+ val request_of_json : Ezjsonm.value -> request
140140+ val response_of_json : Ezjsonm.value -> response
141141+end
142142+143143+(** EmailSubmission-specific filter for /query *)
144144+module Filter : sig
145145+ type t = {
146146+ identity_ids : Id.t list option;
147147+ email_ids : Id.t list option;
148148+ thread_ids : Id.t list option;
149149+ undo_status : undo_status option;
150150+ before : Primitives.UTCDate.t option;
151151+ after : Primitives.UTCDate.t option;
152152+ }
153153+154154+ (** Accessors *)
155155+ val identity_ids : t -> Id.t list option
156156+ val email_ids : t -> Id.t list option
157157+ val thread_ids : t -> Id.t list option
158158+ val undo_status : t -> undo_status option
159159+ val before : t -> Primitives.UTCDate.t option
160160+ val after : t -> Primitives.UTCDate.t option
161161+162162+ (** Constructor *)
163163+ val v :
164164+ ?identity_ids:Id.t list ->
165165+ ?email_ids:Id.t list ->
166166+ ?thread_ids:Id.t list ->
167167+ ?undo_status:undo_status ->
168168+ ?before:Primitives.UTCDate.t ->
169169+ ?after:Primitives.UTCDate.t ->
170170+ unit ->
171171+ t
172172+173173+ val of_json : Ezjsonm.value -> t
174174+end
175175+176176+(** Standard /query method *)
177177+module Query : sig
178178+ type request = Filter.t Standard_methods.Query.request
179179+ type response = Standard_methods.Query.response
180180+181181+ val request_of_json : Ezjsonm.value -> request
182182+ val response_of_json : Ezjsonm.value -> response
183183+end
184184+185185+(** Standard /queryChanges method *)
186186+module QueryChanges : sig
187187+ type request = Filter.t Standard_methods.QueryChanges.request
188188+ type response = Standard_methods.QueryChanges.response
189189+190190+ val request_of_json : Ezjsonm.value -> request
191191+ val response_of_json : Ezjsonm.value -> response
192192+end
193193+194194+(** Standard /set method *)
195195+module Set : sig
196196+ (** On success action for EmailSubmission/set create *)
197197+ type on_success = {
198198+ set_email_keywords : (Id.t * (string * bool) list) option;
199199+ }
200200+201201+ type request = {
202202+ account_id : Id.t;
203203+ if_in_state : string option;
204204+ create : (Id.t * t) list option;
205205+ update : (Id.t * Standard_methods.Set.patch_object) list option;
206206+ destroy : Id.t list option;
207207+ on_success_update_email : (Id.t * on_success) list option;
208208+ on_success_destroy_email : Id.t list option;
209209+ }
210210+211211+ type response = t Standard_methods.Set.response
212212+213213+ (** Accessors for on_success *)
214214+ val on_success_set_email_keywords : on_success -> (Id.t * (string * bool) list) option
215215+216216+ (** Constructor for on_success *)
217217+ val on_success_v :
218218+ ?set_email_keywords:(Id.t * (string * bool) list) ->
219219+ unit ->
220220+ on_success
221221+222222+ (** Accessors for request *)
223223+ val account_id : request -> Id.t
224224+ val if_in_state : request -> string option
225225+ val create : request -> (Id.t * t) list option
226226+ val update : request -> (Id.t * Standard_methods.Set.patch_object) list option
227227+ val destroy : request -> Id.t list option
228228+ val on_success_update_email : request -> (Id.t * on_success) list option
229229+ val on_success_destroy_email : request -> Id.t list option
230230+231231+ (** Constructor for request *)
232232+ val request_v :
233233+ account_id:Id.t ->
234234+ ?if_in_state:string ->
235235+ ?create:(Id.t * t) list ->
236236+ ?update:(Id.t * Standard_methods.Set.patch_object) list ->
237237+ ?destroy:Id.t list ->
238238+ ?on_success_update_email:(Id.t * on_success) list ->
239239+ ?on_success_destroy_email:Id.t list ->
240240+ unit ->
241241+ request
242242+243243+ val request_of_json : Ezjsonm.value -> request
244244+ val response_of_json : Ezjsonm.value -> response
245245+end
246246+247247+(** Parser submodule *)
248248+module Parser : sig
249249+ val of_json : Ezjsonm.value -> t
250250+ val to_json : t -> Ezjsonm.value
251251+end
252252+253253+(** Helper functions for undo_status *)
254254+val undo_status_of_string : string -> undo_status
255255+val undo_status_to_string : undo_status -> string
+142
jmap/jmap-mail/jmap_identity.ml
···11+(** JMAP Identity Type
22+33+ An Identity represents an email address and associated metadata that
44+ the user may send from. Users may have multiple identities for different
55+ purposes (work, personal, aliases, etc.).
66+77+open Jmap_core
88+99+ Reference: RFC 8621 Section 6 (Identity)
1010+ Test files:
1111+ - test/data/mail/identity_get_request.json
1212+ - test/data/mail/identity_get_response.json
1313+*)
1414+1515+(** Identity object type (RFC 8621 Section 6.1) *)
1616+type t = {
1717+ id : Jmap_core.Id.t; (** Immutable server-assigned id *)
1818+ name : string; (** Display name for this identity (e.g., "Alice Jones") *)
1919+ email : string; (** Email address (e.g., "alice@example.com") *)
2020+ reply_to : Jmap_email.EmailAddress.t list option; (** Reply-To addresses to use *)
2121+ bcc : Jmap_email.EmailAddress.t list option; (** BCC addresses to automatically add *)
2222+ text_signature : string; (** Signature to insert for text/plain messages *)
2323+ html_signature : string; (** Signature to insert for text/html messages *)
2424+ may_delete : bool; (** Can user delete this identity? *)
2525+}
2626+2727+(** Accessors *)
2828+let id t = t.id
2929+let name t = t.name
3030+let email t = t.email
3131+let reply_to t = t.reply_to
3232+let bcc t = t.bcc
3333+let text_signature t = t.text_signature
3434+let html_signature t = t.html_signature
3535+let may_delete t = t.may_delete
3636+3737+(** Constructor *)
3838+let v ~id ~name ~email ?reply_to ?bcc ~text_signature ~html_signature ~may_delete () =
3939+ { id; name; email; reply_to; bcc; text_signature; html_signature; may_delete }
4040+4141+(** Standard /get method (RFC 8621 Section 6.2)
4242+4343+ Identities are mostly read-only. They support /get and /changes,
4444+ but /set is typically limited to updating signatures. The server
4545+ controls which identities exist based on account configuration.
4646+*)
4747+module Get = struct
4848+ type request = t Jmap_core.Standard_methods.Get.request
4949+ type response = t Jmap_core.Standard_methods.Get.response
5050+5151+ (** Parse get request from JSON.
5252+ Test files: test/data/mail/identity_get_request.json
5353+5454+ Expected structure:
5555+ {
5656+ "accountId": "u123456",
5757+ "ids": null
5858+ }
5959+ *)
6060+ let request_of_json _json =
6161+ raise (Jmap_core.Error.Parse_error "Identity.Get.request_of_json not yet implemented")
6262+6363+ (** Parse get response from JSON.
6464+ Test files: test/data/mail/identity_get_response.json
6565+6666+ Expected structure:
6767+ {
6868+ "accountId": "u123456",
6969+ "state": "i42:100",
7070+ "list": [
7171+ {
7272+ "id": "id001",
7373+ "name": "Alice Jones",
7474+ "email": "alice@example.com",
7575+ "replyTo": null,
7676+ "bcc": null,
7777+ "textSignature": "Best regards,\nAlice Jones",
7878+ "htmlSignature": "<p>Best regards,<br>Alice Jones</p>",
7979+ "mayDelete": false
8080+ }
8181+ ],
8282+ "notFound": []
8383+ }
8484+ *)
8585+ let response_of_json _json =
8686+ raise (Jmap_core.Error.Parse_error "Identity.Get.response_of_json not yet implemented")
8787+end
8888+8989+(** Standard /changes method (RFC 8621 Section 6.3) *)
9090+module Changes = struct
9191+ type request = Jmap_core.Standard_methods.Changes.request
9292+ type response = Jmap_core.Standard_methods.Changes.response
9393+9494+ let request_of_json _json =
9595+ raise (Jmap_core.Error.Parse_error "Identity.Changes.request_of_json not yet implemented")
9696+9797+ let response_of_json _json =
9898+ raise (Jmap_core.Error.Parse_error "Identity.Changes.response_of_json not yet implemented")
9999+end
100100+101101+(** Standard /set method (RFC 8621 Section 6.4)
102102+103103+ Most servers only allow updating textSignature and htmlSignature fields.
104104+ Creating and destroying identities is typically not allowed, as identities
105105+ are derived from server/account configuration.
106106+*)
107107+module Set = struct
108108+ type request = t Jmap_core.Standard_methods.Set.request
109109+ type response = t Jmap_core.Standard_methods.Set.response
110110+111111+ let request_of_json _json =
112112+ raise (Jmap_core.Error.Parse_error "Identity.Set.request_of_json not yet implemented")
113113+114114+ let response_of_json _json =
115115+ raise (Jmap_core.Error.Parse_error "Identity.Set.response_of_json not yet implemented")
116116+end
117117+118118+(** Parser submodule *)
119119+module Parser = struct
120120+ (** Parse Identity from JSON.
121121+ Test files: test/data/mail/identity_get_response.json (list field)
122122+123123+ Expected structure:
124124+ {
125125+ "id": "id001",
126126+ "name": "Alice Jones",
127127+ "email": "alice@example.com",
128128+ "replyTo": null,
129129+ "bcc": null,
130130+ "textSignature": "Best regards,\nAlice Jones\nSoftware Engineer\nexample.com",
131131+ "htmlSignature": "<div><p>Best regards,</p><p><strong>Alice Jones</strong><br>Software Engineer<br>example.com</p></div>",
132132+ "mayDelete": false
133133+ }
134134+ *)
135135+ let of_json _json =
136136+ (* TODO: Implement JSON parsing *)
137137+ raise (Jmap_core.Error.Parse_error "Identity.Parser.of_json not yet implemented")
138138+139139+ let to_json _t =
140140+ (* TODO: Implement JSON serialization *)
141141+ raise (Jmap_core.Error.Parse_error "Identity.Parser.to_json not yet implemented")
142142+end
+71
jmap/jmap-mail/jmap_identity.mli
···11+(** JMAP Identity Type *)
22+33+open Jmap_core
44+55+(** Identity object type (RFC 8621 Section 6.1) *)
66+type t = {
77+ id : Id.t;
88+ name : string;
99+ email : string;
1010+ reply_to : Jmap_email.EmailAddress.t list option;
1111+ bcc : Jmap_email.EmailAddress.t list option;
1212+ text_signature : string;
1313+ html_signature : string;
1414+ may_delete : bool;
1515+}
1616+1717+(** Accessors *)
1818+val id : t -> Id.t
1919+val name : t -> string
2020+val email : t -> string
2121+val reply_to : t -> Jmap_email.EmailAddress.t list option
2222+val bcc : t -> Jmap_email.EmailAddress.t list option
2323+val text_signature : t -> string
2424+val html_signature : t -> string
2525+val may_delete : t -> bool
2626+2727+(** Constructor *)
2828+val v :
2929+ id:Id.t ->
3030+ name:string ->
3131+ email:string ->
3232+ ?reply_to:Jmap_email.EmailAddress.t list ->
3333+ ?bcc:Jmap_email.EmailAddress.t list ->
3434+ text_signature:string ->
3535+ html_signature:string ->
3636+ may_delete:bool ->
3737+ unit ->
3838+ t
3939+4040+(** Standard /get method *)
4141+module Get : sig
4242+ type request = t Standard_methods.Get.request
4343+ type response = t Standard_methods.Get.response
4444+4545+ val request_of_json : Ezjsonm.value -> request
4646+ val response_of_json : Ezjsonm.value -> response
4747+end
4848+4949+(** Standard /changes method *)
5050+module Changes : sig
5151+ type request = Standard_methods.Changes.request
5252+ type response = Standard_methods.Changes.response
5353+5454+ val request_of_json : Ezjsonm.value -> request
5555+ val response_of_json : Ezjsonm.value -> response
5656+end
5757+5858+(** Standard /set method *)
5959+module Set : sig
6060+ type request = t Standard_methods.Set.request
6161+ type response = t Standard_methods.Set.response
6262+6363+ val request_of_json : Ezjsonm.value -> request
6464+ val response_of_json : Ezjsonm.value -> response
6565+end
6666+6767+(** Parser submodule *)
6868+module Parser : sig
6969+ val of_json : Ezjsonm.value -> t
7070+ val to_json : t -> Ezjsonm.value
7171+end
···11+(** JMAP Mailbox Type
22+33+ A Mailbox represents a named set of emails. Mailboxes can be hierarchical,
44+ with a tree structure defined by the parentId property.
55+66+open Jmap_core
77+88+ Reference: RFC 8621 Section 2 (Mailboxes)
99+ Test files:
1010+ - test/data/mail/mailbox_get_request.json
1111+ - test/data/mail/mailbox_get_response.json
1212+ - test/data/mail/mailbox_query_request.json
1313+ - test/data/mail/mailbox_query_response.json
1414+ - test/data/mail/mailbox_set_request.json
1515+ - test/data/mail/mailbox_set_response.json
1616+*)
1717+1818+(** Mailbox access rights (RFC 8621 Section 2.1) *)
1919+module Rights = struct
2020+ type t = {
2121+ may_read_items : bool; (** User may fetch and read emails in this mailbox *)
2222+ may_add_items : bool; (** User may add mailboxIds for emails to this mailbox *)
2323+ may_remove_items : bool; (** User may remove mailboxIds for emails from this mailbox *)
2424+ may_set_seen : bool; (** User may modify $seen keyword on emails in this mailbox *)
2525+ may_set_keywords : bool; (** User may modify keywords (except $seen) on emails in this mailbox *)
2626+ may_create_child : bool; (** User may create a mailbox with this mailbox as parent *)
2727+ may_rename : bool; (** User may rename this mailbox *)
2828+ may_delete : bool; (** User may delete this mailbox *)
2929+ may_submit : bool; (** User may use this mailbox as source for EmailSubmission *)
3030+ }
3131+3232+ (** Parse Rights from JSON.
3333+ Test files: test/data/mail/mailbox_get_response.json (myRights field)
3434+3535+ Expected JSON structure:
3636+ {
3737+ "mayReadItems": true,
3838+ "mayAddItems": true,
3939+ "mayRemoveItems": true,
4040+ "maySetSeen": true,
4141+ "maySetKeywords": true,
4242+ "mayCreateChild": true,
4343+ "mayRename": false,
4444+ "mayDelete": false,
4545+ "maySubmit": true
4646+ }
4747+ *)
4848+ let of_json json =
4949+ let open Jmap_core.Parser.Helpers in
5050+ let fields = expect_object json in
5151+ {
5252+ may_read_items = get_bool "mayReadItems" fields;
5353+ may_add_items = get_bool "mayAddItems" fields;
5454+ may_remove_items = get_bool "mayRemoveItems" fields;
5555+ may_set_seen = get_bool "maySetSeen" fields;
5656+ may_set_keywords = get_bool "maySetKeywords" fields;
5757+ may_create_child = get_bool "mayCreateChild" fields;
5858+ may_rename = get_bool "mayRename" fields;
5959+ may_delete = get_bool "mayDelete" fields;
6060+ may_submit = get_bool "maySubmit" fields;
6161+ }
6262+6363+ let to_json t =
6464+ `O [
6565+ ("mayReadItems", `Bool t.may_read_items);
6666+ ("mayAddItems", `Bool t.may_add_items);
6767+ ("mayRemoveItems", `Bool t.may_remove_items);
6868+ ("maySetSeen", `Bool t.may_set_seen);
6969+ ("maySetKeywords", `Bool t.may_set_keywords);
7070+ ("mayCreateChild", `Bool t.may_create_child);
7171+ ("mayRename", `Bool t.may_rename);
7272+ ("mayDelete", `Bool t.may_delete);
7373+ ("maySubmit", `Bool t.may_submit);
7474+ ]
7575+7676+ (* Accessors *)
7777+ let may_read_items t = t.may_read_items
7878+ let may_add_items t = t.may_add_items
7979+ let may_remove_items t = t.may_remove_items
8080+ let may_set_seen t = t.may_set_seen
8181+ let may_set_keywords t = t.may_set_keywords
8282+ let may_create_child t = t.may_create_child
8383+ let may_rename t = t.may_rename
8484+ let may_delete t = t.may_delete
8585+ let may_submit t = t.may_submit
8686+8787+ (* Constructor *)
8888+ let v ~may_read_items ~may_add_items ~may_remove_items ~may_set_seen
8989+ ~may_set_keywords ~may_create_child ~may_rename ~may_delete ~may_submit =
9090+ { may_read_items; may_add_items; may_remove_items; may_set_seen;
9191+ may_set_keywords; may_create_child; may_rename; may_delete; may_submit }
9292+end
9393+9494+(** Mailbox object type *)
9595+type t = {
9696+ id : Jmap_core.Id.t; (** Immutable server-assigned id *)
9797+ name : string; (** User-visible mailbox name *)
9898+ parent_id : Jmap_core.Id.t option; (** Parent mailbox id (null for top-level) *)
9999+ role : string option; (** Standard role (inbox, trash, sent, etc.) *)
100100+ sort_order : Jmap_core.Primitives.UnsignedInt.t; (** Sort order for display *)
101101+ total_emails : Jmap_core.Primitives.UnsignedInt.t; (** Total number of emails in mailbox *)
102102+ unread_emails : Jmap_core.Primitives.UnsignedInt.t; (** Number of emails without $seen keyword *)
103103+ total_threads : Jmap_core.Primitives.UnsignedInt.t; (** Total number of threads with emails in mailbox *)
104104+ unread_threads : Jmap_core.Primitives.UnsignedInt.t; (** Number of threads with unread emails in mailbox *)
105105+ my_rights : Rights.t; (** Current user's access rights *)
106106+ is_subscribed : bool; (** Whether user is subscribed to this mailbox *)
107107+}
108108+109109+(** Accessors *)
110110+let id t = t.id
111111+let name t = t.name
112112+let parent_id t = t.parent_id
113113+let role t = t.role
114114+let sort_order t = t.sort_order
115115+let total_emails t = t.total_emails
116116+let unread_emails t = t.unread_emails
117117+let total_threads t = t.total_threads
118118+let unread_threads t = t.unread_threads
119119+let my_rights t = t.my_rights
120120+let is_subscribed t = t.is_subscribed
121121+122122+(** Constructor *)
123123+let v ~id ~name ?parent_id ?role ~sort_order ~total_emails ~unread_emails
124124+ ~total_threads ~unread_threads ~my_rights ~is_subscribed () =
125125+ { id; name; parent_id; role; sort_order; total_emails; unread_emails;
126126+ total_threads; unread_threads; my_rights; is_subscribed }
127127+128128+(** Parser submodule *)
129129+module Parser = struct
130130+ (** Parse Mailbox from JSON.
131131+ Test files: test/data/mail/mailbox_get_response.json (list field)
132132+133133+ Expected structure:
134134+ {
135135+ "id": "mb001",
136136+ "name": "INBOX",
137137+ "parentId": null,
138138+ "role": "inbox",
139139+ "sortOrder": 10,
140140+ "totalEmails": 1523,
141141+ "unreadEmails": 42,
142142+ "totalThreads": 987,
143143+ "unreadThreads": 35,
144144+ "myRights": { ... },
145145+ "isSubscribed": true
146146+ }
147147+ *)
148148+ let of_json json =
149149+ let open Jmap_core.Parser.Helpers in
150150+ let fields = expect_object json in
151151+ let id = Jmap_core.Id.of_json (require_field "id" fields) in
152152+ let name = get_string "name" fields in
153153+ let parent_id = match find_field "parentId" fields with
154154+ | Some `Null | None -> None
155155+ | Some v -> Some (Jmap_core.Id.of_json v)
156156+ in
157157+ let role = match find_field "role" fields with
158158+ | Some `Null | None -> None
159159+ | Some (`String s) -> Some s
160160+ | Some _ -> raise (Jmap_core.Error.Parse_error "role must be a string or null")
161161+ in
162162+ let sort_order = Jmap_core.Primitives.UnsignedInt.of_json (require_field "sortOrder" fields) in
163163+ let total_emails = Jmap_core.Primitives.UnsignedInt.of_json (require_field "totalEmails" fields) in
164164+ let unread_emails = Jmap_core.Primitives.UnsignedInt.of_json (require_field "unreadEmails" fields) in
165165+ let total_threads = Jmap_core.Primitives.UnsignedInt.of_json (require_field "totalThreads" fields) in
166166+ let unread_threads = Jmap_core.Primitives.UnsignedInt.of_json (require_field "unreadThreads" fields) in
167167+ let my_rights = Rights.of_json (require_field "myRights" fields) in
168168+ let is_subscribed = get_bool "isSubscribed" fields in
169169+ { id; name; parent_id; role; sort_order; total_emails; unread_emails;
170170+ total_threads; unread_threads; my_rights; is_subscribed }
171171+172172+ let to_json t =
173173+ let fields = [
174174+ ("id", Jmap_core.Id.to_json t.id);
175175+ ("name", `String t.name);
176176+ ("sortOrder", Jmap_core.Primitives.UnsignedInt.to_json t.sort_order);
177177+ ("totalEmails", Jmap_core.Primitives.UnsignedInt.to_json t.total_emails);
178178+ ("unreadEmails", Jmap_core.Primitives.UnsignedInt.to_json t.unread_emails);
179179+ ("totalThreads", Jmap_core.Primitives.UnsignedInt.to_json t.total_threads);
180180+ ("unreadThreads", Jmap_core.Primitives.UnsignedInt.to_json t.unread_threads);
181181+ ("myRights", Rights.to_json t.my_rights);
182182+ ("isSubscribed", `Bool t.is_subscribed);
183183+ ] in
184184+ let fields = match t.parent_id with
185185+ | Some pid -> ("parentId", Jmap_core.Id.to_json pid) :: fields
186186+ | None -> ("parentId", `Null) :: fields
187187+ in
188188+ let fields = match t.role with
189189+ | Some r -> ("role", `String r) :: fields
190190+ | None -> ("role", `Null) :: fields
191191+ in
192192+ `O fields
193193+end
194194+195195+(** Standard /get method (RFC 8621 Section 2.2) *)
196196+module Get = struct
197197+ type request = t Jmap_core.Standard_methods.Get.request
198198+ type response = t Jmap_core.Standard_methods.Get.response
199199+200200+ (** Constructor for request *)
201201+ let request_v = Jmap_core.Standard_methods.Get.v
202202+203203+ (** Convert request to JSON *)
204204+ let request_to_json = Jmap_core.Standard_methods.Get.request_to_json
205205+206206+ (** Parse get request from JSON *)
207207+ let request_of_json json =
208208+ Jmap_core.Standard_methods.Get.request_of_json Parser.of_json json
209209+210210+ (** Parse get response from JSON *)
211211+ let response_of_json json =
212212+ Jmap_core.Standard_methods.Get.response_of_json Parser.of_json json
213213+end
214214+215215+(** Standard /changes method (RFC 8621 Section 2.3) *)
216216+module Changes = struct
217217+ type request = Jmap_core.Standard_methods.Changes.request
218218+ type response = Jmap_core.Standard_methods.Changes.response
219219+220220+ let request_of_json json =
221221+ Jmap_core.Standard_methods.Changes.request_of_json json
222222+223223+ let response_of_json json =
224224+ Jmap_core.Standard_methods.Changes.response_of_json json
225225+end
226226+227227+(** Mailbox-specific filter for /query (RFC 8621 Section 2.5) *)
228228+module Filter = struct
229229+ type t = {
230230+ parent_id : Jmap_core.Id.t option; (** Mailbox parentId equals this value *)
231231+ name : string option; (** Name contains this string (case-insensitive) *)
232232+ role : string option; (** Role equals this value *)
233233+ has_any_role : bool option; (** Has any role assigned (true) or no role (false) *)
234234+ is_subscribed : bool option; (** isSubscribed equals this value *)
235235+ }
236236+237237+ let of_json json =
238238+ let open Jmap_core.Parser.Helpers in
239239+ let fields = expect_object json in
240240+ let parent_id = match find_field "parentId" fields with
241241+ | Some `Null -> Some None (* Explicitly filter for null parent *)
242242+ | Some v -> Some (Some (Jmap_core.Id.of_json v))
243243+ | None -> None (* Don't filter on parentId *)
244244+ in
245245+ let name = get_string_opt "name" fields in
246246+ let role = get_string_opt "role" fields in
247247+ let has_any_role = match find_field "hasAnyRole" fields with
248248+ | Some (`Bool b) -> Some b
249249+ | Some _ -> raise (Jmap_core.Error.Parse_error "hasAnyRole must be a boolean")
250250+ | None -> None
251251+ in
252252+ let is_subscribed = match find_field "isSubscribed" fields with
253253+ | Some (`Bool b) -> Some b
254254+ | Some _ -> raise (Jmap_core.Error.Parse_error "isSubscribed must be a boolean")
255255+ | None -> None
256256+ in
257257+ (* Note: parent_id has special handling - None means don't filter,
258258+ Some None means filter for null, Some (Some id) means filter for that id *)
259259+ let parent_id_simple = match parent_id with
260260+ | Some (Some id) -> Some id
261261+ | _ -> None (* We'll need to handle the "null" case specially in actual filtering *)
262262+ in
263263+ { parent_id = parent_id_simple; name; role; has_any_role; is_subscribed }
264264+265265+ (* Accessors *)
266266+ let parent_id t = t.parent_id
267267+ let name t = t.name
268268+ let role t = t.role
269269+ let has_any_role t = t.has_any_role
270270+ let is_subscribed t = t.is_subscribed
271271+272272+ (* Constructor *)
273273+ let v ?parent_id ?name ?role ?has_any_role ?is_subscribed () =
274274+ { parent_id; name; role; has_any_role; is_subscribed }
275275+276276+ (* Convert to JSON *)
277277+ let to_json t =
278278+ let fields = [] in
279279+ let fields = match t.parent_id with
280280+ | Some id -> ("parentId", Jmap_core.Id.to_json id) :: fields
281281+ | None -> fields
282282+ in
283283+ let fields = match t.name with
284284+ | Some n -> ("name", `String n) :: fields
285285+ | None -> fields
286286+ in
287287+ let fields = match t.role with
288288+ | Some r -> ("role", `String r) :: fields
289289+ | None -> fields
290290+ in
291291+ let fields = match t.has_any_role with
292292+ | Some har -> ("hasAnyRole", `Bool har) :: fields
293293+ | None -> fields
294294+ in
295295+ let fields = match t.is_subscribed with
296296+ | Some is -> ("isSubscribed", `Bool is) :: fields
297297+ | None -> fields
298298+ in
299299+ `O fields
300300+end
301301+302302+(** Standard /query method with Mailbox-specific extensions (RFC 8621 Section 2.5) *)
303303+module Query = struct
304304+ type request = {
305305+ account_id : Jmap_core.Id.t;
306306+ filter : Filter.t Jmap_core.Filter.t option;
307307+ sort : Jmap_core.Comparator.t list option;
308308+ position : Jmap_core.Primitives.Int53.t option;
309309+ anchor : Jmap_core.Id.t option;
310310+ anchor_offset : Jmap_core.Primitives.Int53.t option;
311311+ limit : Jmap_core.Primitives.UnsignedInt.t option;
312312+ calculate_total : bool option;
313313+ (* Mailbox-specific query arguments *)
314314+ sort_as_tree : bool option; (** Return results in tree order *)
315315+ filter_as_tree : bool option; (** If true, apply filter to tree roots and return descendants *)
316316+ }
317317+318318+ type response = Jmap_core.Standard_methods.Query.response
319319+320320+ (* Accessors for request *)
321321+ let account_id req = req.account_id
322322+ let filter req = req.filter
323323+ let sort req = req.sort
324324+ let position req = req.position
325325+ let anchor req = req.anchor
326326+ let anchor_offset req = req.anchor_offset
327327+ let limit req = req.limit
328328+ let calculate_total req = req.calculate_total
329329+ let sort_as_tree req = req.sort_as_tree
330330+ let filter_as_tree req = req.filter_as_tree
331331+332332+ (* Constructor for request *)
333333+ let request_v ~account_id ?filter ?sort ?position ?anchor ?anchor_offset
334334+ ?limit ?calculate_total ?sort_as_tree ?filter_as_tree () =
335335+ { account_id; filter; sort; position; anchor; anchor_offset;
336336+ limit; calculate_total; sort_as_tree; filter_as_tree }
337337+338338+ (** Parse query request from JSON.
339339+ Test files: test/data/mail/mailbox_query_request.json *)
340340+ let request_of_json json =
341341+ let open Jmap_core.Parser.Helpers in
342342+ let fields = expect_object json in
343343+ let account_id = Jmap_core.Id.of_json (require_field "accountId" fields) in
344344+ let filter = match find_field "filter" fields with
345345+ | Some v -> Some (Jmap_core.Filter.of_json Filter.of_json v)
346346+ | None -> None
347347+ in
348348+ let sort = match find_field "sort" fields with
349349+ | Some v -> Some (parse_array Jmap_core.Comparator.of_json v)
350350+ | None -> None
351351+ in
352352+ let position = match find_field "position" fields with
353353+ | Some v -> Some (Jmap_core.Primitives.Int53.of_json v)
354354+ | None -> None
355355+ in
356356+ let anchor = match find_field "anchor" fields with
357357+ | Some v -> Some (Jmap_core.Id.of_json v)
358358+ | None -> None
359359+ in
360360+ let anchor_offset = match find_field "anchorOffset" fields with
361361+ | Some v -> Some (Jmap_core.Primitives.Int53.of_json v)
362362+ | None -> None
363363+ in
364364+ let limit = match find_field "limit" fields with
365365+ | Some v -> Some (Jmap_core.Primitives.UnsignedInt.of_json v)
366366+ | None -> None
367367+ in
368368+ let calculate_total = match find_field "calculateTotal" fields with
369369+ | Some (`Bool b) -> Some b
370370+ | Some _ -> raise (Jmap_core.Error.Parse_error "calculateTotal must be a boolean")
371371+ | None -> None
372372+ in
373373+ let sort_as_tree = match find_field "sortAsTree" fields with
374374+ | Some (`Bool b) -> Some b
375375+ | Some _ -> raise (Jmap_core.Error.Parse_error "sortAsTree must be a boolean")
376376+ | None -> None
377377+ in
378378+ let filter_as_tree = match find_field "filterAsTree" fields with
379379+ | Some (`Bool b) -> Some b
380380+ | Some _ -> raise (Jmap_core.Error.Parse_error "filterAsTree must be a boolean")
381381+ | None -> None
382382+ in
383383+ { account_id; filter; sort; position; anchor; anchor_offset; limit;
384384+ calculate_total; sort_as_tree; filter_as_tree }
385385+386386+ (** Convert query request to JSON *)
387387+ let request_to_json req =
388388+ let fields = [
389389+ ("accountId", Jmap_core.Id.to_json req.account_id);
390390+ ] in
391391+ let fields = match req.filter with
392392+ | Some f -> ("filter", Jmap_core.Filter.to_json Filter.to_json f) :: fields
393393+ | None -> fields
394394+ in
395395+ let fields = match req.sort with
396396+ | Some s -> ("sort", `A (List.map Jmap_core.Comparator.to_json s)) :: fields
397397+ | None -> fields
398398+ in
399399+ let fields = match req.position with
400400+ | Some p -> ("position", Jmap_core.Primitives.Int53.to_json p) :: fields
401401+ | None -> fields
402402+ in
403403+ let fields = match req.anchor with
404404+ | Some a -> ("anchor", Jmap_core.Id.to_json a) :: fields
405405+ | None -> fields
406406+ in
407407+ let fields = match req.anchor_offset with
408408+ | Some ao -> ("anchorOffset", Jmap_core.Primitives.Int53.to_json ao) :: fields
409409+ | None -> fields
410410+ in
411411+ let fields = match req.limit with
412412+ | Some l -> ("limit", Jmap_core.Primitives.UnsignedInt.to_json l) :: fields
413413+ | None -> fields
414414+ in
415415+ let fields = match req.calculate_total with
416416+ | Some ct -> ("calculateTotal", `Bool ct) :: fields
417417+ | None -> fields
418418+ in
419419+ let fields = match req.sort_as_tree with
420420+ | Some sat -> ("sortAsTree", `Bool sat) :: fields
421421+ | None -> fields
422422+ in
423423+ let fields = match req.filter_as_tree with
424424+ | Some fat -> ("filterAsTree", `Bool fat) :: fields
425425+ | None -> fields
426426+ in
427427+ `O fields
428428+429429+ (** Parse query response from JSON.
430430+ Test files: test/data/mail/mailbox_query_response.json *)
431431+ let response_of_json json =
432432+ Jmap_core.Standard_methods.Query.response_of_json json
433433+end
434434+435435+(** Standard /queryChanges method (RFC 8621 Section 2.6) *)
436436+module QueryChanges = struct
437437+ type request = Filter.t Jmap_core.Standard_methods.QueryChanges.request
438438+ type response = Jmap_core.Standard_methods.QueryChanges.response
439439+440440+ let request_of_json json =
441441+ Jmap_core.Standard_methods.QueryChanges.request_of_json Filter.of_json json
442442+443443+ let response_of_json json =
444444+ Jmap_core.Standard_methods.QueryChanges.response_of_json json
445445+end
446446+447447+(** Standard /set method (RFC 8621 Section 2.4) *)
448448+module Set = struct
449449+ type request = t Jmap_core.Standard_methods.Set.request
450450+ type response = t Jmap_core.Standard_methods.Set.response
451451+452452+ (** Parse set request from JSON.
453453+ Test files: test/data/mail/mailbox_set_request.json *)
454454+ let request_of_json json =
455455+ Jmap_core.Standard_methods.Set.request_of_json Parser.of_json json
456456+457457+ (** Parse set response from JSON.
458458+ Test files: test/data/mail/mailbox_set_response.json *)
459459+ let response_of_json json =
460460+ Jmap_core.Standard_methods.Set.response_of_json Parser.of_json json
461461+end
462462+463463+(** Standard mailbox role values (RFC 8621 Section 2.1) *)
464464+module Role = struct
465465+ let inbox = "inbox" (* Messages delivered to this account by default *)
466466+ let archive = "archive" (* Messages the user has archived *)
467467+ let drafts = "drafts" (* Messages the user is composing *)
468468+ let sent = "sent" (* Messages the user has sent *)
469469+ let trash = "trash" (* Messages the user has deleted *)
470470+ let junk = "junk" (* Spam/junk messages *)
471471+ let important = "important" (* Messages deemed important by the user *)
472472+ let all = "all" (* All messages (virtual mailbox) *)
473473+end
+225
jmap/jmap-mail/jmap_mailbox.mli
···11+(** JMAP Mailbox Type *)
22+33+open Jmap_core
44+55+(** Mailbox access rights (RFC 8621 Section 2.1) *)
66+module Rights : sig
77+ type t = {
88+ may_read_items : bool;
99+ may_add_items : bool;
1010+ may_remove_items : bool;
1111+ may_set_seen : bool;
1212+ may_set_keywords : bool;
1313+ may_create_child : bool;
1414+ may_rename : bool;
1515+ may_delete : bool;
1616+ may_submit : bool;
1717+ }
1818+1919+ (** Accessors *)
2020+ val may_read_items : t -> bool
2121+ val may_add_items : t -> bool
2222+ val may_remove_items : t -> bool
2323+ val may_set_seen : t -> bool
2424+ val may_set_keywords : t -> bool
2525+ val may_create_child : t -> bool
2626+ val may_rename : t -> bool
2727+ val may_delete : t -> bool
2828+ val may_submit : t -> bool
2929+3030+ (** Constructor *)
3131+ val v :
3232+ may_read_items:bool ->
3333+ may_add_items:bool ->
3434+ may_remove_items:bool ->
3535+ may_set_seen:bool ->
3636+ may_set_keywords:bool ->
3737+ may_create_child:bool ->
3838+ may_rename:bool ->
3939+ may_delete:bool ->
4040+ may_submit:bool ->
4141+ t
4242+4343+ val of_json : Ezjsonm.value -> t
4444+ val to_json : t -> Ezjsonm.value
4545+end
4646+4747+(** Mailbox object type *)
4848+type t = {
4949+ id : Id.t;
5050+ name : string;
5151+ parent_id : Id.t option;
5252+ role : string option;
5353+ sort_order : Primitives.UnsignedInt.t;
5454+ total_emails : Primitives.UnsignedInt.t;
5555+ unread_emails : Primitives.UnsignedInt.t;
5656+ total_threads : Primitives.UnsignedInt.t;
5757+ unread_threads : Primitives.UnsignedInt.t;
5858+ my_rights : Rights.t;
5959+ is_subscribed : bool;
6060+}
6161+6262+(** Accessors *)
6363+val id : t -> Id.t
6464+val name : t -> string
6565+val parent_id : t -> Id.t option
6666+val role : t -> string option
6767+val sort_order : t -> Primitives.UnsignedInt.t
6868+val total_emails : t -> Primitives.UnsignedInt.t
6969+val unread_emails : t -> Primitives.UnsignedInt.t
7070+val total_threads : t -> Primitives.UnsignedInt.t
7171+val unread_threads : t -> Primitives.UnsignedInt.t
7272+val my_rights : t -> Rights.t
7373+val is_subscribed : t -> bool
7474+7575+(** Constructor *)
7676+val v :
7777+ id:Id.t ->
7878+ name:string ->
7979+ ?parent_id:Id.t ->
8080+ ?role:string ->
8181+ sort_order:Primitives.UnsignedInt.t ->
8282+ total_emails:Primitives.UnsignedInt.t ->
8383+ unread_emails:Primitives.UnsignedInt.t ->
8484+ total_threads:Primitives.UnsignedInt.t ->
8585+ unread_threads:Primitives.UnsignedInt.t ->
8686+ my_rights:Rights.t ->
8787+ is_subscribed:bool ->
8888+ unit ->
8989+ t
9090+9191+(** Standard /get method *)
9292+module Get : sig
9393+ type request = t Standard_methods.Get.request
9494+ type response = t Standard_methods.Get.response
9595+9696+ val request_v : account_id:Id.t -> ?ids:Id.t list -> ?properties:string list -> unit -> request
9797+ val request_to_json : request -> Ezjsonm.value
9898+ val request_of_json : Ezjsonm.value -> request
9999+ val response_of_json : Ezjsonm.value -> response
100100+end
101101+102102+(** Standard /changes method *)
103103+module Changes : sig
104104+ type request = Standard_methods.Changes.request
105105+ type response = Standard_methods.Changes.response
106106+107107+ val request_of_json : Ezjsonm.value -> request
108108+ val response_of_json : Ezjsonm.value -> response
109109+end
110110+111111+(** Mailbox-specific filter for /query *)
112112+module Filter : sig
113113+ type t = {
114114+ parent_id : Id.t option;
115115+ name : string option;
116116+ role : string option;
117117+ has_any_role : bool option;
118118+ is_subscribed : bool option;
119119+ }
120120+121121+ (** Accessors *)
122122+ val parent_id : t -> Id.t option
123123+ val name : t -> string option
124124+ val role : t -> string option
125125+ val has_any_role : t -> bool option
126126+ val is_subscribed : t -> bool option
127127+128128+ (** Constructor *)
129129+ val v :
130130+ ?parent_id:Id.t ->
131131+ ?name:string ->
132132+ ?role:string ->
133133+ ?has_any_role:bool ->
134134+ ?is_subscribed:bool ->
135135+ unit ->
136136+ t
137137+138138+ val to_json : t -> Ezjsonm.value
139139+ val of_json : Ezjsonm.value -> t
140140+end
141141+142142+(** Standard /query method with Mailbox-specific extensions *)
143143+module Query : sig
144144+ type request = {
145145+ account_id : Id.t;
146146+ filter : Filter.t Jmap_core.Filter.t option;
147147+ sort : Comparator.t list option;
148148+ position : Primitives.Int53.t option;
149149+ anchor : Id.t option;
150150+ anchor_offset : Primitives.Int53.t option;
151151+ limit : Primitives.UnsignedInt.t option;
152152+ calculate_total : bool option;
153153+ sort_as_tree : bool option;
154154+ filter_as_tree : bool option;
155155+ }
156156+157157+ type response = Standard_methods.Query.response
158158+159159+ (** Accessors for request *)
160160+ val account_id : request -> Id.t
161161+ val filter : request -> Filter.t Jmap_core.Filter.t option
162162+ val sort : request -> Comparator.t list option
163163+ val position : request -> Primitives.Int53.t option
164164+ val anchor : request -> Id.t option
165165+ val anchor_offset : request -> Primitives.Int53.t option
166166+ val limit : request -> Primitives.UnsignedInt.t option
167167+ val calculate_total : request -> bool option
168168+ val sort_as_tree : request -> bool option
169169+ val filter_as_tree : request -> bool option
170170+171171+ (** Constructor for request *)
172172+ val request_v :
173173+ account_id:Id.t ->
174174+ ?filter:Filter.t Jmap_core.Filter.t ->
175175+ ?sort:Comparator.t list ->
176176+ ?position:Primitives.Int53.t ->
177177+ ?anchor:Id.t ->
178178+ ?anchor_offset:Primitives.Int53.t ->
179179+ ?limit:Primitives.UnsignedInt.t ->
180180+ ?calculate_total:bool ->
181181+ ?sort_as_tree:bool ->
182182+ ?filter_as_tree:bool ->
183183+ unit ->
184184+ request
185185+186186+ val request_to_json : request -> Ezjsonm.value
187187+ val request_of_json : Ezjsonm.value -> request
188188+ val response_of_json : Ezjsonm.value -> response
189189+end
190190+191191+(** Standard /queryChanges method *)
192192+module QueryChanges : sig
193193+ type request = Filter.t Standard_methods.QueryChanges.request
194194+ type response = Standard_methods.QueryChanges.response
195195+196196+ val request_of_json : Ezjsonm.value -> request
197197+ val response_of_json : Ezjsonm.value -> response
198198+end
199199+200200+(** Standard /set method *)
201201+module Set : sig
202202+ type request = t Standard_methods.Set.request
203203+ type response = t Standard_methods.Set.response
204204+205205+ val request_of_json : Ezjsonm.value -> request
206206+ val response_of_json : Ezjsonm.value -> response
207207+end
208208+209209+(** Parser submodule *)
210210+module Parser : sig
211211+ val of_json : Ezjsonm.value -> t
212212+ val to_json : t -> Ezjsonm.value
213213+end
214214+215215+(** Standard mailbox role values (RFC 8621 Section 2.1) *)
216216+module Role : sig
217217+ val inbox : string
218218+ val archive : string
219219+ val drafts : string
220220+ val sent : string
221221+ val trash : string
222222+ val junk : string
223223+ val important : string
224224+ val all : string
225225+end
+131
jmap/jmap-mail/jmap_search_snippet.ml
···11+(** JMAP SearchSnippet Type
22+33+ A SearchSnippet contains highlighted text snippets from an Email,
44+ showing where search terms matched in the subject and body. This is
55+ typically used to show search results with context.
66+77+open Jmap_core
88+99+ SearchSnippets are generated on-demand by SearchSnippet/get and are not
1010+ stored objects (they have no state, cannot be modified, etc.).
1111+1212+ Reference: RFC 8621 Section 5 (Search Snippets)
1313+ Test files:
1414+ - test/data/mail/search_snippet_request.json
1515+ - test/data/mail/search_snippet_response.json
1616+*)
1717+1818+(** SearchSnippet object type (RFC 8621 Section 5.1)
1919+2020+ SearchSnippets are keyed by email ID and contain highlighted excerpts.
2121+ The <mark> tags indicate where the search terms matched.
2222+*)
2323+type t = {
2424+ email_id : Jmap_core.Id.t; (** Email ID this snippet is for *)
2525+ subject : string option; (** Subject with search terms highlighted using <mark> tags *)
2626+ preview : string option; (** Preview text with search terms highlighted using <mark> tags *)
2727+}
2828+2929+(** Accessors *)
3030+let email_id t = t.email_id
3131+let subject t = t.subject
3232+let preview t = t.preview
3333+3434+(** Constructor *)
3535+let v ~email_id ?subject ?preview () =
3636+ { email_id; subject; preview }
3737+3838+(** SearchSnippet/get method (RFC 8621 Section 5.2)
3939+4040+ This is the only method for SearchSnippets. It takes a filter and a list
4141+ of email IDs, and returns highlighted snippets showing where the filter
4242+ matched in each email.
4343+4444+ Unlike standard /get methods, this requires a filter to know what to highlight.
4545+*)
4646+module Get = struct
4747+ type request = {
4848+ account_id : Jmap_core.Id.t;
4949+ filter : Jmap_email.Filter.t Jmap_core.Filter.t; (** Filter to apply for highlighting *)
5050+ email_ids : Jmap_core.Id.t list; (** Email IDs to get snippets for *)
5151+ }
5252+5353+ type response = {
5454+ account_id : Jmap_core.Id.t;
5555+ list : t list; (** SearchSnippets for requested emails *)
5656+ not_found : Jmap_core.Id.t list; (** Email IDs that don't exist *)
5757+ }
5858+5959+ (** Accessors for request *)
6060+ let account_id (r : request) = r.account_id
6161+ let filter (r : request) = r.filter
6262+ let email_ids (r : request) = r.email_ids
6363+6464+ (** Constructor for request *)
6565+ let request_v ~account_id ~filter ~email_ids =
6666+ { account_id; filter; email_ids }
6767+6868+ (** Accessors for response *)
6969+ let response_account_id (r : response) = r.account_id
7070+ let list (r : response) = r.list
7171+ let not_found r = r.not_found
7272+7373+ (** Constructor for response *)
7474+ let response_v ~account_id ~list ~not_found =
7575+ { account_id; list; not_found }
7676+7777+ (** Parse get request from JSON.
7878+ Test files: test/data/mail/search_snippet_request.json
7979+8080+ Expected structure:
8181+ {
8282+ "accountId": "u123456",
8383+ "filter": {
8484+ "text": "project milestone"
8585+ },
8686+ "emailIds": ["e001", "e005", "e008"]
8787+ }
8888+ *)
8989+ let request_of_json _json =
9090+ raise (Jmap_core.Error.Parse_error "SearchSnippet.Get.request_of_json not yet implemented")
9191+9292+ (** Parse get response from JSON.
9393+ Test files: test/data/mail/search_snippet_response.json
9494+9595+ Expected structure:
9696+ {
9797+ "accountId": "u123456",
9898+ "list": [
9999+ {
100100+ "emailId": "e001",
101101+ "subject": "<mark>Project</mark> Update Q4 2025",
102102+ "preview": "...made significant progress on all major <mark>milestones</mark>..."
103103+ }
104104+ ],
105105+ "notFound": []
106106+ }
107107+ *)
108108+ let response_of_json _json =
109109+ raise (Jmap_core.Error.Parse_error "SearchSnippet.Get.response_of_json not yet implemented")
110110+end
111111+112112+(** Parser submodule *)
113113+module Parser = struct
114114+ (** Parse SearchSnippet from JSON.
115115+ Test files: test/data/mail/search_snippet_response.json (list field)
116116+117117+ Expected structure:
118118+ {
119119+ "emailId": "e001",
120120+ "subject": "<mark>Project</mark> Update Q4 2025",
121121+ "preview": "...made significant progress on all major <mark>milestones</mark> and are on track for delivery..."
122122+ }
123123+ *)
124124+ let of_json _json =
125125+ (* TODO: Implement JSON parsing *)
126126+ raise (Jmap_core.Error.Parse_error "SearchSnippet.Parser.of_json not yet implemented")
127127+128128+ let to_json _t =
129129+ (* TODO: Implement JSON serialization *)
130130+ raise (Jmap_core.Error.Parse_error "SearchSnippet.Parser.to_json not yet implemented")
131131+end
+66
jmap/jmap-mail/jmap_search_snippet.mli
···11+(** JMAP SearchSnippet Type *)
22+33+open Jmap_core
44+55+(** SearchSnippet object type (RFC 8621 Section 5.1) *)
66+type t = {
77+ email_id : Id.t;
88+ subject : string option;
99+ preview : string option;
1010+}
1111+1212+(** Accessors *)
1313+val email_id : t -> Id.t
1414+val subject : t -> string option
1515+val preview : t -> string option
1616+1717+(** Constructor *)
1818+val v : email_id:Id.t -> ?subject:string -> ?preview:string -> unit -> t
1919+2020+(** SearchSnippet/get method *)
2121+module Get : sig
2222+ type request = {
2323+ account_id : Id.t;
2424+ filter : Jmap_email.Filter.t Filter.t;
2525+ email_ids : Id.t list;
2626+ }
2727+2828+ type response = {
2929+ account_id : Id.t;
3030+ list : t list;
3131+ not_found : Id.t list;
3232+ }
3333+3434+ (** Accessors for request *)
3535+ val account_id : request -> Id.t
3636+ val filter : request -> Jmap_email.Filter.t Filter.t
3737+ val email_ids : request -> Id.t list
3838+3939+ (** Constructor for request *)
4040+ val request_v :
4141+ account_id:Id.t ->
4242+ filter:Jmap_email.Filter.t Filter.t ->
4343+ email_ids:Id.t list ->
4444+ request
4545+4646+ (** Accessors for response *)
4747+ val response_account_id : response -> Id.t
4848+ val list : response -> t list
4949+ val not_found : response -> Id.t list
5050+5151+ (** Constructor for response *)
5252+ val response_v :
5353+ account_id:Id.t ->
5454+ list:t list ->
5555+ not_found:Id.t list ->
5656+ response
5757+5858+ val request_of_json : Ezjsonm.value -> request
5959+ val response_of_json : Ezjsonm.value -> response
6060+end
6161+6262+(** Parser submodule *)
6363+module Parser : sig
6464+ val of_json : Ezjsonm.value -> t
6565+ val to_json : t -> Ezjsonm.value
6666+end
+93
jmap/jmap-mail/jmap_thread.ml
···11+(** JMAP Thread Type
22+33+ A Thread represents a conversation or message thread. It is simply a
44+ list of Email ids that are related to each other.
55+66+open Jmap_core
77+88+ Threads are purely server-managed objects - they are calculated by the
99+ server based on message headers (In-Reply-To, References, Subject, etc.)
1010+ and cannot be created, updated, or destroyed by the client.
1111+1212+ Reference: RFC 8621 Section 3 (Threads)
1313+ Test files:
1414+ - test/data/mail/thread_get_request.json
1515+ - test/data/mail/thread_get_response.json
1616+*)
1717+1818+(** Thread object type *)
1919+type t = {
2020+ id : Jmap_core.Id.t; (** Immutable server-assigned thread id *)
2121+ email_ids : Jmap_core.Id.t list; (** List of email ids in this thread, sorted by date (oldest first) *)
2222+}
2323+2424+(** Accessors *)
2525+let id t = t.id
2626+let email_ids t = t.email_ids
2727+2828+(** Constructor *)
2929+let v ~id ~email_ids = { id; email_ids }
3030+3131+(** Standard /get method (RFC 8621 Section 3.2)
3232+3333+ Threads only support the /get method. They do not support:
3434+ - /changes (threads change too frequently)
3535+ - /set (threads are server-managed, not client-modifiable)
3636+ - /query (use Email/query with collapseThreads instead)
3737+ - /queryChanges
3838+*)
3939+module Get = struct
4040+ type request = t Jmap_core.Standard_methods.Get.request
4141+ type response = t Jmap_core.Standard_methods.Get.response
4242+4343+ (** Parse get request from JSON.
4444+ Test files: test/data/mail/thread_get_request.json
4545+4646+ Expected structure:
4747+ {
4848+ "accountId": "u123456",
4949+ "ids": ["t001", "t002", "t003"]
5050+ }
5151+ *)
5252+ let request_of_json _json =
5353+ raise (Jmap_core.Error.Parse_error "Thread.Get.request_of_json not yet implemented")
5454+5555+ (** Parse get response from JSON.
5656+ Test files: test/data/mail/thread_get_response.json
5757+5858+ Expected structure:
5959+ {
6060+ "accountId": "u123456",
6161+ "state": "t42:100",
6262+ "list": [
6363+ {
6464+ "id": "t001",
6565+ "emailIds": ["e001", "e005", "e008"]
6666+ }
6767+ ],
6868+ "notFound": ["t003"]
6969+ }
7070+ *)
7171+ let response_of_json _json =
7272+ raise (Jmap_core.Error.Parse_error "Thread.Get.response_of_json not yet implemented")
7373+end
7474+7575+(** Parser submodule *)
7676+module Parser = struct
7777+ (** Parse Thread from JSON.
7878+ Test files: test/data/mail/thread_get_response.json (list field)
7979+8080+ Expected structure:
8181+ {
8282+ "id": "t001",
8383+ "emailIds": ["e001", "e005", "e008"]
8484+ }
8585+ *)
8686+ let of_json _json =
8787+ (* TODO: Implement JSON parsing *)
8888+ raise (Jmap_core.Error.Parse_error "Thread.Parser.of_json not yet implemented")
8989+9090+ let to_json _t =
9191+ (* TODO: Implement JSON serialization *)
9292+ raise (Jmap_core.Error.Parse_error "Thread.Parser.to_json not yet implemented")
9393+end
+31
jmap/jmap-mail/jmap_thread.mli
···11+(** JMAP Thread Type *)
22+33+open Jmap_core
44+55+(** Thread object type *)
66+type t = {
77+ id : Id.t;
88+ email_ids : Id.t list;
99+}
1010+1111+(** Accessors *)
1212+val id : t -> Id.t
1313+val email_ids : t -> Id.t list
1414+1515+(** Constructor *)
1616+val v : id:Id.t -> email_ids:Id.t list -> t
1717+1818+(** Standard /get method *)
1919+module Get : sig
2020+ type request = t Standard_methods.Get.request
2121+ type response = t Standard_methods.Get.response
2222+2323+ val request_of_json : Ezjsonm.value -> request
2424+ val response_of_json : Ezjsonm.value -> response
2525+end
2626+2727+(** Parser submodule *)
2828+module Parser : sig
2929+ val of_json : Ezjsonm.value -> t
3030+ val to_json : t -> Ezjsonm.value
3131+end
+148
jmap/jmap-mail/jmap_vacation_response.ml
···11+(** JMAP VacationResponse Type
22+33+ A VacationResponse is a singleton object that represents the vacation/
44+ out-of-office auto-responder configuration for an account.
55+66+open Jmap_core
77+88+ Reference: RFC 8621 Section 8 (Vacation Response)
99+ Test files:
1010+ - test/data/mail/vacation_response_get_request.json
1111+ - test/data/mail/vacation_response_get_response.json
1212+*)
1313+1414+(** VacationResponse object type (RFC 8621 Section 8.1)
1515+1616+ VacationResponse is a singleton - there is exactly one per account,
1717+ with id "singleton". It cannot be created or destroyed, only updated.
1818+*)
1919+type t = {
2020+ id : Jmap_core.Id.t; (** Always "singleton" *)
2121+ is_enabled : bool; (** Is vacation response currently active? *)
2222+ from_date : Jmap_core.Primitives.UTCDate.t option; (** Start date (null = active now) *)
2323+ to_date : Jmap_core.Primitives.UTCDate.t option; (** End date (null = no end) *)
2424+ subject : string option; (** Subject for auto-reply message *)
2525+ text_body : string option; (** Plain text auto-reply body *)
2626+ html_body : string option; (** HTML auto-reply body *)
2727+}
2828+2929+(** Accessors *)
3030+let id t = t.id
3131+let is_enabled t = t.is_enabled
3232+let from_date t = t.from_date
3333+let to_date t = t.to_date
3434+let subject t = t.subject
3535+let text_body t = t.text_body
3636+let html_body t = t.html_body
3737+3838+(** Constructor *)
3939+let v ~id ~is_enabled ?from_date ?to_date ?subject ?text_body ?html_body () =
4040+ { id; is_enabled; from_date; to_date; subject; text_body; html_body }
4141+4242+(** Standard /get method (RFC 8621 Section 8.2)
4343+4444+ Since VacationResponse is a singleton, the typical usage is:
4545+ {
4646+ "accountId": "u123456",
4747+ "ids": null // fetches the singleton
4848+ }
4949+*)
5050+module Get = struct
5151+ type request = t Jmap_core.Standard_methods.Get.request
5252+ type response = t Jmap_core.Standard_methods.Get.response
5353+5454+ (** Parse get request from JSON.
5555+ Test files: test/data/mail/vacation_response_get_request.json
5656+5757+ Expected structure:
5858+ {
5959+ "accountId": "u123456",
6060+ "ids": null
6161+ }
6262+ *)
6363+ let request_of_json _json =
6464+ raise (Jmap_core.Error.Parse_error "VacationResponse.Get.request_of_json not yet implemented")
6565+6666+ (** Parse get response from JSON.
6767+ Test files: test/data/mail/vacation_response_get_response.json
6868+6969+ Expected structure:
7070+ {
7171+ "accountId": "u123456",
7272+ "list": [
7373+ {
7474+ "id": "singleton",
7575+ "isEnabled": true,
7676+ "fromDate": "2025-12-20T00:00:00Z",
7777+ "toDate": "2026-01-05T23:59:59Z",
7878+ "subject": "Out of Office",
7979+ "textBody": "Thank you for your email...",
8080+ "htmlBody": "<html><body>...</body></html>"
8181+ }
8282+ ],
8383+ "notFound": []
8484+ }
8585+ *)
8686+ let response_of_json _json =
8787+ raise (Jmap_core.Error.Parse_error "VacationResponse.Get.response_of_json not yet implemented")
8888+end
8989+9090+(** Standard /set method (RFC 8621 Section 8.3)
9191+9292+ VacationResponse only supports update operations. The create and destroy
9393+ operations will return errors:
9494+ - create: returns "singleton" error (cannot create singleton)
9595+ - destroy: returns "singleton" error (cannot destroy singleton)
9696+9797+ Typical usage:
9898+ {
9999+ "update": {
100100+ "singleton": {
101101+ "isEnabled": true,
102102+ "fromDate": "2025-12-20T00:00:00Z",
103103+ "toDate": "2026-01-05T23:59:59Z",
104104+ "subject": "Out of Office",
105105+ "textBody": "...",
106106+ "htmlBody": "..."
107107+ }
108108+ }
109109+ }
110110+*)
111111+module Set = struct
112112+ type request = t Jmap_core.Standard_methods.Set.request
113113+ type response = t Jmap_core.Standard_methods.Set.response
114114+115115+ let request_of_json _json =
116116+ raise (Jmap_core.Error.Parse_error "VacationResponse.Set.request_of_json not yet implemented")
117117+118118+ let response_of_json _json =
119119+ raise (Jmap_core.Error.Parse_error "VacationResponse.Set.response_of_json not yet implemented")
120120+end
121121+122122+(** Parser submodule *)
123123+module Parser = struct
124124+ (** Parse VacationResponse from JSON.
125125+ Test files: test/data/mail/vacation_response_get_response.json (list field)
126126+127127+ Expected structure:
128128+ {
129129+ "id": "singleton",
130130+ "isEnabled": true,
131131+ "fromDate": "2025-12-20T00:00:00Z",
132132+ "toDate": "2026-01-05T23:59:59Z",
133133+ "subject": "Out of Office",
134134+ "textBody": "Thank you for your email. I am currently out of the office...",
135135+ "htmlBody": "<html><body><p>Thank you for your email.</p>...</body></html>"
136136+ }
137137+ *)
138138+ let of_json _json =
139139+ (* TODO: Implement JSON parsing *)
140140+ raise (Jmap_core.Error.Parse_error "VacationResponse.Parser.of_json not yet implemented")
141141+142142+ let to_json _t =
143143+ (* TODO: Implement JSON serialization *)
144144+ raise (Jmap_core.Error.Parse_error "VacationResponse.Parser.to_json not yet implemented")
145145+end
146146+147147+(** Singleton ID constant *)
148148+let singleton_id = "singleton"
+62
jmap/jmap-mail/jmap_vacation_response.mli
···11+(** JMAP VacationResponse Type *)
22+33+open Jmap_core
44+55+(** VacationResponse object type (RFC 8621 Section 8.1) *)
66+type t = {
77+ id : Id.t;
88+ is_enabled : bool;
99+ from_date : Primitives.UTCDate.t option;
1010+ to_date : Primitives.UTCDate.t option;
1111+ subject : string option;
1212+ text_body : string option;
1313+ html_body : string option;
1414+}
1515+1616+(** Accessors *)
1717+val id : t -> Id.t
1818+val is_enabled : t -> bool
1919+val from_date : t -> Primitives.UTCDate.t option
2020+val to_date : t -> Primitives.UTCDate.t option
2121+val subject : t -> string option
2222+val text_body : t -> string option
2323+val html_body : t -> string option
2424+2525+(** Constructor *)
2626+val v :
2727+ id:Id.t ->
2828+ is_enabled:bool ->
2929+ ?from_date:Primitives.UTCDate.t ->
3030+ ?to_date:Primitives.UTCDate.t ->
3131+ ?subject:string ->
3232+ ?text_body:string ->
3333+ ?html_body:string ->
3434+ unit ->
3535+ t
3636+3737+(** Standard /get method *)
3838+module Get : sig
3939+ type request = t Standard_methods.Get.request
4040+ type response = t Standard_methods.Get.response
4141+4242+ val request_of_json : Ezjsonm.value -> request
4343+ val response_of_json : Ezjsonm.value -> response
4444+end
4545+4646+(** Standard /set method *)
4747+module Set : sig
4848+ type request = t Standard_methods.Set.request
4949+ type response = t Standard_methods.Set.response
5050+5151+ val request_of_json : Ezjsonm.value -> request
5252+ val response_of_json : Ezjsonm.value -> response
5353+end
5454+5555+(** Parser submodule *)
5656+module Parser : sig
5757+ val of_json : Ezjsonm.value -> t
5858+ val to_json : t -> Ezjsonm.value
5959+end
6060+6161+(** Singleton ID constant *)
6262+val singleton_id : string
···11+{
22+ "methodResponses": [
33+ [
44+ "VacationResponse/get",
55+ {
66+ "accountId": "u123456",
77+ "list": [
88+ {
99+ "id": "singleton",
1010+ "isEnabled": true,
1111+ "fromDate": "2025-12-20T00:00:00Z",
1212+ "toDate": "2026-01-05T23:59:59Z",
1313+ "subject": "Out of Office",
1414+ "textBody": "Thank you for your email. I am currently out of the office on vacation and will return on January 6, 2026. I will have limited access to email during this time.\n\nFor urgent matters, please contact support@example.com.\n\nBest regards,\nAlice Jones",
1515+ "htmlBody": "<html><body><p>Thank you for your email.</p><p>I am currently out of the office on vacation and will return on <strong>January 6, 2026</strong>. I will have limited access to email during this time.</p><p>For urgent matters, please contact <a href=\"mailto:support@example.com\">support@example.com</a>.</p><p>Best regards,<br>Alice Jones</p></body></html>"
1616+ }
1717+ ],
1818+ "notFound": []
1919+ },
2020+ "c1"
2121+ ]
2222+ ],
2323+ "sessionState": "cyrus-0"
2424+}
+50
jmap/test/data/validate_all.sh
···11+#!/bin/bash
22+# Validate all JMAP test JSON files
33+44+set -e
55+66+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
77+CORE_DIR="$SCRIPT_DIR/core"
88+MAIL_DIR="$SCRIPT_DIR/mail"
99+1010+echo "======================================"
1111+echo "JMAP Test Data Validation"
1212+echo "======================================"
1313+echo ""
1414+1515+# Count files
1616+CORE_COUNT=$(find "$CORE_DIR" -name "*.json" | wc -l)
1717+MAIL_COUNT=$(find "$MAIL_DIR" -name "*.json" | wc -l)
1818+TOTAL_COUNT=$((CORE_COUNT + MAIL_COUNT))
1919+2020+echo "Files to validate:"
2121+echo " Core protocol: $CORE_COUNT files"
2222+echo " Mail protocol: $MAIL_COUNT files"
2323+echo " Total: $TOTAL_COUNT files"
2424+echo ""
2525+2626+# Validate JSON syntax
2727+echo "Validating JSON syntax..."
2828+ERRORS=0
2929+3030+for file in "$CORE_DIR"/*.json "$MAIL_DIR"/*.json; do
3131+ if [ -f "$file" ]; then
3232+ filename=$(basename "$file")
3333+ if python3 -m json.tool "$file" > /dev/null 2>&1; then
3434+ echo " ✓ $filename"
3535+ else
3636+ echo " ✗ $filename - INVALID JSON"
3737+ ERRORS=$((ERRORS + 1))
3838+ fi
3939+ fi
4040+done
4141+4242+echo ""
4343+echo "======================================"
4444+if [ $ERRORS -eq 0 ]; then
4545+ echo "✓ SUCCESS: All $TOTAL_COUNT files are valid!"
4646+ exit 0
4747+else
4848+ echo "✗ FAILED: $ERRORS file(s) with errors"
4949+ exit 1
5050+fi