this repo has no description

move chain

Changed files
+1970 -122
bin
eio
lib
+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
··· 6 6 module Codec = Codec 7 7 module Client = Client 8 8 module Cli = Cli 9 + module Chain = Jmap.Chain
+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
··· 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
··· 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
··· 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
··· 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
+1
lib/dune
··· 8 8 ; Core unified interface 9 9 jmap 10 10 jmap_types 11 + chain 11 12 ; Protocol layer wrapper (combines core + mail) 12 13 jmap_proto 13 14 ; Core protocol modules