+545
bin/jmap.ml
+545
bin/jmap.ml
···
716
716
let info = Cmd.info "identities" ~doc in
717
717
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term)
718
718
719
+
(** {1 Chained Commands - Using the Chain monad} *)
720
+
721
+
(** Inbox command - demonstrates simple query+get chain *)
722
+
let inbox_cmd =
723
+
let limit_term =
724
+
let doc = "Maximum number of emails to show" in
725
+
Arg.(value & opt int 20 & info ["limit"; "n"] ~docv:"N" ~doc)
726
+
in
727
+
let run cfg limit =
728
+
Eio_main.run @@ fun env ->
729
+
Eio.Switch.run @@ fun sw ->
730
+
let client = Jmap_eio.Cli.create_client ~sw env cfg in
731
+
let account_id = Jmap_eio.Cli.get_account_id cfg client in
732
+
733
+
Jmap_eio.Cli.debug cfg "Fetching inbox emails using Chain API";
734
+
735
+
(* Find inbox mailbox first *)
736
+
let mbox_req = Jmap_eio.Client.Build.(
737
+
make_request
738
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
739
+
[mailbox_get ~call_id:"m1" ~account_id ()]
740
+
) in
741
+
742
+
match Jmap_eio.Client.request client mbox_req with
743
+
| Error e ->
744
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
745
+
exit 1
746
+
| Ok mbox_response ->
747
+
match Jmap_eio.Client.Parse.parse_mailbox_get ~call_id:"m1" mbox_response with
748
+
| Error e ->
749
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
750
+
exit 1
751
+
| Ok mbox_result ->
752
+
(* Find inbox *)
753
+
let inbox =
754
+
List.find_opt (fun (m : Jmap.Proto.Mailbox.t) ->
755
+
m.role = Some Jmap.Proto.Mailbox.Inbox
756
+
) mbox_result.list
757
+
in
758
+
match inbox with
759
+
| None ->
760
+
Fmt.epr "No inbox found@.";
761
+
exit 1
762
+
| Some inbox ->
763
+
Jmap_eio.Cli.debug cfg "Found inbox: %s" (Jmap.Proto.Id.to_string inbox.id);
764
+
765
+
(* Now use Chain API to query and get emails in one request *)
766
+
let open Jmap_eio.Chain in
767
+
let filter_cond : Jmap.Proto.Email.Filter_condition.t = {
768
+
in_mailbox = Some inbox.id;
769
+
in_mailbox_other_than = None;
770
+
before = None; after = None;
771
+
min_size = None; max_size = None;
772
+
all_in_thread_have_keyword = None;
773
+
some_in_thread_have_keyword = None;
774
+
none_in_thread_have_keyword = None;
775
+
has_keyword = None; not_keyword = None;
776
+
has_attachment = None;
777
+
text = None; from = None; to_ = None;
778
+
cc = None; bcc = None; subject = None;
779
+
body = None; header = None;
780
+
} in
781
+
let sort = [Jmap.Proto.Filter.comparator ~is_ascending:false "receivedAt"] in
782
+
783
+
let request, email_handle = build
784
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
785
+
begin
786
+
let* query = email_query ~account_id
787
+
~filter:(Jmap.Proto.Filter.Condition filter_cond)
788
+
~sort
789
+
~limit:(Int64.of_int limit)
790
+
()
791
+
in
792
+
let* emails = email_get ~account_id
793
+
~ids:(from_query query)
794
+
~properties:["id"; "blobId"; "threadId"; "mailboxIds"; "size"; "receivedAt"; "subject"; "from"; "preview"; "keywords"]
795
+
()
796
+
in
797
+
return emails
798
+
end in
799
+
800
+
Jmap_eio.Cli.debug cfg "Sending chained request (query + get in one round trip)";
801
+
802
+
match Jmap_eio.Client.request client request with
803
+
| Error e ->
804
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
805
+
exit 1
806
+
| Ok response ->
807
+
match parse email_handle response with
808
+
| Error e ->
809
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
810
+
exit 1
811
+
| Ok result ->
812
+
Fmt.pr "@[<v>%a (%d emails in inbox)@,@,"
813
+
Fmt.(styled `Bold string) "Inbox"
814
+
(List.length result.list);
815
+
List.iter (fun (email : Jmap.Proto.Email.t) ->
816
+
let from_str = match email.from with
817
+
| Some (addr :: _) ->
818
+
Option.value addr.name ~default:addr.email
819
+
| _ -> "?"
820
+
in
821
+
let subject = Option.value email.subject ~default:"(no subject)" in
822
+
let flags = format_keywords email.keywords in
823
+
Fmt.pr " %a %s@,"
824
+
Fmt.(styled `Cyan string) (Jmap.Proto.Id.to_string email.id)
825
+
(ptime_to_string email.received_at);
826
+
Fmt.pr " From: %s@," (truncate_string 40 from_str);
827
+
Fmt.pr " Subject: %a%s@,"
828
+
Fmt.(styled `White string) (truncate_string 50 subject)
829
+
(if flags = "" then "" else " [" ^ flags ^ "]");
830
+
Fmt.pr "@,"
831
+
) result.list;
832
+
Fmt.pr "@]@."
833
+
in
834
+
let doc = "List inbox emails (uses Chain API for query+get in single request)" in
835
+
let info = Cmd.info "inbox" ~doc in
836
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ limit_term)
837
+
838
+
(** Thread-view command - demonstrates multi-step chaining (RFC 8620 example) *)
839
+
let thread_view_cmd =
840
+
let limit_term =
841
+
let doc = "Number of threads to show" in
842
+
Arg.(value & opt int 10 & info ["limit"; "n"] ~docv:"N" ~doc)
843
+
in
844
+
let run cfg limit =
845
+
Eio_main.run @@ fun env ->
846
+
Eio.Switch.run @@ fun sw ->
847
+
let client = Jmap_eio.Cli.create_client ~sw env cfg in
848
+
let account_id = Jmap_eio.Cli.get_account_id cfg client in
849
+
850
+
Jmap_eio.Cli.debug cfg "Fetching threaded view using multi-step Chain API";
851
+
852
+
(*
853
+
This implements the RFC 8620 example:
854
+
1. Email/query with collapseThreads to get one email per thread
855
+
2. Email/get to fetch threadId for each
856
+
3. Thread/get to fetch all emailIds in each thread
857
+
4. Email/get to fetch details for all emails in those threads
858
+
*)
859
+
let open Jmap_eio.Chain in
860
+
let sort = [Jmap.Proto.Filter.comparator ~is_ascending:false "receivedAt"] in
861
+
862
+
let request, (query_h, final_emails_h) = build
863
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
864
+
begin
865
+
(* Step 1: Query for recent emails, collapsing threads *)
866
+
let* query = email_query ~account_id
867
+
~sort
868
+
~collapse_threads:true
869
+
~limit:(Int64.of_int limit)
870
+
()
871
+
in
872
+
(* Step 2: Get just threadId for those emails *)
873
+
let* emails1 = email_get ~account_id
874
+
~ids:(from_query query)
875
+
~properties:["id"; "blobId"; "threadId"; "mailboxIds"; "size"; "receivedAt"]
876
+
()
877
+
in
878
+
(* Step 3: Get threads using threadIds from step 2 *)
879
+
let* threads = thread_get ~account_id
880
+
~ids:(from_get_field emails1 "threadId")
881
+
()
882
+
in
883
+
(* Step 4: Get all emails in those threads *)
884
+
let* emails2 = email_get ~account_id
885
+
~ids:(from_get_field threads "emailIds")
886
+
~properties:["id"; "blobId"; "threadId"; "mailboxIds"; "size"; "receivedAt"; "subject"; "from"; "preview"]
887
+
()
888
+
in
889
+
return (query, emails2)
890
+
end in
891
+
892
+
Jmap_eio.Cli.debug cfg "Sending 4-step chained request in single round trip";
893
+
894
+
match Jmap_eio.Client.request client request with
895
+
| Error e ->
896
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
897
+
exit 1
898
+
| Ok response ->
899
+
let query_result = parse_exn query_h response in
900
+
let emails_result = parse_exn final_emails_h response in
901
+
902
+
(* Group emails by thread *)
903
+
let threads_map = Hashtbl.create 16 in
904
+
List.iter (fun (email : Jmap.Proto.Email.t) ->
905
+
let tid = Jmap.Proto.Id.to_string email.thread_id in
906
+
let existing = try Hashtbl.find threads_map tid with Not_found -> [] in
907
+
Hashtbl.replace threads_map tid (email :: existing)
908
+
) emails_result.list;
909
+
910
+
Fmt.pr "@[<v>%a (%d threads, %d total emails)@,@,"
911
+
Fmt.(styled `Bold string) "Threaded View"
912
+
(Hashtbl.length threads_map)
913
+
(List.length emails_result.list);
914
+
Fmt.pr "Query found %s total matching emails@,@,"
915
+
(match query_result.total with Some n -> Int64.to_string n | None -> "?");
916
+
917
+
(* Print threads *)
918
+
Hashtbl.iter (fun _tid emails ->
919
+
let emails = List.sort (fun (a : Jmap.Proto.Email.t) (b : Jmap.Proto.Email.t) ->
920
+
Ptime.compare a.received_at b.received_at
921
+
) emails in
922
+
let first_email = List.hd emails in
923
+
let subject = Option.value first_email.subject ~default:"(no subject)" in
924
+
Fmt.pr " %a Thread: %s (%d emails)@,"
925
+
Fmt.(styled `Bold string) "▸"
926
+
(truncate_string 50 subject)
927
+
(List.length emails);
928
+
List.iter (fun (email : Jmap.Proto.Email.t) ->
929
+
let from_str = match email.from with
930
+
| Some (addr :: _) -> Option.value addr.name ~default:addr.email
931
+
| _ -> "?"
932
+
in
933
+
Fmt.pr " %s %s %s@,"
934
+
(Jmap.Proto.Id.to_string email.id |> truncate_string 12)
935
+
(ptime_to_string email.received_at)
936
+
(truncate_string 30 from_str)
937
+
) emails;
938
+
Fmt.pr "@,"
939
+
) threads_map;
940
+
Fmt.pr "@]@."
941
+
in
942
+
let doc = "Show threaded view (demonstrates RFC 8620 multi-step chain)" in
943
+
let info = Cmd.info "thread-view" ~doc in
944
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ limit_term)
945
+
946
+
(** Mark-read command - demonstrates email_set for updating keywords *)
947
+
let mark_read_cmd =
948
+
let email_id_term =
949
+
let doc = "Email ID to mark as read" in
950
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"EMAIL_ID" ~doc)
951
+
in
952
+
let unread_term =
953
+
let doc = "Mark as unread instead of read" in
954
+
Arg.(value & flag & info ["unread"; "u"] ~doc)
955
+
in
956
+
let run cfg email_id_str unread =
957
+
Eio_main.run @@ fun env ->
958
+
Eio.Switch.run @@ fun sw ->
959
+
let client = Jmap_eio.Cli.create_client ~sw env cfg in
960
+
let account_id = Jmap_eio.Cli.get_account_id cfg client in
961
+
let email_id = Jmap.Proto.Id.of_string_exn email_id_str in
962
+
963
+
Jmap_eio.Cli.debug cfg "%s email %s"
964
+
(if unread then "Marking as unread" else "Marking as read")
965
+
email_id_str;
966
+
967
+
(* Build the patch object - set or unset $seen keyword *)
968
+
let patch =
969
+
let open Jmap_eio.Chain in
970
+
if unread then
971
+
json_obj [("keywords/$seen", json_null)]
972
+
else
973
+
json_obj [("keywords/$seen", json_bool true)]
974
+
in
975
+
976
+
let open Jmap_eio.Chain in
977
+
let request, set_h = build
978
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
979
+
begin
980
+
email_set ~account_id
981
+
~update:[(email_id, patch)]
982
+
()
983
+
end in
984
+
985
+
match Jmap_eio.Client.request client request with
986
+
| Error e ->
987
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
988
+
exit 1
989
+
| Ok response ->
990
+
match parse set_h response with
991
+
| Error e ->
992
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
993
+
exit 1
994
+
| Ok result ->
995
+
(* Check if update succeeded *)
996
+
let updated_ids =
997
+
result.updated
998
+
|> Option.value ~default:[]
999
+
|> List.map (fun (id, _) -> Jmap.Proto.Id.to_string id)
1000
+
in
1001
+
if List.mem email_id_str updated_ids then
1002
+
Fmt.pr "Email %s marked as %s@."
1003
+
email_id_str
1004
+
(if unread then "unread" else "read")
1005
+
else (
1006
+
Fmt.epr "Failed to update email. ";
1007
+
let not_updated = Option.value ~default:[] result.not_updated in
1008
+
(match List.find_opt (fun (id, _) -> Jmap.Proto.Id.to_string id = email_id_str) not_updated with
1009
+
| Some (_, err) ->
1010
+
let open Jmap.Proto.Error in
1011
+
let err_type = set_error_type_to_string err.type_ in
1012
+
let err_desc = Option.value ~default:"" err.description in
1013
+
Fmt.epr "Error: %s (%s)@." err_type err_desc
1014
+
| None ->
1015
+
Fmt.epr "Unknown error@.");
1016
+
exit 1
1017
+
)
1018
+
in
1019
+
let doc = "Mark an email as read/unread (demonstrates Email/set)" in
1020
+
let info = Cmd.info "mark-read" ~doc in
1021
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ email_id_term $ unread_term)
1022
+
1023
+
(** Delete email command - demonstrates email_set destroy *)
1024
+
let delete_email_cmd =
1025
+
let email_ids_term =
1026
+
let doc = "Email IDs to delete" in
1027
+
Arg.(non_empty & pos_all string [] & info [] ~docv:"EMAIL_ID" ~doc)
1028
+
in
1029
+
let run cfg email_id_strs =
1030
+
Eio_main.run @@ fun env ->
1031
+
Eio.Switch.run @@ fun sw ->
1032
+
let client = Jmap_eio.Cli.create_client ~sw env cfg in
1033
+
let account_id = Jmap_eio.Cli.get_account_id cfg client in
1034
+
let email_ids = List.map Jmap.Proto.Id.of_string_exn email_id_strs in
1035
+
1036
+
Jmap_eio.Cli.debug cfg "Deleting %d email(s)" (List.length email_ids);
1037
+
1038
+
let open Jmap_eio.Chain in
1039
+
let request, set_h = build
1040
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
1041
+
begin
1042
+
email_set ~account_id
1043
+
~destroy:(ids email_ids)
1044
+
()
1045
+
end in
1046
+
1047
+
match Jmap_eio.Client.request client request with
1048
+
| Error e ->
1049
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
1050
+
exit 1
1051
+
| Ok response ->
1052
+
match parse set_h response with
1053
+
| Error e ->
1054
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
1055
+
exit 1
1056
+
| Ok result ->
1057
+
let destroyed = Option.value ~default:[] result.destroyed in
1058
+
let destroyed_ids = List.map Jmap.Proto.Id.to_string destroyed in
1059
+
Fmt.pr "Deleted %d email(s):@." (List.length destroyed_ids);
1060
+
List.iter (fun id -> Fmt.pr " %s@." id) destroyed_ids;
1061
+
(* Report any failures *)
1062
+
let not_destroyed = Option.value ~default:[] result.not_destroyed in
1063
+
if not_destroyed <> [] then begin
1064
+
Fmt.epr "Failed to delete %d email(s):@." (List.length not_destroyed);
1065
+
List.iter (fun (id, err) ->
1066
+
let open Jmap.Proto.Error in
1067
+
let err_type = set_error_type_to_string err.type_ in
1068
+
Fmt.epr " %s: %s@."
1069
+
(Jmap.Proto.Id.to_string id)
1070
+
err_type
1071
+
) not_destroyed
1072
+
end
1073
+
in
1074
+
let doc = "Delete emails (demonstrates Email/set destroy)" in
1075
+
let info = Cmd.info "delete" ~doc in
1076
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ email_ids_term)
1077
+
1078
+
(** Changes command - demonstrates email_changes for sync *)
1079
+
let changes_cmd =
1080
+
let state_term =
1081
+
let doc = "State to get changes since (use 'current' to just show current state)" in
1082
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"STATE" ~doc)
1083
+
in
1084
+
let run cfg state_str =
1085
+
Eio_main.run @@ fun env ->
1086
+
Eio.Switch.run @@ fun sw ->
1087
+
let client = Jmap_eio.Cli.create_client ~sw env cfg in
1088
+
let account_id = Jmap_eio.Cli.get_account_id cfg client in
1089
+
1090
+
if state_str = "current" then (
1091
+
(* Just get current state by doing a minimal query *)
1092
+
let open Jmap_eio.Chain in
1093
+
let request, get_h = build
1094
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
1095
+
begin
1096
+
(* Get empty list just to see state *)
1097
+
email_get ~account_id ~ids:(ids []) ()
1098
+
end in
1099
+
1100
+
match Jmap_eio.Client.request client request with
1101
+
| Error e ->
1102
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
1103
+
exit 1
1104
+
| Ok response ->
1105
+
match parse get_h response with
1106
+
| Error e ->
1107
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
1108
+
exit 1
1109
+
| Ok result ->
1110
+
Fmt.pr "Current email state: %a@."
1111
+
Fmt.(styled `Cyan string) result.state
1112
+
) else (
1113
+
Jmap_eio.Cli.debug cfg "Getting changes since state: %s" state_str;
1114
+
1115
+
let open Jmap_eio.Chain in
1116
+
let request, changes_h = build
1117
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
1118
+
begin
1119
+
email_changes ~account_id ~since_state:state_str ()
1120
+
end in
1121
+
1122
+
match Jmap_eio.Client.request client request with
1123
+
| Error e ->
1124
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
1125
+
exit 1
1126
+
| Ok response ->
1127
+
match parse changes_h response with
1128
+
| Error e ->
1129
+
Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e);
1130
+
exit 1
1131
+
| Ok result ->
1132
+
Fmt.pr "@[<v>%a@,@,"
1133
+
Fmt.(styled `Bold string) "Email Changes";
1134
+
Fmt.pr "Old state: %s@," result.old_state;
1135
+
Fmt.pr "New state: %a@," Fmt.(styled `Cyan string) result.new_state;
1136
+
Fmt.pr "Has more changes: %b@,@," result.has_more_changes;
1137
+
Fmt.pr "Created: %d email(s)@," (List.length result.created);
1138
+
List.iter (fun id ->
1139
+
Fmt.pr " + %s@," (Jmap.Proto.Id.to_string id)
1140
+
) result.created;
1141
+
Fmt.pr "Updated: %d email(s)@," (List.length result.updated);
1142
+
List.iter (fun id ->
1143
+
Fmt.pr " ~ %s@," (Jmap.Proto.Id.to_string id)
1144
+
) result.updated;
1145
+
Fmt.pr "Destroyed: %d email(s)@," (List.length result.destroyed);
1146
+
List.iter (fun id ->
1147
+
Fmt.pr " - %s@," (Jmap.Proto.Id.to_string id)
1148
+
) result.destroyed;
1149
+
Fmt.pr "@]@."
1150
+
)
1151
+
in
1152
+
let doc = "Show email changes since a state (demonstrates Email/changes)" in
1153
+
let info = Cmd.info "changes" ~doc in
1154
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ state_term)
1155
+
1156
+
(** Sync command - demonstrates changes + get pattern for incremental sync *)
1157
+
let sync_cmd =
1158
+
let state_term =
1159
+
let doc = "State to sync from" in
1160
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"STATE" ~doc)
1161
+
in
1162
+
let run cfg state_str =
1163
+
Eio_main.run @@ fun env ->
1164
+
Eio.Switch.run @@ fun sw ->
1165
+
let client = Jmap_eio.Cli.create_client ~sw env cfg in
1166
+
let account_id = Jmap_eio.Cli.get_account_id cfg client in
1167
+
1168
+
Jmap_eio.Cli.debug cfg "Syncing from state: %s" state_str;
1169
+
1170
+
(* Chain: changes → get created → get updated *)
1171
+
let open Jmap_eio.Chain in
1172
+
let request, (changes_h, created_h, updated_h) = build
1173
+
~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail]
1174
+
begin
1175
+
let* changes = email_changes ~account_id ~since_state:state_str () in
1176
+
let* created = email_get ~account_id
1177
+
~ids:(from_changes_created changes)
1178
+
~properties:["id"; "blobId"; "threadId"; "mailboxIds"; "size"; "receivedAt"; "subject"; "from"; "preview"]
1179
+
()
1180
+
in
1181
+
let* updated = email_get ~account_id
1182
+
~ids:(from_changes_updated changes)
1183
+
~properties:["id"; "blobId"; "threadId"; "mailboxIds"; "size"; "receivedAt"; "subject"; "from"; "keywords"]
1184
+
()
1185
+
in
1186
+
return (changes, created, updated)
1187
+
end in
1188
+
1189
+
Jmap_eio.Cli.debug cfg "Sending chained sync request";
1190
+
1191
+
match Jmap_eio.Client.request client request with
1192
+
| Error e ->
1193
+
Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e);
1194
+
exit 1
1195
+
| Ok response ->
1196
+
let changes_result = parse_exn changes_h response in
1197
+
let created_result = parse_exn created_h response in
1198
+
let updated_result = parse_exn updated_h response in
1199
+
1200
+
Fmt.pr "@[<v>%a (state: %s → %s)@,@,"
1201
+
Fmt.(styled `Bold string) "Sync Results"
1202
+
changes_result.old_state
1203
+
changes_result.new_state;
1204
+
1205
+
if List.length created_result.list > 0 then begin
1206
+
Fmt.pr "%a (%d)@,"
1207
+
Fmt.(styled `Green string) "New emails"
1208
+
(List.length created_result.list);
1209
+
List.iter (fun (email : Jmap.Proto.Email.t) ->
1210
+
let from_str = match email.from with
1211
+
| Some (addr :: _) -> Option.value addr.name ~default:addr.email
1212
+
| _ -> "?"
1213
+
in
1214
+
let subject = Option.value email.subject ~default:"(no subject)" in
1215
+
Fmt.pr " + %s %s %s@,"
1216
+
(Jmap.Proto.Id.to_string email.id |> truncate_string 12)
1217
+
(truncate_string 20 from_str)
1218
+
(truncate_string 40 subject)
1219
+
) created_result.list;
1220
+
Fmt.pr "@,"
1221
+
end;
1222
+
1223
+
if List.length updated_result.list > 0 then begin
1224
+
Fmt.pr "%a (%d)@,"
1225
+
Fmt.(styled `Yellow string) "Updated emails"
1226
+
(List.length updated_result.list);
1227
+
List.iter (fun (email : Jmap.Proto.Email.t) ->
1228
+
let flags = format_keywords email.keywords in
1229
+
Fmt.pr " ~ %s [%s]@,"
1230
+
(Jmap.Proto.Id.to_string email.id |> truncate_string 12)
1231
+
flags
1232
+
) updated_result.list;
1233
+
Fmt.pr "@,"
1234
+
end;
1235
+
1236
+
if List.length changes_result.destroyed > 0 then begin
1237
+
Fmt.pr "%a (%d)@,"
1238
+
Fmt.(styled `Red string) "Deleted emails"
1239
+
(List.length changes_result.destroyed);
1240
+
List.iter (fun id ->
1241
+
Fmt.pr " - %s@," (Jmap.Proto.Id.to_string id)
1242
+
) changes_result.destroyed;
1243
+
Fmt.pr "@,"
1244
+
end;
1245
+
1246
+
if changes_result.has_more_changes then
1247
+
Fmt.pr "%a - call sync again with state %s@,"
1248
+
Fmt.(styled `Bold string) "More changes available"
1249
+
changes_result.new_state;
1250
+
1251
+
Fmt.pr "@]@."
1252
+
in
1253
+
let doc = "Incremental sync (demonstrates changes + get chain)" in
1254
+
let info = Cmd.info "sync" ~doc in
1255
+
Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ state_term)
1256
+
719
1257
(** {1 Main Command Group} *)
720
1258
721
1259
let main_cmd =
···
742
1280
recent_cmd;
743
1281
threads_cmd;
744
1282
identities_cmd;
1283
+
(* Chain API examples *)
1284
+
inbox_cmd;
1285
+
thread_view_cmd;
1286
+
mark_read_cmd;
1287
+
delete_email_cmd;
1288
+
changes_cmd;
1289
+
sync_cmd;
745
1290
]
746
1291
747
1292
let () =
+1
eio/jmap_eio.ml
+1
eio/jmap_eio.ml
+7
eio/jmap_eio.mli
+7
eio/jmap_eio.mli
···
74
74
75
75
(** CLI configuration and cmdliner terms for JMAP tools. *)
76
76
module Cli = Cli
77
+
78
+
(** Method chaining with automatic result references.
79
+
80
+
Provides a monadic interface for building JMAP requests where method
81
+
calls can reference results from previous calls in the same request.
82
+
Call IDs are generated automatically. *)
83
+
module Chain = Jmap.Chain
+851
lib/core/chain.ml
+851
lib/core/chain.ml
···
1
+
(*---------------------------------------------------------------------------
2
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3
+
SPDX-License-Identifier: ISC
4
+
---------------------------------------------------------------------------*)
5
+
6
+
open Jmap_proto
7
+
8
+
(* Phantom types for handle kinds *)
9
+
type query
10
+
type get
11
+
type changes
12
+
type set
13
+
type query_changes
14
+
type copy
15
+
type import
16
+
type parse
17
+
18
+
(* Internal handle representation with GADT for response type *)
19
+
type (_, _) handle =
20
+
| Query_handle : {
21
+
call_id : string;
22
+
method_name : string;
23
+
} -> (query, Method.query_response) handle
24
+
| Query_changes_handle : {
25
+
call_id : string;
26
+
method_name : string;
27
+
} -> (query_changes, Method.query_changes_response) handle
28
+
| Email_get_handle : {
29
+
call_id : string;
30
+
method_name : string;
31
+
} -> (get, Email.t Method.get_response) handle
32
+
| Thread_get_handle : {
33
+
call_id : string;
34
+
method_name : string;
35
+
} -> (get, Thread.t Method.get_response) handle
36
+
| Mailbox_get_handle : {
37
+
call_id : string;
38
+
method_name : string;
39
+
} -> (get, Mailbox.t Method.get_response) handle
40
+
| Identity_get_handle : {
41
+
call_id : string;
42
+
method_name : string;
43
+
} -> (get, Identity.t Method.get_response) handle
44
+
| Submission_get_handle : {
45
+
call_id : string;
46
+
method_name : string;
47
+
} -> (get, Submission.t Method.get_response) handle
48
+
| Search_snippet_get_handle : {
49
+
call_id : string;
50
+
method_name : string;
51
+
} -> (get, Search_snippet.t Method.get_response) handle
52
+
| Vacation_get_handle : {
53
+
call_id : string;
54
+
method_name : string;
55
+
} -> (get, Vacation.t Method.get_response) handle
56
+
| Changes_handle : {
57
+
call_id : string;
58
+
method_name : string;
59
+
} -> (changes, Method.changes_response) handle
60
+
| Email_set_handle : {
61
+
call_id : string;
62
+
method_name : string;
63
+
} -> (set, Email.t Method.set_response) handle
64
+
| Mailbox_set_handle : {
65
+
call_id : string;
66
+
method_name : string;
67
+
} -> (set, Mailbox.t Method.set_response) handle
68
+
| Identity_set_handle : {
69
+
call_id : string;
70
+
method_name : string;
71
+
} -> (set, Identity.t Method.set_response) handle
72
+
| Submission_set_handle : {
73
+
call_id : string;
74
+
method_name : string;
75
+
} -> (set, Submission.t Method.set_response) handle
76
+
| Vacation_set_handle : {
77
+
call_id : string;
78
+
method_name : string;
79
+
} -> (set, Vacation.t Method.set_response) handle
80
+
| Email_copy_handle : {
81
+
call_id : string;
82
+
method_name : string;
83
+
} -> (copy, Email.t Method.copy_response) handle
84
+
| Raw_handle : {
85
+
call_id : string;
86
+
method_name : string;
87
+
} -> (unit, Jsont.Json.t) handle
88
+
89
+
let call_id : type k r. (k, r) handle -> string = function
90
+
| Query_handle h -> h.call_id
91
+
| Query_changes_handle h -> h.call_id
92
+
| Email_get_handle h -> h.call_id
93
+
| Thread_get_handle h -> h.call_id
94
+
| Mailbox_get_handle h -> h.call_id
95
+
| Identity_get_handle h -> h.call_id
96
+
| Submission_get_handle h -> h.call_id
97
+
| Search_snippet_get_handle h -> h.call_id
98
+
| Vacation_get_handle h -> h.call_id
99
+
| Changes_handle h -> h.call_id
100
+
| Email_set_handle h -> h.call_id
101
+
| Mailbox_set_handle h -> h.call_id
102
+
| Identity_set_handle h -> h.call_id
103
+
| Submission_set_handle h -> h.call_id
104
+
| Vacation_set_handle h -> h.call_id
105
+
| Email_copy_handle h -> h.call_id
106
+
| Raw_handle h -> h.call_id
107
+
108
+
let method_name : type k r. (k, r) handle -> string = function
109
+
| Query_handle h -> h.method_name
110
+
| Query_changes_handle h -> h.method_name
111
+
| Email_get_handle h -> h.method_name
112
+
| Thread_get_handle h -> h.method_name
113
+
| Mailbox_get_handle h -> h.method_name
114
+
| Identity_get_handle h -> h.method_name
115
+
| Submission_get_handle h -> h.method_name
116
+
| Search_snippet_get_handle h -> h.method_name
117
+
| Vacation_get_handle h -> h.method_name
118
+
| Changes_handle h -> h.method_name
119
+
| Email_set_handle h -> h.method_name
120
+
| Mailbox_set_handle h -> h.method_name
121
+
| Identity_set_handle h -> h.method_name
122
+
| Submission_set_handle h -> h.method_name
123
+
| Vacation_set_handle h -> h.method_name
124
+
| Email_copy_handle h -> h.method_name
125
+
| Raw_handle h -> h.method_name
126
+
127
+
(* Creation IDs *)
128
+
type 'a create_id = string
129
+
130
+
let created_id cid = Id.of_string_exn ("#" ^ cid)
131
+
let created_id_of_string s = Id.of_string_exn ("#" ^ s)
132
+
133
+
(* ID sources *)
134
+
type id_source =
135
+
| Ids of Id.t list
136
+
| Ref of Invocation.result_reference
137
+
138
+
let ids lst = Ids lst
139
+
let id x = Ids [x]
140
+
141
+
let make_ref ~call_id ~method_name ~path =
142
+
Ref (Invocation.result_reference_of_strings
143
+
~result_of:call_id
144
+
~name:method_name
145
+
~path)
146
+
147
+
let from_query : type r. (query, r) handle -> id_source = fun h ->
148
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/ids"
149
+
150
+
let from_get_ids : type r. (get, r) handle -> id_source = fun h ->
151
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/list/*/id"
152
+
153
+
let from_get_field : type r. (get, r) handle -> string -> id_source = fun h field ->
154
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h)
155
+
~path:(Printf.sprintf "/list/*/%s" field)
156
+
157
+
let from_changes_created : type r. (changes, r) handle -> id_source = fun h ->
158
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/created"
159
+
160
+
let from_changes_updated : type r. (changes, r) handle -> id_source = fun h ->
161
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/updated"
162
+
163
+
let from_changes_destroyed : type r. (changes, r) handle -> id_source = fun h ->
164
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/destroyed"
165
+
166
+
let from_set_created : type r. (set, r) handle -> id_source = fun h ->
167
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/created/*/id"
168
+
169
+
let from_set_updated : type r. (set, r) handle -> id_source = fun h ->
170
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/updated"
171
+
172
+
let from_query_changes_removed : type r. (query_changes, r) handle -> id_source = fun h ->
173
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/removed"
174
+
175
+
let from_query_changes_added : type r. (query_changes, r) handle -> id_source = fun h ->
176
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/added/*/id"
177
+
178
+
let from_copy_created : type r. (copy, r) handle -> id_source = fun h ->
179
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/created/*/id"
180
+
181
+
let from_import_created : type r. (import, r) handle -> id_source = fun h ->
182
+
make_ref ~call_id:(call_id h) ~method_name:(method_name h) ~path:"/created/*/id"
183
+
184
+
(* Chain state *)
185
+
type state = {
186
+
mutable next_id : int;
187
+
mutable next_create_id : int;
188
+
mutable invocations : Invocation.t list;
189
+
}
190
+
191
+
(* Chain monad *)
192
+
type 'a t = state -> 'a
193
+
194
+
let return x _state = x
195
+
196
+
let bind m f state =
197
+
let a = m state in
198
+
f a state
199
+
200
+
let map f m state =
201
+
f (m state)
202
+
203
+
let both a b state =
204
+
let x = a state in
205
+
let y = b state in
206
+
(x, y)
207
+
208
+
let ( let* ) = bind
209
+
let ( let+ ) m f = map f m
210
+
let ( and* ) = both
211
+
let ( and+ ) = both
212
+
213
+
(* Building *)
214
+
let fresh_call_id state =
215
+
let id = Printf.sprintf "c%d" state.next_id in
216
+
state.next_id <- state.next_id + 1;
217
+
id
218
+
219
+
let fresh_create_id () state =
220
+
let id = Printf.sprintf "k%d" state.next_create_id in
221
+
state.next_create_id <- state.next_create_id + 1;
222
+
id
223
+
224
+
let record_invocation inv state =
225
+
state.invocations <- inv :: state.invocations
226
+
227
+
let build ~capabilities chain =
228
+
let state = { next_id = 0; next_create_id = 0; invocations = [] } in
229
+
let result = chain state in
230
+
let request = Request.create
231
+
~using:capabilities
232
+
~method_calls:(List.rev state.invocations)
233
+
()
234
+
in
235
+
(request, result)
236
+
237
+
let build_request ~capabilities chain =
238
+
fst (build ~capabilities chain)
239
+
240
+
(* JSON helpers - exported *)
241
+
let json_null = Jsont.Null ((), Jsont.Meta.none)
242
+
243
+
let json_bool b = Jsont.Bool (b, Jsont.Meta.none)
244
+
245
+
let json_string s = Jsont.String (s, Jsont.Meta.none)
246
+
247
+
let json_int n = Jsont.Number (Int64.to_float n, Jsont.Meta.none)
248
+
249
+
let json_name s = (s, Jsont.Meta.none)
250
+
251
+
let json_obj fields =
252
+
let fields' = List.map (fun (k, v) -> (json_name k, v)) fields in
253
+
Jsont.Object (fields', Jsont.Meta.none)
254
+
255
+
let json_array items = Jsont.Array (items, Jsont.Meta.none)
256
+
257
+
(* JSON helpers - internal *)
258
+
let json_of_id id =
259
+
Jsont.String (Id.to_string id, Jsont.Meta.none)
260
+
261
+
let json_of_id_list ids =
262
+
let items = List.map json_of_id ids in
263
+
Jsont.Array (items, Jsont.Meta.none)
264
+
265
+
let json_of_string_list strs =
266
+
let items = List.map json_string strs in
267
+
Jsont.Array (items, Jsont.Meta.none)
268
+
269
+
let json_map pairs =
270
+
let fields' = List.map (fun (k, v) -> (json_name k, v)) pairs in
271
+
Jsont.Object (fields', Jsont.Meta.none)
272
+
273
+
let encode_to_json jsont value =
274
+
match Jsont.Json.encode' jsont value with
275
+
| Ok j -> j
276
+
| Error _ -> json_obj []
277
+
278
+
let encode_list_to_json jsont values =
279
+
match Jsont.Json.encode' (Jsont.list jsont) values with
280
+
| Ok j -> j
281
+
| Error _ -> Jsont.Array ([], Jsont.Meta.none)
282
+
283
+
(* Add id_source to args *)
284
+
let add_ids_arg args = function
285
+
| None -> args
286
+
| Some (Ids ids) ->
287
+
("ids", json_of_id_list ids) :: args
288
+
| Some (Ref ref_) ->
289
+
let ref_json = encode_to_json Invocation.result_reference_jsont ref_ in
290
+
("#ids", ref_json) :: args
291
+
292
+
let add_destroy_arg args = function
293
+
| None -> args
294
+
| Some (Ids ids) ->
295
+
("destroy", json_of_id_list ids) :: args
296
+
| Some (Ref ref_) ->
297
+
let ref_json = encode_to_json Invocation.result_reference_jsont ref_ in
298
+
("#destroy", ref_json) :: args
299
+
300
+
(* Query builder helper *)
301
+
let build_query_args ~account_id ?filter ?filter_jsont ?sort ?position ?anchor
302
+
?anchor_offset ?limit ?calculate_total () =
303
+
let args = [ ("accountId", json_of_id account_id) ] in
304
+
let args = match filter, filter_jsont with
305
+
| Some f, Some jsont -> ("filter", encode_to_json jsont f) :: args
306
+
| _ -> args
307
+
in
308
+
let args = match sort with
309
+
| None -> args
310
+
| Some comparators -> ("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
311
+
in
312
+
let args = match position with
313
+
| None -> args
314
+
| Some n -> ("position", json_int n) :: args
315
+
in
316
+
let args = match anchor with
317
+
| None -> args
318
+
| Some id -> ("anchor", json_of_id id) :: args
319
+
in
320
+
let args = match anchor_offset with
321
+
| None -> args
322
+
| Some n -> ("anchorOffset", json_int n) :: args
323
+
in
324
+
let args = match limit with
325
+
| None -> args
326
+
| Some n -> ("limit", json_int n) :: args
327
+
in
328
+
let args = match calculate_total with
329
+
| None -> args
330
+
| Some b -> ("calculateTotal", json_bool b) :: args
331
+
in
332
+
args
333
+
334
+
(* Changes builder helper *)
335
+
let build_changes_args ~account_id ~since_state ?max_changes () =
336
+
let args = [
337
+
("accountId", json_of_id account_id);
338
+
("sinceState", json_string since_state);
339
+
] in
340
+
let args = match max_changes with
341
+
| None -> args
342
+
| Some n -> ("maxChanges", json_int n) :: args
343
+
in
344
+
args
345
+
346
+
(* QueryChanges builder helper *)
347
+
let build_query_changes_args ~account_id ~since_query_state ?filter ?filter_jsont
348
+
?sort ?max_changes ?up_to_id ?calculate_total () =
349
+
let args = [
350
+
("accountId", json_of_id account_id);
351
+
("sinceQueryState", json_string since_query_state);
352
+
] in
353
+
let args = match filter, filter_jsont with
354
+
| Some f, Some jsont -> ("filter", encode_to_json jsont f) :: args
355
+
| _ -> args
356
+
in
357
+
let args = match sort with
358
+
| None -> args
359
+
| Some comparators -> ("sort", encode_list_to_json Filter.comparator_jsont comparators) :: args
360
+
in
361
+
let args = match max_changes with
362
+
| None -> args
363
+
| Some n -> ("maxChanges", json_int n) :: args
364
+
in
365
+
let args = match up_to_id with
366
+
| None -> args
367
+
| Some id -> ("upToId", json_of_id id) :: args
368
+
in
369
+
let args = match calculate_total with
370
+
| None -> args
371
+
| Some b -> ("calculateTotal", json_bool b) :: args
372
+
in
373
+
args
374
+
375
+
(* Set builder helper *)
376
+
let build_set_args ~account_id ?if_in_state ?create ?update ?destroy () =
377
+
let args = [ ("accountId", json_of_id account_id) ] in
378
+
let args = match if_in_state with
379
+
| None -> args
380
+
| Some s -> ("ifInState", json_string s) :: args
381
+
in
382
+
let args = match create with
383
+
| None | Some [] -> args
384
+
| Some items ->
385
+
let create_map = json_map (List.map (fun (cid, obj) -> (cid, obj)) items) in
386
+
("create", create_map) :: args
387
+
in
388
+
let args = match update with
389
+
| None | Some [] -> args
390
+
| Some items ->
391
+
let update_map = json_map (List.map (fun (id, patch) -> (Id.to_string id, patch)) items) in
392
+
("update", update_map) :: args
393
+
in
394
+
let args = add_destroy_arg args destroy in
395
+
args
396
+
397
+
(* Method builders *)
398
+
399
+
let email_query ~account_id ?filter ?sort ?position ?anchor ?anchor_offset
400
+
?limit ?calculate_total ?collapse_threads () state =
401
+
let call_id = fresh_call_id state in
402
+
let args = build_query_args ~account_id ?filter
403
+
~filter_jsont:Mail_filter.email_filter_jsont
404
+
?sort ?position ?anchor ?anchor_offset ?limit ?calculate_total () in
405
+
let args = match collapse_threads with
406
+
| None -> args
407
+
| Some b -> ("collapseThreads", json_bool b) :: args
408
+
in
409
+
let inv = Invocation.create
410
+
~name:"Email/query"
411
+
~arguments:(json_obj args)
412
+
~method_call_id:call_id
413
+
in
414
+
record_invocation inv state;
415
+
Query_handle { call_id; method_name = "Email/query" }
416
+
417
+
let email_get ~account_id ?ids ?properties ?body_properties
418
+
?fetch_text_body_values ?fetch_html_body_values ?fetch_all_body_values
419
+
?max_body_value_bytes () state =
420
+
let call_id = fresh_call_id state in
421
+
let args = [ ("accountId", json_of_id account_id) ] in
422
+
let args = add_ids_arg args ids in
423
+
let args = match properties with
424
+
| None -> args
425
+
| Some props -> ("properties", json_of_string_list props) :: args
426
+
in
427
+
let args = match body_properties with
428
+
| None -> args
429
+
| Some props -> ("bodyProperties", json_of_string_list props) :: args
430
+
in
431
+
let args = match fetch_text_body_values with
432
+
| None -> args
433
+
| Some b -> ("fetchTextBodyValues", json_bool b) :: args
434
+
in
435
+
let args = match fetch_html_body_values with
436
+
| None -> args
437
+
| Some b -> ("fetchHTMLBodyValues", json_bool b) :: args
438
+
in
439
+
let args = match fetch_all_body_values with
440
+
| None -> args
441
+
| Some b -> ("fetchAllBodyValues", json_bool b) :: args
442
+
in
443
+
let args = match max_body_value_bytes with
444
+
| None -> args
445
+
| Some n -> ("maxBodyValueBytes", json_int n) :: args
446
+
in
447
+
let inv = Invocation.create
448
+
~name:"Email/get"
449
+
~arguments:(json_obj args)
450
+
~method_call_id:call_id
451
+
in
452
+
record_invocation inv state;
453
+
Email_get_handle { call_id; method_name = "Email/get" }
454
+
455
+
let email_changes ~account_id ~since_state ?max_changes () state =
456
+
let call_id = fresh_call_id state in
457
+
let args = build_changes_args ~account_id ~since_state ?max_changes () in
458
+
let inv = Invocation.create
459
+
~name:"Email/changes"
460
+
~arguments:(json_obj args)
461
+
~method_call_id:call_id
462
+
in
463
+
record_invocation inv state;
464
+
Changes_handle { call_id; method_name = "Email/changes" }
465
+
466
+
let email_query_changes ~account_id ~since_query_state ?filter ?sort
467
+
?max_changes ?up_to_id ?calculate_total () state =
468
+
let call_id = fresh_call_id state in
469
+
let args = build_query_changes_args ~account_id ~since_query_state
470
+
?filter ~filter_jsont:Mail_filter.email_filter_jsont
471
+
?sort ?max_changes ?up_to_id ?calculate_total () in
472
+
let inv = Invocation.create
473
+
~name:"Email/queryChanges"
474
+
~arguments:(json_obj args)
475
+
~method_call_id:call_id
476
+
in
477
+
record_invocation inv state;
478
+
Query_changes_handle { call_id; method_name = "Email/queryChanges" }
479
+
480
+
let email_set ~account_id ?if_in_state ?create ?update ?destroy () state =
481
+
let call_id = fresh_call_id state in
482
+
let args = build_set_args ~account_id ?if_in_state ?create ?update ?destroy () in
483
+
let inv = Invocation.create
484
+
~name:"Email/set"
485
+
~arguments:(json_obj args)
486
+
~method_call_id:call_id
487
+
in
488
+
record_invocation inv state;
489
+
Email_set_handle { call_id; method_name = "Email/set" }
490
+
491
+
let email_copy ~from_account_id ~account_id ?if_from_in_state ?if_in_state
492
+
?create ?on_success_destroy_original ?destroy_from_if_in_state () state =
493
+
let call_id = fresh_call_id state in
494
+
let args = [
495
+
("fromAccountId", json_of_id from_account_id);
496
+
("accountId", json_of_id account_id);
497
+
] in
498
+
let args = match if_from_in_state with
499
+
| None -> args
500
+
| Some s -> ("ifFromInState", json_string s) :: args
501
+
in
502
+
let args = match if_in_state with
503
+
| None -> args
504
+
| Some s -> ("ifInState", json_string s) :: args
505
+
in
506
+
let args = match create with
507
+
| None | Some [] -> args
508
+
| Some items ->
509
+
let create_map = json_map (List.map (fun (id, obj) -> (Id.to_string id, obj)) items) in
510
+
("create", create_map) :: args
511
+
in
512
+
let args = match on_success_destroy_original with
513
+
| None -> args
514
+
| Some b -> ("onSuccessDestroyOriginal", json_bool b) :: args
515
+
in
516
+
let args = match destroy_from_if_in_state with
517
+
| None -> args
518
+
| Some s -> ("destroyFromIfInState", json_string s) :: args
519
+
in
520
+
let inv = Invocation.create
521
+
~name:"Email/copy"
522
+
~arguments:(json_obj args)
523
+
~method_call_id:call_id
524
+
in
525
+
record_invocation inv state;
526
+
Email_copy_handle { call_id; method_name = "Email/copy" }
527
+
528
+
let thread_get ~account_id ?ids () state =
529
+
let call_id = fresh_call_id state in
530
+
let args = [ ("accountId", json_of_id account_id) ] in
531
+
let args = add_ids_arg args ids in
532
+
let inv = Invocation.create
533
+
~name:"Thread/get"
534
+
~arguments:(json_obj args)
535
+
~method_call_id:call_id
536
+
in
537
+
record_invocation inv state;
538
+
Thread_get_handle { call_id; method_name = "Thread/get" }
539
+
540
+
let thread_changes ~account_id ~since_state ?max_changes () state =
541
+
let call_id = fresh_call_id state in
542
+
let args = build_changes_args ~account_id ~since_state ?max_changes () in
543
+
let inv = Invocation.create
544
+
~name:"Thread/changes"
545
+
~arguments:(json_obj args)
546
+
~method_call_id:call_id
547
+
in
548
+
record_invocation inv state;
549
+
Changes_handle { call_id; method_name = "Thread/changes" }
550
+
551
+
let mailbox_query ~account_id ?filter ?sort ?position ?anchor ?anchor_offset
552
+
?limit ?calculate_total () state =
553
+
let call_id = fresh_call_id state in
554
+
let args = build_query_args ~account_id ?filter
555
+
~filter_jsont:Mail_filter.mailbox_filter_jsont
556
+
?sort ?position ?anchor ?anchor_offset ?limit ?calculate_total () in
557
+
let inv = Invocation.create
558
+
~name:"Mailbox/query"
559
+
~arguments:(json_obj args)
560
+
~method_call_id:call_id
561
+
in
562
+
record_invocation inv state;
563
+
Query_handle { call_id; method_name = "Mailbox/query" }
564
+
565
+
let mailbox_get ~account_id ?ids ?properties () state =
566
+
let call_id = fresh_call_id state in
567
+
let args = [ ("accountId", json_of_id account_id) ] in
568
+
let args = add_ids_arg args ids in
569
+
let args = match properties with
570
+
| None -> args
571
+
| Some props -> ("properties", json_of_string_list props) :: args
572
+
in
573
+
let inv = Invocation.create
574
+
~name:"Mailbox/get"
575
+
~arguments:(json_obj args)
576
+
~method_call_id:call_id
577
+
in
578
+
record_invocation inv state;
579
+
Mailbox_get_handle { call_id; method_name = "Mailbox/get" }
580
+
581
+
let mailbox_changes ~account_id ~since_state ?max_changes () state =
582
+
let call_id = fresh_call_id state in
583
+
let args = build_changes_args ~account_id ~since_state ?max_changes () in
584
+
let inv = Invocation.create
585
+
~name:"Mailbox/changes"
586
+
~arguments:(json_obj args)
587
+
~method_call_id:call_id
588
+
in
589
+
record_invocation inv state;
590
+
Changes_handle { call_id; method_name = "Mailbox/changes" }
591
+
592
+
let mailbox_query_changes ~account_id ~since_query_state ?filter ?sort
593
+
?max_changes ?up_to_id ?calculate_total () state =
594
+
let call_id = fresh_call_id state in
595
+
let args = build_query_changes_args ~account_id ~since_query_state
596
+
?filter ~filter_jsont:Mail_filter.mailbox_filter_jsont
597
+
?sort ?max_changes ?up_to_id ?calculate_total () in
598
+
let inv = Invocation.create
599
+
~name:"Mailbox/queryChanges"
600
+
~arguments:(json_obj args)
601
+
~method_call_id:call_id
602
+
in
603
+
record_invocation inv state;
604
+
Query_changes_handle { call_id; method_name = "Mailbox/queryChanges" }
605
+
606
+
let mailbox_set ~account_id ?if_in_state ?create ?update ?destroy
607
+
?on_destroy_remove_emails () state =
608
+
let call_id = fresh_call_id state in
609
+
let args = build_set_args ~account_id ?if_in_state ?create ?update ?destroy () in
610
+
let args = match on_destroy_remove_emails with
611
+
| None -> args
612
+
| Some b -> ("onDestroyRemoveEmails", json_bool b) :: args
613
+
in
614
+
let inv = Invocation.create
615
+
~name:"Mailbox/set"
616
+
~arguments:(json_obj args)
617
+
~method_call_id:call_id
618
+
in
619
+
record_invocation inv state;
620
+
Mailbox_set_handle { call_id; method_name = "Mailbox/set" }
621
+
622
+
let identity_get ~account_id ?ids ?properties () state =
623
+
let call_id = fresh_call_id state in
624
+
let args = [ ("accountId", json_of_id account_id) ] in
625
+
let args = add_ids_arg args ids in
626
+
let args = match properties with
627
+
| None -> args
628
+
| Some props -> ("properties", json_of_string_list props) :: args
629
+
in
630
+
let inv = Invocation.create
631
+
~name:"Identity/get"
632
+
~arguments:(json_obj args)
633
+
~method_call_id:call_id
634
+
in
635
+
record_invocation inv state;
636
+
Identity_get_handle { call_id; method_name = "Identity/get" }
637
+
638
+
let identity_changes ~account_id ~since_state ?max_changes () state =
639
+
let call_id = fresh_call_id state in
640
+
let args = build_changes_args ~account_id ~since_state ?max_changes () in
641
+
let inv = Invocation.create
642
+
~name:"Identity/changes"
643
+
~arguments:(json_obj args)
644
+
~method_call_id:call_id
645
+
in
646
+
record_invocation inv state;
647
+
Changes_handle { call_id; method_name = "Identity/changes" }
648
+
649
+
let identity_set ~account_id ?if_in_state ?create ?update ?destroy () state =
650
+
let call_id = fresh_call_id state in
651
+
let args = build_set_args ~account_id ?if_in_state ?create ?update ?destroy () in
652
+
let inv = Invocation.create
653
+
~name:"Identity/set"
654
+
~arguments:(json_obj args)
655
+
~method_call_id:call_id
656
+
in
657
+
record_invocation inv state;
658
+
Identity_set_handle { call_id; method_name = "Identity/set" }
659
+
660
+
let email_submission_query ~account_id ?filter ?sort ?position ?anchor
661
+
?anchor_offset ?limit ?calculate_total () state =
662
+
let call_id = fresh_call_id state in
663
+
let args = build_query_args ~account_id ?filter
664
+
~filter_jsont:Mail_filter.submission_filter_jsont
665
+
?sort ?position ?anchor ?anchor_offset ?limit ?calculate_total () in
666
+
let inv = Invocation.create
667
+
~name:"EmailSubmission/query"
668
+
~arguments:(json_obj args)
669
+
~method_call_id:call_id
670
+
in
671
+
record_invocation inv state;
672
+
Query_handle { call_id; method_name = "EmailSubmission/query" }
673
+
674
+
let email_submission_get ~account_id ?ids ?properties () state =
675
+
let call_id = fresh_call_id state in
676
+
let args = [ ("accountId", json_of_id account_id) ] in
677
+
let args = add_ids_arg args ids in
678
+
let args = match properties with
679
+
| None -> args
680
+
| Some props -> ("properties", json_of_string_list props) :: args
681
+
in
682
+
let inv = Invocation.create
683
+
~name:"EmailSubmission/get"
684
+
~arguments:(json_obj args)
685
+
~method_call_id:call_id
686
+
in
687
+
record_invocation inv state;
688
+
Submission_get_handle { call_id; method_name = "EmailSubmission/get" }
689
+
690
+
let email_submission_changes ~account_id ~since_state ?max_changes () state =
691
+
let call_id = fresh_call_id state in
692
+
let args = build_changes_args ~account_id ~since_state ?max_changes () in
693
+
let inv = Invocation.create
694
+
~name:"EmailSubmission/changes"
695
+
~arguments:(json_obj args)
696
+
~method_call_id:call_id
697
+
in
698
+
record_invocation inv state;
699
+
Changes_handle { call_id; method_name = "EmailSubmission/changes" }
700
+
701
+
let email_submission_query_changes ~account_id ~since_query_state ?filter ?sort
702
+
?max_changes ?up_to_id ?calculate_total () state =
703
+
let call_id = fresh_call_id state in
704
+
let args = build_query_changes_args ~account_id ~since_query_state
705
+
?filter ~filter_jsont:Mail_filter.submission_filter_jsont
706
+
?sort ?max_changes ?up_to_id ?calculate_total () in
707
+
let inv = Invocation.create
708
+
~name:"EmailSubmission/queryChanges"
709
+
~arguments:(json_obj args)
710
+
~method_call_id:call_id
711
+
in
712
+
record_invocation inv state;
713
+
Query_changes_handle { call_id; method_name = "EmailSubmission/queryChanges" }
714
+
715
+
let email_submission_set ~account_id ?if_in_state ?create ?update ?destroy
716
+
?on_success_update_email ?on_success_destroy_email () state =
717
+
let call_id = fresh_call_id state in
718
+
let args = build_set_args ~account_id ?if_in_state ?create ?update ?destroy () in
719
+
let args = match on_success_update_email with
720
+
| None | Some [] -> args
721
+
| Some items ->
722
+
let update_map = json_map items in
723
+
("onSuccessUpdateEmail", update_map) :: args
724
+
in
725
+
let args = match on_success_destroy_email with
726
+
| None | Some [] -> args
727
+
| Some ids ->
728
+
("onSuccessDestroyEmail", json_of_string_list ids) :: args
729
+
in
730
+
let inv = Invocation.create
731
+
~name:"EmailSubmission/set"
732
+
~arguments:(json_obj args)
733
+
~method_call_id:call_id
734
+
in
735
+
record_invocation inv state;
736
+
Submission_set_handle { call_id; method_name = "EmailSubmission/set" }
737
+
738
+
let search_snippet_get ~account_id ~filter ~email_ids () state =
739
+
let call_id = fresh_call_id state in
740
+
let args = [ ("accountId", json_of_id account_id) ] in
741
+
let args = ("filter", encode_to_json Mail_filter.email_filter_jsont filter) :: args in
742
+
let args = match email_ids with
743
+
| Ids ids -> ("emailIds", json_of_id_list ids) :: args
744
+
| Ref ref_ ->
745
+
let ref_json = encode_to_json Invocation.result_reference_jsont ref_ in
746
+
("#emailIds", ref_json) :: args
747
+
in
748
+
let inv = Invocation.create
749
+
~name:"SearchSnippet/get"
750
+
~arguments:(json_obj args)
751
+
~method_call_id:call_id
752
+
in
753
+
record_invocation inv state;
754
+
Search_snippet_get_handle { call_id; method_name = "SearchSnippet/get" }
755
+
756
+
let vacation_response_get ~account_id ?properties () state =
757
+
let call_id = fresh_call_id state in
758
+
let args = [ ("accountId", json_of_id account_id) ] in
759
+
let args = match properties with
760
+
| None -> args
761
+
| Some props -> ("properties", json_of_string_list props) :: args
762
+
in
763
+
let inv = Invocation.create
764
+
~name:"VacationResponse/get"
765
+
~arguments:(json_obj args)
766
+
~method_call_id:call_id
767
+
in
768
+
record_invocation inv state;
769
+
Vacation_get_handle { call_id; method_name = "VacationResponse/get" }
770
+
771
+
let vacation_response_set ~account_id ?if_in_state ~update () state =
772
+
let call_id = fresh_call_id state in
773
+
let args = [ ("accountId", json_of_id account_id) ] in
774
+
let args = match if_in_state with
775
+
| None -> args
776
+
| Some s -> ("ifInState", json_string s) :: args
777
+
in
778
+
let args = ("update", json_map [("singleton", update)]) :: args in
779
+
let inv = Invocation.create
780
+
~name:"VacationResponse/set"
781
+
~arguments:(json_obj args)
782
+
~method_call_id:call_id
783
+
in
784
+
record_invocation inv state;
785
+
Vacation_set_handle { call_id; method_name = "VacationResponse/set" }
786
+
787
+
let raw_invocation ~name ~arguments state =
788
+
let call_id = fresh_call_id state in
789
+
let inv = Invocation.create
790
+
~name
791
+
~arguments
792
+
~method_call_id:call_id
793
+
in
794
+
record_invocation inv state;
795
+
Raw_handle { call_id; method_name = name }
796
+
797
+
(* Response parsing *)
798
+
799
+
let find_invocation ~call_id response =
800
+
List.find_opt
801
+
(fun inv -> Invocation.method_call_id inv = call_id)
802
+
(Response.method_responses response)
803
+
804
+
let parse : type k r. (k, r) handle -> Response.t -> (r, Jsont.Error.t) result =
805
+
fun handle response ->
806
+
let cid = call_id handle in
807
+
match find_invocation ~call_id:cid response with
808
+
| None ->
809
+
Error (Jsont.Error.msgf Jsont.Meta.none "No response found for call_id: %s" cid)
810
+
| Some inv ->
811
+
let args = Invocation.arguments inv in
812
+
match handle with
813
+
| Query_handle _ ->
814
+
Jsont.Json.decode' Method.query_response_jsont args
815
+
| Query_changes_handle _ ->
816
+
Jsont.Json.decode' Method.query_changes_response_jsont args
817
+
| Email_get_handle _ ->
818
+
Jsont.Json.decode' (Method.get_response_jsont Email.jsont) args
819
+
| Thread_get_handle _ ->
820
+
Jsont.Json.decode' (Method.get_response_jsont Thread.jsont) args
821
+
| Mailbox_get_handle _ ->
822
+
Jsont.Json.decode' (Method.get_response_jsont Mailbox.jsont) args
823
+
| Identity_get_handle _ ->
824
+
Jsont.Json.decode' (Method.get_response_jsont Identity.jsont) args
825
+
| Submission_get_handle _ ->
826
+
Jsont.Json.decode' (Method.get_response_jsont Submission.jsont) args
827
+
| Search_snippet_get_handle _ ->
828
+
Jsont.Json.decode' (Method.get_response_jsont Search_snippet.jsont) args
829
+
| Vacation_get_handle _ ->
830
+
Jsont.Json.decode' (Method.get_response_jsont Vacation.jsont) args
831
+
| Changes_handle _ ->
832
+
Jsont.Json.decode' Method.changes_response_jsont args
833
+
| Email_set_handle _ ->
834
+
Jsont.Json.decode' (Method.set_response_jsont Email.jsont) args
835
+
| Mailbox_set_handle _ ->
836
+
Jsont.Json.decode' (Method.set_response_jsont Mailbox.jsont) args
837
+
| Identity_set_handle _ ->
838
+
Jsont.Json.decode' (Method.set_response_jsont Identity.jsont) args
839
+
| Submission_set_handle _ ->
840
+
Jsont.Json.decode' (Method.set_response_jsont Submission.jsont) args
841
+
| Vacation_set_handle _ ->
842
+
Jsont.Json.decode' (Method.set_response_jsont Vacation.jsont) args
843
+
| Email_copy_handle _ ->
844
+
Jsont.Json.decode' (Method.copy_response_jsont Email.jsont) args
845
+
| Raw_handle _ ->
846
+
Ok args
847
+
848
+
let parse_exn handle response =
849
+
match parse handle response with
850
+
| Ok r -> r
851
+
| Error e -> failwith (Jsont.Error.to_string e)
+556
lib/core/chain.mli
+556
lib/core/chain.mli
···
1
+
(*---------------------------------------------------------------------------
2
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3
+
SPDX-License-Identifier: ISC
4
+
---------------------------------------------------------------------------*)
5
+
6
+
(** JMAP method chaining with automatic result references.
7
+
8
+
This module provides a monadic interface for building JMAP requests
9
+
where method calls can reference results from previous calls in the
10
+
same request. Call IDs are generated automatically.
11
+
12
+
{2 Basic Example}
13
+
14
+
Query for emails and fetch their details in a single request:
15
+
{[
16
+
let open Jmap.Chain in
17
+
let request, emails = build ~capabilities:[core; mail] begin
18
+
let* query = email_query ~account_id
19
+
~filter:(Condition { in_mailbox = Some inbox_id; _ })
20
+
~limit:50L ()
21
+
in
22
+
let* emails = email_get ~account_id
23
+
~ids:(from_query query)
24
+
~properties:["subject"; "from"; "receivedAt"]
25
+
()
26
+
in
27
+
return emails
28
+
end in
29
+
match Client.request client request with
30
+
| Ok response ->
31
+
let emails = parse emails response in
32
+
...
33
+
]}
34
+
35
+
{2 Creation and Submission}
36
+
37
+
Create a draft email and submit it in one request:
38
+
{[
39
+
let* set_h, draft_cid = email_set ~account_id
40
+
~create:[email_create ~mailbox_ids:[drafts_id] ~subject:"Hello" ...]
41
+
()
42
+
in
43
+
let* _ = email_submission_set ~account_id
44
+
~create:[submission_create
45
+
~email_id:(created_id draft_cid)
46
+
~identity_id]
47
+
()
48
+
in
49
+
return set_h
50
+
]}
51
+
52
+
{2 Multi-step Chains}
53
+
54
+
The RFC 8620 example - fetch from/date/subject for all emails in
55
+
the first 10 threads in the inbox:
56
+
{[
57
+
let* q = email_query ~account_id
58
+
~filter:(Condition { in_mailbox = Some inbox_id; _ })
59
+
~sort:[comparator ~is_ascending:false "receivedAt"]
60
+
~collapse_threads:true ~limit:10L ()
61
+
in
62
+
let* e1 = email_get ~account_id
63
+
~ids:(from_query q)
64
+
~properties:["threadId"]
65
+
()
66
+
in
67
+
let* threads = thread_get ~account_id
68
+
~ids:(from_get_field e1 "threadId")
69
+
()
70
+
in
71
+
let* e2 = email_get ~account_id
72
+
~ids:(from_get_field threads "emailIds")
73
+
~properties:["from"; "receivedAt"; "subject"]
74
+
()
75
+
in
76
+
return e2
77
+
]} *)
78
+
79
+
(** {1 Handles}
80
+
81
+
Method invocations return handles that encode both the method kind
82
+
(for building result references) and the exact response type
83
+
(for type-safe parsing). *)
84
+
85
+
(** Phantom type for query method handles. *)
86
+
type query
87
+
88
+
(** Phantom type for get method handles. *)
89
+
type get
90
+
91
+
(** Phantom type for changes method handles. *)
92
+
type changes
93
+
94
+
(** Phantom type for set method handles. *)
95
+
type set
96
+
97
+
(** Phantom type for query_changes method handles. *)
98
+
type query_changes
99
+
100
+
(** Phantom type for copy method handles. *)
101
+
type copy
102
+
103
+
(** Phantom type for import method handles. *)
104
+
type import
105
+
106
+
(** Phantom type for parse method handles. *)
107
+
type parse
108
+
109
+
(** A handle to a method invocation.
110
+
111
+
The first type parameter indicates the method kind (query/get/changes/set/...),
112
+
used for building result references. The second type parameter is the
113
+
parsed response type, enabling type-safe parsing via {!parse}. *)
114
+
type (_, _) handle
115
+
116
+
val call_id : (_, _) handle -> string
117
+
(** [call_id h] returns the auto-generated call ID for this invocation. *)
118
+
119
+
val method_name : (_, _) handle -> string
120
+
(** [method_name h] returns the method name (e.g., "Email/query"). *)
121
+
122
+
(** {1 Creation IDs}
123
+
124
+
When creating objects via [/set] methods, you can reference the
125
+
server-assigned ID before the request completes using creation IDs. *)
126
+
127
+
type 'a create_id
128
+
(** A creation ID for an object of type ['a]. Used to reference
129
+
newly created objects within the same request. *)
130
+
131
+
val created_id : _ create_id -> Jmap_proto.Id.t
132
+
(** [created_id cid] returns a placeholder ID (["#cN"]) that the server
133
+
will substitute with the real ID. Use this to reference a created
134
+
object in subsequent method calls within the same request. *)
135
+
136
+
val created_id_of_string : string -> Jmap_proto.Id.t
137
+
(** [created_id_of_string s] returns a placeholder ID for a string creation ID.
138
+
For example, [created_id_of_string "draft1"] returns ["#draft1"]. *)
139
+
140
+
(** {1 ID Sources}
141
+
142
+
Methods that accept IDs can take them either as concrete values
143
+
or as references to results from previous method calls. *)
144
+
145
+
type id_source =
146
+
| Ids of Jmap_proto.Id.t list
147
+
(** Concrete list of IDs. *)
148
+
| Ref of Jmap_proto.Invocation.result_reference
149
+
(** Back-reference to a previous method's result. *)
150
+
151
+
val ids : Jmap_proto.Id.t list -> id_source
152
+
(** [ids lst] provides concrete IDs. *)
153
+
154
+
val id : Jmap_proto.Id.t -> id_source
155
+
(** [id x] provides a single concrete ID. *)
156
+
157
+
(** {2 References from Query} *)
158
+
159
+
val from_query : (query, _) handle -> id_source
160
+
(** [from_query h] references [/ids] from a query response. *)
161
+
162
+
(** {2 References from Get} *)
163
+
164
+
val from_get_ids : (get, _) handle -> id_source
165
+
(** [from_get_ids h] references [/list/*/id] from a get response. *)
166
+
167
+
val from_get_field : (get, _) handle -> string -> id_source
168
+
(** [from_get_field h field] references [/list/*/field] from a get response.
169
+
Common fields: ["threadId"], ["emailIds"], ["mailboxIds"]. *)
170
+
171
+
(** {2 References from Changes} *)
172
+
173
+
val from_changes_created : (changes, _) handle -> id_source
174
+
(** [from_changes_created h] references [/created] from a changes response. *)
175
+
176
+
val from_changes_updated : (changes, _) handle -> id_source
177
+
(** [from_changes_updated h] references [/updated] from a changes response. *)
178
+
179
+
val from_changes_destroyed : (changes, _) handle -> id_source
180
+
(** [from_changes_destroyed h] references [/destroyed] from a changes response. *)
181
+
182
+
(** {2 References from Set} *)
183
+
184
+
val from_set_created : (set, _) handle -> id_source
185
+
(** [from_set_created h] references [/created/*/id] - IDs of objects created
186
+
by a set operation. *)
187
+
188
+
val from_set_updated : (set, _) handle -> id_source
189
+
(** [from_set_updated h] references [/updated] - IDs of objects updated. *)
190
+
191
+
(** {2 References from QueryChanges} *)
192
+
193
+
val from_query_changes_removed : (query_changes, _) handle -> id_source
194
+
(** [from_query_changes_removed h] references [/removed] from queryChanges. *)
195
+
196
+
val from_query_changes_added : (query_changes, _) handle -> id_source
197
+
(** [from_query_changes_added h] references [/added/*/id] from queryChanges. *)
198
+
199
+
(** {2 References from Copy} *)
200
+
201
+
val from_copy_created : (copy, _) handle -> id_source
202
+
(** [from_copy_created h] references [/created/*/id] from copy response. *)
203
+
204
+
(** {2 References from Import} *)
205
+
206
+
val from_import_created : (import, _) handle -> id_source
207
+
(** [from_import_created h] references [/created/*/id] from import response. *)
208
+
209
+
(** {1 Chain Monad}
210
+
211
+
A monad for building JMAP requests with automatic call ID generation
212
+
and invocation collection. *)
213
+
214
+
type 'a t
215
+
(** A chain computation that produces ['a] (typically a handle). *)
216
+
217
+
val return : 'a -> 'a t
218
+
(** [return x] is a computation that produces [x] without adding any
219
+
method invocations. *)
220
+
221
+
val bind : 'a t -> ('a -> 'b t) -> 'b t
222
+
(** [bind m f] sequences computations, threading the chain state. *)
223
+
224
+
val map : ('a -> 'b) -> 'a t -> 'b t
225
+
(** [map f m] applies [f] to the result of [m]. *)
226
+
227
+
val both : 'a t -> 'b t -> ('a * 'b) t
228
+
(** [both a b] runs both computations, returning their results as a pair. *)
229
+
230
+
(** {2 Syntax} *)
231
+
232
+
val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t
233
+
val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t
234
+
val ( and* ) : 'a t -> 'b t -> ('a * 'b) t
235
+
val ( and+ ) : 'a t -> 'b t -> ('a * 'b) t
236
+
237
+
(** {1 Building Requests} *)
238
+
239
+
val build :
240
+
capabilities:string list ->
241
+
'a t ->
242
+
Jmap_proto.Request.t * 'a
243
+
(** [build ~capabilities chain] runs the chain computation, returning
244
+
the JMAP request and the final value (typically a handle for parsing). *)
245
+
246
+
val build_request :
247
+
capabilities:string list ->
248
+
'a t ->
249
+
Jmap_proto.Request.t
250
+
(** [build_request ~capabilities chain] is like {!build} but discards
251
+
the final value. *)
252
+
253
+
(** {1 Method Builders}
254
+
255
+
Each builder returns a handle wrapped in the chain monad.
256
+
Call IDs are assigned automatically based on invocation order. *)
257
+
258
+
(** {2 Email Methods} *)
259
+
260
+
val email_query :
261
+
account_id:Jmap_proto.Id.t ->
262
+
?filter:Jmap_proto.Mail_filter.email_filter ->
263
+
?sort:Jmap_proto.Filter.comparator list ->
264
+
?position:int64 ->
265
+
?anchor:Jmap_proto.Id.t ->
266
+
?anchor_offset:int64 ->
267
+
?limit:int64 ->
268
+
?calculate_total:bool ->
269
+
?collapse_threads:bool ->
270
+
unit ->
271
+
(query, Jmap_proto.Method.query_response) handle t
272
+
273
+
val email_get :
274
+
account_id:Jmap_proto.Id.t ->
275
+
?ids:id_source ->
276
+
?properties:string list ->
277
+
?body_properties:string list ->
278
+
?fetch_text_body_values:bool ->
279
+
?fetch_html_body_values:bool ->
280
+
?fetch_all_body_values:bool ->
281
+
?max_body_value_bytes:int64 ->
282
+
unit ->
283
+
(get, Jmap_proto.Email.t Jmap_proto.Method.get_response) handle t
284
+
285
+
val email_changes :
286
+
account_id:Jmap_proto.Id.t ->
287
+
since_state:string ->
288
+
?max_changes:int64 ->
289
+
unit ->
290
+
(changes, Jmap_proto.Method.changes_response) handle t
291
+
292
+
val email_query_changes :
293
+
account_id:Jmap_proto.Id.t ->
294
+
since_query_state:string ->
295
+
?filter:Jmap_proto.Mail_filter.email_filter ->
296
+
?sort:Jmap_proto.Filter.comparator list ->
297
+
?max_changes:int64 ->
298
+
?up_to_id:Jmap_proto.Id.t ->
299
+
?calculate_total:bool ->
300
+
unit ->
301
+
(query_changes, Jmap_proto.Method.query_changes_response) handle t
302
+
303
+
val email_set :
304
+
account_id:Jmap_proto.Id.t ->
305
+
?if_in_state:string ->
306
+
?create:(string * Jsont.Json.t) list ->
307
+
?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
308
+
?destroy:id_source ->
309
+
unit ->
310
+
(set, Jmap_proto.Email.t Jmap_proto.Method.set_response) handle t
311
+
(** Build an Email/set invocation.
312
+
313
+
[create] is a list of [(creation_id, email_object)] pairs where
314
+
[creation_id] is a client-chosen string (e.g., "draft1") and
315
+
[email_object] is the JSON representation of the email to create.
316
+
317
+
Use {!created_id_of_string} to reference created objects in later calls. *)
318
+
319
+
val email_copy :
320
+
from_account_id:Jmap_proto.Id.t ->
321
+
account_id:Jmap_proto.Id.t ->
322
+
?if_from_in_state:string ->
323
+
?if_in_state:string ->
324
+
?create:(Jmap_proto.Id.t * Jsont.Json.t) list ->
325
+
?on_success_destroy_original:bool ->
326
+
?destroy_from_if_in_state:string ->
327
+
unit ->
328
+
(copy, Jmap_proto.Email.t Jmap_proto.Method.copy_response) handle t
329
+
(** Build an Email/copy invocation.
330
+
331
+
[create] maps source email IDs to override objects. The source email
332
+
is copied to the target account with any overridden properties. *)
333
+
334
+
(** {2 Thread Methods} *)
335
+
336
+
val thread_get :
337
+
account_id:Jmap_proto.Id.t ->
338
+
?ids:id_source ->
339
+
unit ->
340
+
(get, Jmap_proto.Thread.t Jmap_proto.Method.get_response) handle t
341
+
342
+
val thread_changes :
343
+
account_id:Jmap_proto.Id.t ->
344
+
since_state:string ->
345
+
?max_changes:int64 ->
346
+
unit ->
347
+
(changes, Jmap_proto.Method.changes_response) handle t
348
+
349
+
(** {2 Mailbox Methods} *)
350
+
351
+
val mailbox_query :
352
+
account_id:Jmap_proto.Id.t ->
353
+
?filter:Jmap_proto.Mail_filter.mailbox_filter ->
354
+
?sort:Jmap_proto.Filter.comparator list ->
355
+
?position:int64 ->
356
+
?anchor:Jmap_proto.Id.t ->
357
+
?anchor_offset:int64 ->
358
+
?limit:int64 ->
359
+
?calculate_total:bool ->
360
+
unit ->
361
+
(query, Jmap_proto.Method.query_response) handle t
362
+
363
+
val mailbox_get :
364
+
account_id:Jmap_proto.Id.t ->
365
+
?ids:id_source ->
366
+
?properties:string list ->
367
+
unit ->
368
+
(get, Jmap_proto.Mailbox.t Jmap_proto.Method.get_response) handle t
369
+
370
+
val mailbox_changes :
371
+
account_id:Jmap_proto.Id.t ->
372
+
since_state:string ->
373
+
?max_changes:int64 ->
374
+
unit ->
375
+
(changes, Jmap_proto.Method.changes_response) handle t
376
+
377
+
val mailbox_query_changes :
378
+
account_id:Jmap_proto.Id.t ->
379
+
since_query_state:string ->
380
+
?filter:Jmap_proto.Mail_filter.mailbox_filter ->
381
+
?sort:Jmap_proto.Filter.comparator list ->
382
+
?max_changes:int64 ->
383
+
?up_to_id:Jmap_proto.Id.t ->
384
+
?calculate_total:bool ->
385
+
unit ->
386
+
(query_changes, Jmap_proto.Method.query_changes_response) handle t
387
+
388
+
val mailbox_set :
389
+
account_id:Jmap_proto.Id.t ->
390
+
?if_in_state:string ->
391
+
?create:(string * Jsont.Json.t) list ->
392
+
?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
393
+
?destroy:id_source ->
394
+
?on_destroy_remove_emails:bool ->
395
+
unit ->
396
+
(set, Jmap_proto.Mailbox.t Jmap_proto.Method.set_response) handle t
397
+
398
+
(** {2 Identity Methods} *)
399
+
400
+
val identity_get :
401
+
account_id:Jmap_proto.Id.t ->
402
+
?ids:id_source ->
403
+
?properties:string list ->
404
+
unit ->
405
+
(get, Jmap_proto.Identity.t Jmap_proto.Method.get_response) handle t
406
+
407
+
val identity_changes :
408
+
account_id:Jmap_proto.Id.t ->
409
+
since_state:string ->
410
+
?max_changes:int64 ->
411
+
unit ->
412
+
(changes, Jmap_proto.Method.changes_response) handle t
413
+
414
+
val identity_set :
415
+
account_id:Jmap_proto.Id.t ->
416
+
?if_in_state:string ->
417
+
?create:(string * Jsont.Json.t) list ->
418
+
?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
419
+
?destroy:id_source ->
420
+
unit ->
421
+
(set, Jmap_proto.Identity.t Jmap_proto.Method.set_response) handle t
422
+
423
+
(** {2 EmailSubmission Methods} *)
424
+
425
+
val email_submission_query :
426
+
account_id:Jmap_proto.Id.t ->
427
+
?filter:Jmap_proto.Mail_filter.submission_filter ->
428
+
?sort:Jmap_proto.Filter.comparator list ->
429
+
?position:int64 ->
430
+
?anchor:Jmap_proto.Id.t ->
431
+
?anchor_offset:int64 ->
432
+
?limit:int64 ->
433
+
?calculate_total:bool ->
434
+
unit ->
435
+
(query, Jmap_proto.Method.query_response) handle t
436
+
437
+
val email_submission_get :
438
+
account_id:Jmap_proto.Id.t ->
439
+
?ids:id_source ->
440
+
?properties:string list ->
441
+
unit ->
442
+
(get, Jmap_proto.Submission.t Jmap_proto.Method.get_response) handle t
443
+
444
+
val email_submission_changes :
445
+
account_id:Jmap_proto.Id.t ->
446
+
since_state:string ->
447
+
?max_changes:int64 ->
448
+
unit ->
449
+
(changes, Jmap_proto.Method.changes_response) handle t
450
+
451
+
val email_submission_query_changes :
452
+
account_id:Jmap_proto.Id.t ->
453
+
since_query_state:string ->
454
+
?filter:Jmap_proto.Mail_filter.submission_filter ->
455
+
?sort:Jmap_proto.Filter.comparator list ->
456
+
?max_changes:int64 ->
457
+
?up_to_id:Jmap_proto.Id.t ->
458
+
?calculate_total:bool ->
459
+
unit ->
460
+
(query_changes, Jmap_proto.Method.query_changes_response) handle t
461
+
462
+
val email_submission_set :
463
+
account_id:Jmap_proto.Id.t ->
464
+
?if_in_state:string ->
465
+
?create:(string * Jsont.Json.t) list ->
466
+
?update:(Jmap_proto.Id.t * Jsont.Json.t) list ->
467
+
?destroy:id_source ->
468
+
?on_success_update_email:(string * Jsont.Json.t) list ->
469
+
?on_success_destroy_email:string list ->
470
+
unit ->
471
+
(set, Jmap_proto.Submission.t Jmap_proto.Method.set_response) handle t
472
+
(** Build an EmailSubmission/set invocation.
473
+
474
+
[on_success_update_email] and [on_success_destroy_email] take creation IDs
475
+
(like ["#draft1"]) or real email IDs to update/destroy the email after
476
+
successful submission. *)
477
+
478
+
(** {2 SearchSnippet Methods} *)
479
+
480
+
val search_snippet_get :
481
+
account_id:Jmap_proto.Id.t ->
482
+
filter:Jmap_proto.Mail_filter.email_filter ->
483
+
email_ids:id_source ->
484
+
unit ->
485
+
(get, Jmap_proto.Search_snippet.t Jmap_proto.Method.get_response) handle t
486
+
(** Build a SearchSnippet/get invocation. Note that the filter must match
487
+
the filter used in the Email/query that produced the email IDs. *)
488
+
489
+
(** {2 VacationResponse Methods} *)
490
+
491
+
val vacation_response_get :
492
+
account_id:Jmap_proto.Id.t ->
493
+
?properties:string list ->
494
+
unit ->
495
+
(get, Jmap_proto.Vacation.t Jmap_proto.Method.get_response) handle t
496
+
497
+
val vacation_response_set :
498
+
account_id:Jmap_proto.Id.t ->
499
+
?if_in_state:string ->
500
+
update:Jsont.Json.t ->
501
+
unit ->
502
+
(set, Jmap_proto.Vacation.t Jmap_proto.Method.set_response) handle t
503
+
(** VacationResponse is a singleton - you can only update "singleton". *)
504
+
505
+
(** {1 Response Parsing} *)
506
+
507
+
val parse :
508
+
(_, 'resp) handle ->
509
+
Jmap_proto.Response.t ->
510
+
('resp, Jsont.Error.t) result
511
+
(** [parse handle response] extracts and parses the response for [handle].
512
+
513
+
The response type is determined by the handle's type parameter,
514
+
providing compile-time type safety. *)
515
+
516
+
val parse_exn : (_, 'resp) handle -> Jmap_proto.Response.t -> 'resp
517
+
(** [parse_exn handle response] is like {!parse} but raises on error. *)
518
+
519
+
(** {1 JSON Helpers}
520
+
521
+
Convenience functions for building JSON patch objects for /set methods. *)
522
+
523
+
val json_null : Jsont.Json.t
524
+
(** A JSON null value. Use to unset a property. *)
525
+
526
+
val json_bool : bool -> Jsont.Json.t
527
+
(** [json_bool b] creates a JSON boolean. *)
528
+
529
+
val json_string : string -> Jsont.Json.t
530
+
(** [json_string s] creates a JSON string. *)
531
+
532
+
val json_int : int64 -> Jsont.Json.t
533
+
(** [json_int n] creates a JSON number from an int64. *)
534
+
535
+
val json_obj : (string * Jsont.Json.t) list -> Jsont.Json.t
536
+
(** [json_obj fields] creates a JSON object from key-value pairs. *)
537
+
538
+
val json_array : Jsont.Json.t list -> Jsont.Json.t
539
+
(** [json_array items] creates a JSON array. *)
540
+
541
+
(** {1 Creation ID Helpers} *)
542
+
543
+
val fresh_create_id : unit -> 'a create_id t
544
+
(** [fresh_create_id ()] generates a fresh creation ID within the chain.
545
+
The ID is unique within the request. *)
546
+
547
+
(** {1 Low-Level Access}
548
+
549
+
For users who need direct access to the underlying invocation. *)
550
+
551
+
val raw_invocation :
552
+
name:string ->
553
+
arguments:Jsont.Json.t ->
554
+
(unit, Jsont.Json.t) handle t
555
+
(** [raw_invocation ~name ~arguments] adds a raw method invocation.
556
+
Use this for methods not yet supported by the high-level API. *)
+4
-61
lib/core/jmap.ml
+4
-61
lib/core/jmap.ml
···
428
428
val to_string : t -> (string, Error.t) result
429
429
end
430
430
431
-
(** {1 Private Interface} *)
431
+
(** {1 Request Chaining} *)
432
432
433
-
(** Private module for internal use by Jmap_eio.
433
+
(** JMAP method chaining with automatic result references.
434
434
435
-
This exposes the underlying Jsont codecs for serialization. *)
436
-
module Private = struct
437
-
module Session = struct
438
-
let jsont = Proto.Session.jsont
439
-
end
440
-
441
-
module Request = struct
442
-
let jsont = Proto.Request.jsont
443
-
end
444
-
445
-
module Response = struct
446
-
let jsont = Proto.Response.jsont
447
-
end
448
-
449
-
module Mailbox = struct
450
-
let jsont = Proto.Mailbox.jsont
451
-
end
452
-
453
-
module Email = struct
454
-
let jsont = Proto.Email.jsont
455
-
end
456
-
457
-
module Thread = struct
458
-
let jsont = Proto.Thread.jsont
459
-
end
460
-
461
-
module Identity = struct
462
-
let jsont = Proto.Identity.jsont
463
-
end
464
-
465
-
module Submission = struct
466
-
let jsont = Proto.Submission.jsont
467
-
end
468
-
469
-
module Vacation = struct
470
-
let jsont = Proto.Vacation.jsont
471
-
end
472
-
473
-
module Blob = struct
474
-
let upload_response_jsont = Proto.Blob.upload_response_jsont
475
-
end
476
-
477
-
module Method = struct
478
-
let get_response_jsont = Proto.Method.get_response_jsont
479
-
let query_response_jsont = Proto.Method.query_response_jsont
480
-
let changes_response_jsont = Proto.Method.changes_response_jsont
481
-
let set_response_jsont = Proto.Method.set_response_jsont
482
-
end
483
-
484
-
module Mail_filter = struct
485
-
let email_filter_jsont = Proto.Mail_filter.email_filter_jsont
486
-
let mailbox_filter_jsont = Proto.Mail_filter.mailbox_filter_jsont
487
-
let submission_filter_jsont = Proto.Mail_filter.submission_filter_jsont
488
-
end
489
-
490
-
module Filter = struct
491
-
let comparator_jsont = Proto.Filter.comparator_jsont
492
-
end
493
-
end
435
+
See {!Chain} for the full interface. *)
436
+
module Chain = Chain
+5
-61
lib/core/jmap.mli
+5
-61
lib/core/jmap.mli
···
453
453
val to_string : t -> (string, Error.t) result
454
454
end
455
455
456
-
(** {1 Private Interface} *)
457
-
458
-
(** Private module for internal use by Jmap_eio.
459
-
460
-
This exposes the underlying Jsont codecs for serialization. *)
461
-
module Private : sig
462
-
module Session : sig
463
-
val jsont : Proto.Session.t Jsont.t
464
-
end
465
-
466
-
module Request : sig
467
-
val jsont : Proto.Request.t Jsont.t
468
-
end
469
-
470
-
module Response : sig
471
-
val jsont : Proto.Response.t Jsont.t
472
-
end
473
-
474
-
module Mailbox : sig
475
-
val jsont : Proto.Mailbox.t Jsont.t
476
-
end
477
-
478
-
module Email : sig
479
-
val jsont : Proto.Email.t Jsont.t
480
-
end
481
-
482
-
module Thread : sig
483
-
val jsont : Proto.Thread.t Jsont.t
484
-
end
485
-
486
-
module Identity : sig
487
-
val jsont : Proto.Identity.t Jsont.t
488
-
end
456
+
(** {1 Request Chaining} *)
489
457
490
-
module Submission : sig
491
-
val jsont : Proto.Submission.t Jsont.t
492
-
end
458
+
(** JMAP method chaining with automatic result references.
493
459
494
-
module Vacation : sig
495
-
val jsont : Proto.Vacation.t Jsont.t
496
-
end
497
-
498
-
module Blob : sig
499
-
val upload_response_jsont : Proto.Blob.upload_response Jsont.t
500
-
end
501
-
502
-
module Method : sig
503
-
val get_response_jsont : 'a Jsont.t -> 'a Proto.Method.get_response Jsont.t
504
-
val query_response_jsont : Proto.Method.query_response Jsont.t
505
-
val changes_response_jsont : Proto.Method.changes_response Jsont.t
506
-
val set_response_jsont : 'a Jsont.t -> 'a Proto.Method.set_response Jsont.t
507
-
end
508
-
509
-
module Mail_filter : sig
510
-
val email_filter_jsont : Proto.Mail_filter.email_filter Jsont.t
511
-
val mailbox_filter_jsont : Proto.Mail_filter.mailbox_filter Jsont.t
512
-
val submission_filter_jsont : Proto.Mail_filter.submission_filter Jsont.t
513
-
end
514
-
515
-
module Filter : sig
516
-
val comparator_jsont : Proto.Filter.comparator Jsont.t
517
-
end
518
-
end
460
+
This module provides a monadic interface for building JMAP requests
461
+
where method calls can reference results from previous calls. *)
462
+
module Chain = Chain