+20
-1
.gitignore
+20
-1
.gitignore
···
1
-
_build
1
+
# OCaml build artifacts
2
+
_build/
3
+
*.install
4
+
*.merlin
5
+
6
+
# Third-party sources (fetch locally with opam source)
7
+
third_party/
8
+
9
+
# Editor and OS files
10
+
.DS_Store
11
+
*.swp
12
+
*~
13
+
.vscode/
14
+
.idea/
15
+
16
+
# Opam local switch
17
+
_opam/
18
+
19
+
# Environment and secrets
2
20
.env
3
21
.api-key
22
+
.api-key-rw
4
23
.api-url
+1
.ocamlformat
+1
.ocamlformat
···
1
+
version=0.28.1
+53
.tangled/workflows/build.yml
+53
.tangled/workflows/build.yml
···
1
+
when:
2
+
- event: ["push", "pull_request"]
3
+
branch: ["main"]
4
+
5
+
engine: nixery
6
+
7
+
dependencies:
8
+
nixpkgs:
9
+
- shell
10
+
- stdenv
11
+
- findutils
12
+
- binutils
13
+
- libunwind
14
+
- ncurses
15
+
- opam
16
+
- git
17
+
- gawk
18
+
- gnupatch
19
+
- gnum4
20
+
- gnumake
21
+
- gnutar
22
+
- gnused
23
+
- gnugrep
24
+
- diffutils
25
+
- gzip
26
+
- bzip2
27
+
- gcc
28
+
- ocaml
29
+
- pkg-config
30
+
31
+
steps:
32
+
- name: opam
33
+
command: |
34
+
opam init --disable-sandboxing -a -y
35
+
- name: repo
36
+
command: |
37
+
opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git
38
+
- name: switch
39
+
command: |
40
+
opam install . --confirm-level=unsafe-yes --deps-only
41
+
- name: build
42
+
command: |
43
+
opam exec -- dune build -p jmap
44
+
- name: switch-test
45
+
command: |
46
+
opam install . --confirm-level=unsafe-yes --deps-only --with-test
47
+
- name: test
48
+
command: |
49
+
opam exec -- dune runtest --verbose
50
+
- name: doc
51
+
command: |
52
+
opam install -y odoc
53
+
opam exec -- dune build @doc
+15
LICENSE.md
+15
LICENSE.md
···
1
+
ISC License
2
+
3
+
Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
4
+
5
+
Permission to use, copy, modify, and distribute this software for any
6
+
purpose with or without fee is hereby granted, provided that the above
7
+
copyright notice and this permission notice appear in all copies.
8
+
9
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+54
README.md
+54
README.md
···
1
+
# ocaml-jmap - JMAP Protocol Implementation for OCaml
2
+
3
+
A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail).
4
+
5
+
## Packages
6
+
7
+
- **jmap** - Core JMAP protocol types and serialization
8
+
- **jmap-eio** - JMAP client using Eio for async I/O
9
+
- **jmap-brr** - JMAP client for browsers using js_of_ocaml
10
+
11
+
## Key Features
12
+
13
+
- Full RFC 8620 (JMAP Core) support: sessions, accounts, method calls, and error handling
14
+
- Full RFC 8621 (JMAP Mail) support: mailboxes, emails, threads, identities, and submissions
15
+
- Type-safe API with comprehensive type definitions
16
+
- Multiple backends: Eio for native async, Brr for browser-based clients
17
+
- JSON serialization via jsont
18
+
19
+
## Usage
20
+
21
+
```ocaml
22
+
(* Query emails from a mailbox *)
23
+
open Jmap
24
+
25
+
let query_emails ~client ~account_id ~mailbox_id =
26
+
let filter = Email.Query.Filter.(in_mailbox mailbox_id) in
27
+
let query = Email.Query.make ~account_id ~filter () in
28
+
Client.call client query
29
+
```
30
+
31
+
## Installation
32
+
33
+
```
34
+
opam install jmap jmap-eio
35
+
```
36
+
37
+
For browser-based applications:
38
+
39
+
```
40
+
opam install jmap jmap-brr
41
+
```
42
+
43
+
## Documentation
44
+
45
+
API documentation is available via:
46
+
47
+
```
48
+
opam install jmap
49
+
odig doc jmap
50
+
```
51
+
52
+
## License
53
+
54
+
ISC
+6
-4
bin/dune
+6
-4
bin/dune
···
1
1
(executable
2
2
(name jmap)
3
3
(public_name jmap)
4
-
(package jmap-eio)
4
+
(package jmap)
5
+
(optional)
5
6
(modules jmap)
6
-
(libraries jmap-eio eio_main))
7
+
(libraries jmap.eio eio_main))
7
8
8
9
(executable
9
10
(name jmapq)
10
11
(public_name jmapq)
11
-
(package jmap-eio)
12
+
(package jmap)
13
+
(optional)
12
14
(modules jmapq)
13
-
(libraries jmap-eio eio_main re jsont.bytesrw))
15
+
(libraries jmap.eio eio_main re jsont.bytesrw))
+233
-5
bin/jmapq.ml
+233
-5
bin/jmapq.ml
···
308
308
let doc = "Email IDs to mark as processed" in
309
309
Arg.(non_empty & pos_all string [] & info [] ~docv:"EMAIL_ID" ~doc)
310
310
in
311
-
let run cfg email_id_strs =
311
+
let verbose_term =
312
+
let doc = "Show the raw JMAP server response" in
313
+
Arg.(value & flag & info ["v"; "verbose"] ~doc)
314
+
in
315
+
let run cfg verbose email_id_strs =
312
316
Eio_main.run @@ fun env ->
313
317
Eio.Switch.run @@ fun sw ->
314
318
let client = Jmap_eio.Cli.create_client ~sw env cfg in
···
318
322
Jmap_eio.Cli.debug cfg "Marking %d email(s) with '%s' keyword"
319
323
(List.length email_ids) zulip_processed_keyword;
320
324
321
-
(* Build patch to add the zulip-processed keyword *)
325
+
(* Build patch to add the zulip-processed keyword and mark as read *)
322
326
let patch =
323
327
let open Jmap_eio.Chain in
324
-
json_obj [("keywords/" ^ zulip_processed_keyword, json_bool true)]
328
+
json_obj [
329
+
("keywords/" ^ zulip_processed_keyword, json_bool true);
330
+
("keywords/$seen", json_bool true);
331
+
]
325
332
in
326
333
327
334
(* Build updates list: each email ID gets the same patch *)
···
341
348
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
342
349
exit 1
343
350
| Ok response ->
351
+
(* Print raw response if verbose *)
352
+
if verbose then begin
353
+
Fmt.pr "@[<v>%a:@," Fmt.(styled `Bold string) "Server Response";
354
+
(match Jsont_bytesrw.encode_string' ~format:Jsont.Indent
355
+
Jmap.Proto.Response.jsont response with
356
+
| Ok json_str -> Fmt.pr "%s@,@]@." json_str
357
+
| Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e))
358
+
end;
344
359
(* Check for JMAP method-level errors first *)
345
360
let call_id = Jmap_eio.Chain.call_id set_h in
346
361
(match Jmap.Proto.Response.find_response call_id response with
···
370
385
|> List.map (fun (id, _) -> Jmap.Proto.Id.to_string id)
371
386
in
372
387
if List.length updated_ids > 0 then begin
373
-
Fmt.pr "@[<v>%a %d email(s) with '%s':@,"
388
+
Fmt.pr "@[<v>%a %d email(s) as read with '%s':@,"
374
389
Fmt.(styled `Green string) "Marked"
375
390
(List.length updated_ids)
376
391
zulip_processed_keyword;
···
411
426
`Pre " jmapq zulip-timeout StrrDTS_WEa3 StrsGZ7P8Dpc StrsGuCSXJ3Z";
412
427
] in
413
428
let info = Cmd.info "zulip-timeout" ~doc ~man in
414
-
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ email_ids_term)
429
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ verbose_term $ email_ids_term)
430
+
431
+
(** {1 Zulip View Command} *)
432
+
433
+
let zulip_view_cmd =
434
+
let json_term =
435
+
let doc = "Output as JSON" in
436
+
Arg.(value & flag & info ["json"] ~doc)
437
+
in
438
+
let limit_term =
439
+
let doc = "Maximum number of messages to fetch (default: all)" in
440
+
Arg.(value & opt (some int) None & info ["limit"; "n"] ~docv:"N" ~doc)
441
+
in
442
+
let verbose_term =
443
+
let doc = "Show the raw JMAP request and response" in
444
+
Arg.(value & flag & info ["v"; "verbose"] ~doc)
445
+
in
446
+
let run cfg json_output limit verbose =
447
+
Eio_main.run @@ fun env ->
448
+
Eio.Switch.run @@ fun sw ->
449
+
let client = Jmap_eio.Cli.create_client ~sw env cfg in
450
+
let account_id = Jmap_eio.Cli.get_account_id cfg client in
451
+
452
+
Jmap_eio.Cli.debug cfg "Searching for Zulip emails marked as processed";
453
+
454
+
(* Build filter for emails from noreply@zulip.com with zulip-processed keyword *)
455
+
let cond : Jmap.Proto.Email.Filter_condition.t = {
456
+
in_mailbox = None; in_mailbox_other_than = None;
457
+
before = None; after = None;
458
+
min_size = None; max_size = None;
459
+
all_in_thread_have_keyword = None;
460
+
some_in_thread_have_keyword = None;
461
+
none_in_thread_have_keyword = None;
462
+
has_keyword = Some zulip_processed_keyword;
463
+
not_keyword = None;
464
+
has_attachment = None;
465
+
text = None;
466
+
from = Some "noreply@zulip.com";
467
+
to_ = None;
468
+
cc = None; bcc = None; subject = None;
469
+
body = None; header = None;
470
+
} in
471
+
let filter = Jmap.Proto.Filter.Condition cond in
472
+
let sort = [Jmap.Proto.Filter.comparator ~is_ascending:false "receivedAt"] in
473
+
474
+
(* Query for processed Zulip emails *)
475
+
let query_limit = match limit with
476
+
| Some n -> Int64.of_int n
477
+
| None -> Int64.of_int 10000
478
+
in
479
+
let query_inv = Jmap_eio.Client.Build.email_query
480
+
~call_id:"q1"
481
+
~account_id
482
+
~filter
483
+
~sort
484
+
~limit:query_limit
485
+
()
486
+
in
487
+
488
+
let req = Jmap_eio.Client.Build.(
489
+
make_request
490
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
491
+
[query_inv]
492
+
) in
493
+
494
+
(* Print request if verbose *)
495
+
if verbose then begin
496
+
Fmt.pr "@[<v>%a:@," Fmt.(styled `Bold string) "Request";
497
+
(match Jsont_bytesrw.encode_string' ~format:Jsont.Indent
498
+
Jmap.Proto.Request.jsont req with
499
+
| Ok json_str -> Fmt.pr "%s@,@]@." json_str
500
+
| Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e))
501
+
end;
502
+
503
+
match Jmap_eio.Client.request client req with
504
+
| Error e ->
505
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
506
+
exit 1
507
+
| Ok response ->
508
+
(* Print response if verbose *)
509
+
if verbose then begin
510
+
Fmt.pr "@[<v>%a:@," Fmt.(styled `Bold string) "Response";
511
+
(match Jsont_bytesrw.encode_string' ~format:Jsont.Indent
512
+
Jmap.Proto.Response.jsont response with
513
+
| Ok json_str -> Fmt.pr "%s@,@]@." json_str
514
+
| Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e))
515
+
end;
516
+
match Jmap_eio.Client.Parse.parse_email_query ~call_id:"q1" response with
517
+
| Error e ->
518
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
519
+
exit 1
520
+
| Ok query_result ->
521
+
let email_ids = query_result.ids in
522
+
Jmap_eio.Cli.debug cfg "Found %d processed Zulip email IDs" (List.length email_ids);
523
+
524
+
if List.length email_ids = 0 then (
525
+
if json_output then
526
+
Fmt.pr "[]@."
527
+
else
528
+
Fmt.pr "No Zulip emails marked as processed.@."
529
+
) else (
530
+
(* Fetch email details *)
531
+
let get_inv = Jmap_eio.Client.Build.email_get
532
+
~call_id:"g1"
533
+
~account_id
534
+
~ids:email_ids
535
+
~properties:["id"; "blobId"; "threadId"; "mailboxIds"; "keywords";
536
+
"size"; "receivedAt"; "subject"; "from"]
537
+
()
538
+
in
539
+
let req2 = Jmap_eio.Client.Build.(
540
+
make_request
541
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
542
+
[get_inv]
543
+
) in
544
+
545
+
match Jmap_eio.Client.request client req2 with
546
+
| Error e ->
547
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
548
+
exit 1
549
+
| Ok response2 ->
550
+
match Jmap_eio.Client.Parse.parse_email_get ~call_id:"g1" response2 with
551
+
| Error e ->
552
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
553
+
exit 1
554
+
| Ok get_result ->
555
+
(* Parse Zulip subjects and filter successful parses *)
556
+
let zulip_messages =
557
+
get_result.list
558
+
|> List.filter_map Zulip_message.of_email
559
+
in
560
+
561
+
Jmap_eio.Cli.debug cfg "Parsed %d Zulip messages from %d emails"
562
+
(List.length zulip_messages)
563
+
(List.length get_result.list);
564
+
565
+
if json_output then (
566
+
(* Output as JSON *)
567
+
match Jsont_bytesrw.encode_string' ~format:Jsont.Indent Zulip_message.list_jsont zulip_messages with
568
+
| Ok json_str -> Fmt.pr "%s@." json_str
569
+
| Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e)
570
+
) else (
571
+
(* Human-readable output *)
572
+
Fmt.pr "@[<v>%a (%d messages)@,@,"
573
+
Fmt.(styled `Bold string) "Processed Zulip Notifications"
574
+
(List.length zulip_messages);
575
+
576
+
(* Group by server, then by channel *)
577
+
let by_server = Hashtbl.create 8 in
578
+
List.iter (fun (msg : Zulip_message.t) ->
579
+
let existing = try Hashtbl.find by_server msg.server with Not_found -> [] in
580
+
Hashtbl.replace by_server msg.server (msg :: existing)
581
+
) zulip_messages;
582
+
583
+
Hashtbl.iter (fun server msgs ->
584
+
Fmt.pr "%a [%s]@,"
585
+
Fmt.(styled `Bold string) "Server:"
586
+
server;
587
+
588
+
(* Group by channel within server *)
589
+
let by_channel = Hashtbl.create 8 in
590
+
List.iter (fun (msg : Zulip_message.t) ->
591
+
let existing = try Hashtbl.find by_channel msg.channel with Not_found -> [] in
592
+
Hashtbl.replace by_channel msg.channel (msg :: existing)
593
+
) msgs;
594
+
595
+
Hashtbl.iter (fun channel channel_msgs ->
596
+
Fmt.pr " %a #%s (%d)@,"
597
+
Fmt.(styled `Cyan string) "Channel:"
598
+
channel
599
+
(List.length channel_msgs);
600
+
601
+
(* Sort by date descending *)
602
+
let sorted = List.sort (fun a b ->
603
+
Ptime.compare b.Zulip_message.date a.Zulip_message.date
604
+
) channel_msgs in
605
+
606
+
List.iter (fun (msg : Zulip_message.t) ->
607
+
let read_marker = if msg.is_read then " " else "*" in
608
+
let labels_str = match msg.labels with
609
+
| [] -> ""
610
+
| ls -> " [" ^ String.concat ", " ls ^ "]"
611
+
in
612
+
Fmt.pr " %s %s %a %s%s@,"
613
+
read_marker
614
+
(ptime_to_string msg.date)
615
+
Fmt.(styled `Yellow string) (truncate_string 40 msg.topic)
616
+
(truncate_string 12 msg.id)
617
+
labels_str
618
+
) sorted;
619
+
Fmt.pr "@,"
620
+
) by_channel
621
+
) by_server;
622
+
623
+
Fmt.pr "@]@."
624
+
)
625
+
)
626
+
in
627
+
let doc = "List Zulip emails that have been marked as processed" in
628
+
let man = [
629
+
`S Manpage.s_description;
630
+
`P (Printf.sprintf "Lists all Zulip notification emails that have the '%s' keyword."
631
+
zulip_processed_keyword);
632
+
`S Manpage.s_examples;
633
+
`P "List all processed Zulip notifications:";
634
+
`Pre " jmapq zulip-view";
635
+
`P "Output as JSON:";
636
+
`Pre " jmapq zulip-view --json";
637
+
`P "Limit to 50 most recent:";
638
+
`Pre " jmapq zulip-view -n 50";
639
+
] in
640
+
let info = Cmd.info "zulip-view" ~doc ~man in
641
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ json_term $ limit_term $ verbose_term)
415
642
416
643
(** {1 Main Command Group} *)
417
644
···
427
654
Cmd.group info [
428
655
zulip_list_cmd;
429
656
zulip_timeout_cmd;
657
+
zulip_view_cmd;
430
658
]
431
659
432
660
let () =
+11
-27
dune-project
+11
-27
dune-project
···
6
6
7
7
(generate_opam_files true)
8
8
9
-
(source
10
-
(github avsm/ocaml-jmap))
9
+
(license ISC)
11
10
12
11
(authors "Anil Madhavapeddy <anil@recoil.org>")
13
12
14
13
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
15
14
16
-
(license ISC)
15
+
(homepage "https://tangled.org/@anil.recoil.org/ocaml-jmap")
17
16
18
-
(documentation https://avsm.github.io/ocaml-jmap)
17
+
(bug_reports "https://tangled.org/@anil.recoil.org/ocaml-jmap/issues")
18
+
19
+
(maintenance_intent "(latest)")
19
20
20
21
(package
21
22
(name jmap)
22
23
(synopsis "JMAP protocol implementation for OCaml")
23
24
(description
24
-
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail).")
25
+
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail). Includes subpackages for Eio (jmap.eio) and browser (jmap.brr) clients.")
25
26
(depends
26
27
(ocaml (>= 5.4.0))
27
28
(jsont (>= 0.2.0))
28
29
json-pointer
29
-
(ptime (>= 1.0.0))))
30
-
31
-
(package
32
-
(name jmap-eio)
33
-
(synopsis "JMAP client for Eio")
34
-
(description "High-level JMAP client using Eio for async I/O and the Requests HTTP library.")
35
-
(depends
36
-
(ocaml (>= 5.4.0))
37
-
(jmap (= :version))
38
-
(jsont (>= 0.2.0))
39
-
eio
40
-
requests))
41
-
42
-
(package
43
-
(name jmap-brr)
44
-
(synopsis "JMAP client for browsers")
45
-
(description "JMAP client using Brr for browser-based email clients with js_of_ocaml.")
46
-
(depends
47
-
(ocaml (>= 5.4.0))
48
-
(jmap (= :version))
49
-
(jsont (>= 0.2.0))
50
-
(brr (>= 0.0.6))))
30
+
(ptime (>= 1.0.0))
31
+
(eio :with-test)
32
+
(requests :with-test)
33
+
(brr :with-test))
34
+
(depopts eio requests brr))
+2
-1
eio/dune
+2
-1
eio/dune
-35
jmap-brr.opam
-35
jmap-brr.opam
···
1
-
# This file is generated by dune, edit dune-project instead
2
-
opam-version: "2.0"
3
-
synopsis: "JMAP client for browsers"
4
-
description:
5
-
"JMAP client using Brr for browser-based email clients with js_of_ocaml."
6
-
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
7
-
authors: ["Anil Madhavapeddy <anil@recoil.org>"]
8
-
license: "ISC"
9
-
homepage: "https://github.com/avsm/ocaml-jmap"
10
-
doc: "https://avsm.github.io/ocaml-jmap"
11
-
bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
12
-
depends: [
13
-
"dune" {>= "3.20"}
14
-
"ocaml" {>= "5.4.0"}
15
-
"jmap" {= version}
16
-
"jsont" {>= "0.2.0"}
17
-
"brr" {>= "0.0.6"}
18
-
"odoc" {with-doc}
19
-
]
20
-
build: [
21
-
["dune" "subst"] {dev}
22
-
[
23
-
"dune"
24
-
"build"
25
-
"-p"
26
-
name
27
-
"-j"
28
-
jobs
29
-
"@install"
30
-
"@runtest" {with-test}
31
-
"@doc" {with-doc}
32
-
]
33
-
]
34
-
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
35
-
x-maintenance-intent: ["(latest)"]
-36
jmap-eio.opam
-36
jmap-eio.opam
···
1
-
# This file is generated by dune, edit dune-project instead
2
-
opam-version: "2.0"
3
-
synopsis: "JMAP client for Eio"
4
-
description:
5
-
"High-level JMAP client using Eio for async I/O and the Requests HTTP library."
6
-
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
7
-
authors: ["Anil Madhavapeddy <anil@recoil.org>"]
8
-
license: "ISC"
9
-
homepage: "https://github.com/avsm/ocaml-jmap"
10
-
doc: "https://avsm.github.io/ocaml-jmap"
11
-
bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
12
-
depends: [
13
-
"dune" {>= "3.20"}
14
-
"ocaml" {>= "5.4.0"}
15
-
"jmap" {= version}
16
-
"jsont" {>= "0.2.0"}
17
-
"eio"
18
-
"requests"
19
-
"odoc" {with-doc}
20
-
]
21
-
build: [
22
-
["dune" "subst"] {dev}
23
-
[
24
-
"dune"
25
-
"build"
26
-
"-p"
27
-
name
28
-
"-j"
29
-
jobs
30
-
"@install"
31
-
"@runtest" {with-test}
32
-
"@doc" {with-doc}
33
-
]
34
-
]
35
-
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
36
-
x-maintenance-intent: ["(latest)"]
+7
-5
jmap.opam
+7
-5
jmap.opam
···
2
2
opam-version: "2.0"
3
3
synopsis: "JMAP protocol implementation for OCaml"
4
4
description:
5
-
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail)."
5
+
"A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail). Includes subpackages for Eio (jmap.eio) and browser (jmap.brr) clients."
6
6
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
7
7
authors: ["Anil Madhavapeddy <anil@recoil.org>"]
8
8
license: "ISC"
9
-
homepage: "https://github.com/avsm/ocaml-jmap"
10
-
doc: "https://avsm.github.io/ocaml-jmap"
11
-
bug-reports: "https://github.com/avsm/ocaml-jmap/issues"
9
+
homepage: "https://tangled.org/@anil.recoil.org/ocaml-jmap"
10
+
bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-jmap/issues"
12
11
depends: [
13
12
"dune" {>= "3.20"}
14
13
"ocaml" {>= "5.4.0"}
15
14
"jsont" {>= "0.2.0"}
16
15
"json-pointer"
17
16
"ptime" {>= "1.0.0"}
17
+
"eio" {with-test}
18
+
"requests" {with-test}
19
+
"brr" {with-test}
18
20
"odoc" {with-doc}
19
21
]
22
+
depopts: ["eio" "requests" "brr"]
20
23
build: [
21
24
["dune" "subst"] {dev}
22
25
[
···
31
34
"@doc" {with-doc}
32
35
]
33
36
]
34
-
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
35
37
x-maintenance-intent: ["(latest)"]
+2
-1
lib/js/dune
+2
-1
lib/js/dune
+18
-8
lib/proto/proto_method.ml
+18
-8
lib/proto/proto_method.ml
···
132
132
133
133
let set_response_jsont (type a) (obj_jsont : a Jsont.t) : a set_response Jsont.t =
134
134
let kind = "SetResponse" in
135
+
(* All map/list fields in SetResponse can be null per RFC 8620 Section 5.3 *)
136
+
(* opt_mem handles missing keys, Jsont.option handles explicit null values *)
137
+
(* Option.join flattens option option -> option *)
138
+
let join = Option.join in
135
139
let make account_id old_state new_state created updated destroyed
136
140
not_created not_updated not_destroyed =
137
-
{ account_id; old_state; new_state; created; updated; destroyed;
138
-
not_created; not_updated; not_destroyed }
141
+
{ account_id; old_state; new_state;
142
+
created = join created;
143
+
updated = join updated;
144
+
destroyed = join destroyed;
145
+
not_created = join not_created;
146
+
not_updated = join not_updated;
147
+
not_destroyed = join not_destroyed }
139
148
in
140
149
(* For updated values, the server may return null or an object - RFC 8620 Section 5.3 *)
141
150
(* "Id[Foo|null]" means map values can be null, use Jsont.option to handle this *)
142
151
let nullable_obj = Jsont.(option obj_jsont) in
152
+
let opt enc = Option.map Option.some enc in
143
153
Jsont.Object.map ~kind make
144
154
|> Jsont.Object.mem "accountId" Proto_id.jsont ~enc:(fun r -> r.account_id)
145
155
|> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state)
146
156
|> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state)
147
-
|> Jsont.Object.opt_mem "created" (Proto_json_map.of_id obj_jsont) ~enc:(fun r -> r.created)
148
-
|> Jsont.Object.opt_mem "updated" (Proto_json_map.of_id nullable_obj) ~enc:(fun r -> r.updated)
149
-
|> Jsont.Object.opt_mem "destroyed" (Jsont.list Proto_id.jsont) ~enc:(fun r -> r.destroyed)
150
-
|> Jsont.Object.opt_mem "notCreated" (Proto_json_map.of_id Proto_error.set_error_jsont) ~enc:(fun r -> r.not_created)
151
-
|> Jsont.Object.opt_mem "notUpdated" (Proto_json_map.of_id Proto_error.set_error_jsont) ~enc:(fun r -> r.not_updated)
152
-
|> Jsont.Object.opt_mem "notDestroyed" (Proto_json_map.of_id Proto_error.set_error_jsont) ~enc:(fun r -> r.not_destroyed)
157
+
|> Jsont.Object.opt_mem "created" Jsont.(option (Proto_json_map.of_id obj_jsont)) ~enc:(fun r -> opt r.created)
158
+
|> Jsont.Object.opt_mem "updated" Jsont.(option (Proto_json_map.of_id nullable_obj)) ~enc:(fun r -> opt r.updated)
159
+
|> Jsont.Object.opt_mem "destroyed" Jsont.(option (list Proto_id.jsont)) ~enc:(fun r -> opt r.destroyed)
160
+
|> Jsont.Object.opt_mem "notCreated" Jsont.(option (Proto_json_map.of_id Proto_error.set_error_jsont)) ~enc:(fun r -> opt r.not_created)
161
+
|> Jsont.Object.opt_mem "notUpdated" Jsont.(option (Proto_json_map.of_id Proto_error.set_error_jsont)) ~enc:(fun r -> opt r.not_updated)
162
+
|> Jsont.Object.opt_mem "notDestroyed" Jsont.(option (Proto_json_map.of_id Proto_error.set_error_jsont)) ~enc:(fun r -> opt r.not_destroyed)
153
163
|> Jsont.Object.finish
154
164
155
165
(* Foo/copy *)