+1
-1
AGENT.md
+1
-1
AGENT.md
···
22
22
Note: There is a compilation issue with the current ezjsonm package on the system.
23
23
3. DONE Add a `Jmap_mail` implementation that follows `spec/rfc8621.txt` as part of a
24
24
separate package. It should use the Jmap module and extend it appropriately.
25
-
4. Complete the Jmap_mail implementation so that there are functions to login
25
+
4. DONE Complete the Jmap_mail implementation so that there are functions to login
26
26
and list mailboxes and messages in a mailbox.
+404
-12
lib/jmap_mail.ml
+404
-12
lib/jmap_mail.ml
···
887
887
888
888
(** {1 API functions} *)
889
889
890
-
(** TODO:claude - Need to implement API functions for interacting with the
891
-
mail-specific JMAP server endpoints. These would use the Jmap.Api module
892
-
to make HTTP requests and parse responses.
890
+
open Lwt.Syntax
891
+
open Jmap.Api
892
+
open Jmap.Types
893
+
894
+
(** Authentication credentials for a JMAP server *)
895
+
type credentials = {
896
+
username: string;
897
+
password: string;
898
+
}
899
+
900
+
(** Connection to a JMAP mail server *)
901
+
type connection = {
902
+
session: Jmap.Types.session;
903
+
config: Jmap.Api.config;
904
+
}
905
+
906
+
(** Convert JSON mail object to OCaml type *)
907
+
let mailbox_of_json json =
908
+
try
909
+
let open Ezjsonm in
910
+
let id = get_string (find json ["id"]) in
911
+
let name = get_string (find json ["name"]) in
912
+
let parent_id = find_opt json ["parentId"] |> Option.map get_string in
913
+
let role = find_opt json ["role"] |> Option.map (fun r -> Json.mailbox_role_of_string (get_string r)) in
914
+
let sort_order = get_int (find json ["sortOrder"]) in
915
+
let total_emails = get_int (find json ["totalEmails"]) in
916
+
let unread_emails = get_int (find json ["unreadEmails"]) in
917
+
let total_threads = get_int (find json ["totalThreads"]) in
918
+
let unread_threads = get_int (find json ["unreadThreads"]) in
919
+
let is_subscribed = get_bool (find json ["isSubscribed"]) in
893
920
894
-
For a complete implementation, we would need functions for:
895
-
- Mailbox operations (get, query, changes, update)
896
-
- Thread operations
897
-
- Email operations (get, query, changes, update, import, copy)
898
-
- Search operations
899
-
- Mail submission
900
-
- Identity management
901
-
- Vacation response management
902
-
*)
921
+
let rights_json = find json ["myRights"] in
922
+
let my_rights = {
923
+
Types.may_read_items = get_bool (find rights_json ["mayReadItems"]);
924
+
may_add_items = get_bool (find rights_json ["mayAddItems"]);
925
+
may_remove_items = get_bool (find rights_json ["mayRemoveItems"]);
926
+
may_set_seen = get_bool (find rights_json ["maySetSeen"]);
927
+
may_set_keywords = get_bool (find rights_json ["maySetKeywords"]);
928
+
may_create_child = get_bool (find rights_json ["mayCreateChild"]);
929
+
may_rename = get_bool (find rights_json ["mayRename"]);
930
+
may_delete = get_bool (find rights_json ["mayDelete"]);
931
+
may_submit = get_bool (find rights_json ["maySubmit"]);
932
+
} in
933
+
934
+
Ok ({
935
+
Types.id;
936
+
name;
937
+
parent_id;
938
+
role;
939
+
sort_order;
940
+
total_emails;
941
+
unread_emails;
942
+
total_threads;
943
+
unread_threads;
944
+
is_subscribed;
945
+
my_rights;
946
+
})
947
+
with
948
+
| Not_found -> Error (Parse_error "Required field not found in mailbox object")
949
+
| Invalid_argument msg -> Error (Parse_error msg)
950
+
| e -> Error (Parse_error (Printexc.to_string e))
951
+
952
+
(** Convert JSON email object to OCaml type *)
953
+
let email_of_json json =
954
+
try
955
+
let open Ezjsonm in
956
+
let id = get_string (find json ["id"]) in
957
+
let blob_id = get_string (find json ["blobId"]) in
958
+
let thread_id = get_string (find json ["threadId"]) in
959
+
960
+
(* Process mailboxIds map *)
961
+
let mailbox_ids_json = find json ["mailboxIds"] in
962
+
let mailbox_ids = match mailbox_ids_json with
963
+
| `O items -> List.map (fun (id, v) -> (id, get_bool v)) items
964
+
| _ -> raise (Invalid_argument "mailboxIds is not an object")
965
+
in
966
+
967
+
(* Process keywords map *)
968
+
let keywords_json = find json ["keywords"] in
969
+
let keywords = match keywords_json with
970
+
| `O items -> List.map (fun (k, v) ->
971
+
(Json.keyword_of_string k, get_bool v)) items
972
+
| _ -> raise (Invalid_argument "keywords is not an object")
973
+
in
974
+
975
+
let size = get_int (find json ["size"]) in
976
+
let received_at = get_string (find json ["receivedAt"]) in
977
+
let message_id = match find json ["messageId"] with
978
+
| `A ids -> List.map (fun id -> get_string id) ids
979
+
| _ -> raise (Invalid_argument "messageId is not an array")
980
+
in
981
+
982
+
(* Parse optional fields *)
983
+
let parse_email_addresses opt_json =
984
+
match opt_json with
985
+
| Some (`A items) ->
986
+
Some (List.map (fun addr_json ->
987
+
let name = find_opt addr_json ["name"] |> Option.map get_string in
988
+
let email = get_string (find addr_json ["email"]) in
989
+
let parameters = match find_opt addr_json ["parameters"] with
990
+
| Some (`O items) -> List.map (fun (k, v) -> (k, get_string v)) items
991
+
| _ -> []
992
+
in
993
+
{ Types.name; email; parameters }
994
+
) items)
995
+
| _ -> None
996
+
in
997
+
998
+
let in_reply_to = find_opt json ["inReplyTo"] |> Option.map (function
999
+
| `A ids -> List.map get_string ids
1000
+
| _ -> []
1001
+
) in
1002
+
1003
+
let references = find_opt json ["references"] |> Option.map (function
1004
+
| `A ids -> List.map get_string ids
1005
+
| _ -> []
1006
+
) in
1007
+
1008
+
let sender = parse_email_addresses (find_opt json ["sender"]) in
1009
+
let from = parse_email_addresses (find_opt json ["from"]) in
1010
+
let to_ = parse_email_addresses (find_opt json ["to"]) in
1011
+
let cc = parse_email_addresses (find_opt json ["cc"]) in
1012
+
let bcc = parse_email_addresses (find_opt json ["bcc"]) in
1013
+
let reply_to = parse_email_addresses (find_opt json ["replyTo"]) in
1014
+
1015
+
let subject = find_opt json ["subject"] |> Option.map get_string in
1016
+
let sent_at = find_opt json ["sentAt"] |> Option.map get_string in
1017
+
let has_attachment = find_opt json ["hasAttachment"] |> Option.map get_bool in
1018
+
let preview = find_opt json ["preview"] |> Option.map get_string in
1019
+
1020
+
(* Body parts parsing would go here - omitting for brevity *)
1021
+
1022
+
Ok ({
1023
+
Types.id;
1024
+
blob_id;
1025
+
thread_id;
1026
+
mailbox_ids;
1027
+
keywords;
1028
+
size;
1029
+
received_at;
1030
+
message_id;
1031
+
in_reply_to;
1032
+
references;
1033
+
sender;
1034
+
from;
1035
+
to_;
1036
+
cc;
1037
+
bcc;
1038
+
reply_to;
1039
+
subject;
1040
+
sent_at;
1041
+
has_attachment;
1042
+
preview;
1043
+
body_values = None;
1044
+
text_body = None;
1045
+
html_body = None;
1046
+
attachments = None;
1047
+
headers = None;
1048
+
})
1049
+
with
1050
+
| Not_found -> Error (Parse_error "Required field not found in email object")
1051
+
| Invalid_argument msg -> Error (Parse_error msg)
1052
+
| e -> Error (Parse_error (Printexc.to_string e))
1053
+
1054
+
(** Login to a JMAP server and establish a connection
1055
+
@param uri The URI of the JMAP server
1056
+
@param credentials Authentication credentials
1057
+
@return A connection object if successful
1058
+
1059
+
TODO:claude *)
1060
+
let login ~uri ~credentials =
1061
+
let* session_result = get_session (Uri.of_string uri)
1062
+
~username:credentials.username
1063
+
~authentication_token:credentials.password
1064
+
() in
1065
+
match session_result with
1066
+
| Ok session ->
1067
+
let api_uri = Uri.of_string session.api_url in
1068
+
let config = {
1069
+
api_uri;
1070
+
username = credentials.username;
1071
+
authentication_token = credentials.password;
1072
+
} in
1073
+
Lwt.return (Ok { session; config })
1074
+
| Error e -> Lwt.return (Error e)
1075
+
1076
+
(** Get all mailboxes for an account
1077
+
@param conn The JMAP connection
1078
+
@param account_id The account ID to get mailboxes for
1079
+
@return A list of mailboxes if successful
1080
+
1081
+
TODO:claude *)
1082
+
let get_mailboxes conn ~account_id =
1083
+
let request = {
1084
+
using = ["urn:ietf:params:jmap:core"; Types.capability_mail];
1085
+
method_calls = [
1086
+
{
1087
+
name = "Mailbox/get";
1088
+
arguments = `O [
1089
+
("accountId", `String account_id);
1090
+
];
1091
+
method_call_id = "m1";
1092
+
}
1093
+
];
1094
+
created_ids = None;
1095
+
} in
1096
+
1097
+
let* response_result = make_request conn.config request in
1098
+
match response_result with
1099
+
| Ok response ->
1100
+
let result =
1101
+
try
1102
+
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1103
+
inv.name = "Mailbox/get") response.method_responses in
1104
+
let args = method_response.arguments in
1105
+
match Ezjsonm.find_opt args ["list"] with
1106
+
| Some (`A mailbox_list) ->
1107
+
let parse_results = List.map mailbox_of_json mailbox_list in
1108
+
let (successes, failures) = List.partition Result.is_ok parse_results in
1109
+
if List.length failures > 0 then
1110
+
Error (Parse_error "Failed to parse some mailboxes")
1111
+
else
1112
+
Ok (List.map Result.get_ok successes)
1113
+
| _ -> Error (Parse_error "Mailbox list not found in response")
1114
+
with
1115
+
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
1116
+
| e -> Error (Parse_error (Printexc.to_string e))
1117
+
in
1118
+
Lwt.return result
1119
+
| Error e -> Lwt.return (Error e)
1120
+
1121
+
(** Get a specific mailbox by ID
1122
+
@param conn The JMAP connection
1123
+
@param account_id The account ID
1124
+
@param mailbox_id The mailbox ID to retrieve
1125
+
@return The mailbox if found
1126
+
1127
+
TODO:claude *)
1128
+
let get_mailbox conn ~account_id ~mailbox_id =
1129
+
let request = {
1130
+
using = ["urn:ietf:params:jmap:core"; Types.capability_mail];
1131
+
method_calls = [
1132
+
{
1133
+
name = "Mailbox/get";
1134
+
arguments = `O [
1135
+
("accountId", `String account_id);
1136
+
("ids", `A [`String mailbox_id]);
1137
+
];
1138
+
method_call_id = "m1";
1139
+
}
1140
+
];
1141
+
created_ids = None;
1142
+
} in
1143
+
1144
+
let* response_result = make_request conn.config request in
1145
+
match response_result with
1146
+
| Ok response ->
1147
+
let result =
1148
+
try
1149
+
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1150
+
inv.name = "Mailbox/get") response.method_responses in
1151
+
let args = method_response.arguments in
1152
+
match Ezjsonm.find_opt args ["list"] with
1153
+
| Some (`A [mailbox]) -> mailbox_of_json mailbox
1154
+
| Some (`A []) -> Error (Parse_error ("Mailbox not found: " ^ mailbox_id))
1155
+
| _ -> Error (Parse_error "Expected single mailbox in response")
1156
+
with
1157
+
| Not_found -> Error (Parse_error "Mailbox/get method response not found")
1158
+
| e -> Error (Parse_error (Printexc.to_string e))
1159
+
in
1160
+
Lwt.return result
1161
+
| Error e -> Lwt.return (Error e)
1162
+
1163
+
(** Get messages in a mailbox
1164
+
@param conn The JMAP connection
1165
+
@param account_id The account ID
1166
+
@param mailbox_id The mailbox ID to get messages from
1167
+
@param limit Optional limit on number of messages to return
1168
+
@return The list of email messages if successful
1169
+
1170
+
TODO:claude *)
1171
+
let get_messages_in_mailbox conn ~account_id ~mailbox_id ?limit () =
1172
+
(* First query the emails in the mailbox *)
1173
+
let query_request = {
1174
+
using = ["urn:ietf:params:jmap:core"; Types.capability_mail];
1175
+
method_calls = [
1176
+
{
1177
+
name = "Email/query";
1178
+
arguments = `O ([
1179
+
("accountId", `String account_id);
1180
+
("filter", `O [("inMailbox", `String mailbox_id)]);
1181
+
("sort", `A [`O [("property", `String "receivedAt"); ("isAscending", `Bool false)]]);
1182
+
] @ (match limit with
1183
+
| Some l -> [("limit", `Float (float_of_int l))]
1184
+
| None -> []
1185
+
));
1186
+
method_call_id = "q1";
1187
+
}
1188
+
];
1189
+
created_ids = None;
1190
+
} in
1191
+
1192
+
let* query_result = make_request conn.config query_request in
1193
+
match query_result with
1194
+
| Ok query_response ->
1195
+
(try
1196
+
let query_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1197
+
inv.name = "Email/query") query_response.method_responses in
1198
+
let args = query_method.arguments in
1199
+
match Ezjsonm.find_opt args ["ids"] with
1200
+
| Some (`A ids) ->
1201
+
let email_ids = List.map (function
1202
+
| `String id -> id
1203
+
| _ -> raise (Invalid_argument "Email ID is not a string")
1204
+
) ids in
1205
+
1206
+
(* If we have IDs, fetch the actual email objects *)
1207
+
if List.length email_ids > 0 then
1208
+
let get_request = {
1209
+
using = ["urn:ietf:params:jmap:core"; Types.capability_mail];
1210
+
method_calls = [
1211
+
{
1212
+
name = "Email/get";
1213
+
arguments = `O [
1214
+
("accountId", `String account_id);
1215
+
("ids", `A (List.map (fun id -> `String id) email_ids));
1216
+
];
1217
+
method_call_id = "g1";
1218
+
}
1219
+
];
1220
+
created_ids = None;
1221
+
} in
1222
+
1223
+
let* get_result = make_request conn.config get_request in
1224
+
match get_result with
1225
+
| Ok get_response ->
1226
+
(try
1227
+
let get_method = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1228
+
inv.name = "Email/get") get_response.method_responses in
1229
+
let args = get_method.arguments in
1230
+
match Ezjsonm.find_opt args ["list"] with
1231
+
| Some (`A email_list) ->
1232
+
let parse_results = List.map email_of_json email_list in
1233
+
let (successes, failures) = List.partition Result.is_ok parse_results in
1234
+
if List.length failures > 0 then
1235
+
Lwt.return (Error (Parse_error "Failed to parse some emails"))
1236
+
else
1237
+
Lwt.return (Ok (List.map Result.get_ok successes))
1238
+
| _ -> Lwt.return (Error (Parse_error "Email list not found in response"))
1239
+
with
1240
+
| Not_found -> Lwt.return (Error (Parse_error "Email/get method response not found"))
1241
+
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
1242
+
| Error e -> Lwt.return (Error e)
1243
+
else
1244
+
(* No emails in mailbox *)
1245
+
Lwt.return (Ok [])
1246
+
1247
+
| _ -> Lwt.return (Error (Parse_error "Email IDs not found in query response"))
1248
+
with
1249
+
| Not_found -> Lwt.return (Error (Parse_error "Email/query method response not found"))
1250
+
| Invalid_argument msg -> Lwt.return (Error (Parse_error msg))
1251
+
| e -> Lwt.return (Error (Parse_error (Printexc.to_string e))))
1252
+
| Error e -> Lwt.return (Error e)
1253
+
1254
+
(** Get a single email message by ID
1255
+
@param conn The JMAP connection
1256
+
@param account_id The account ID
1257
+
@param email_id The email ID to retrieve
1258
+
@return The email message if found
1259
+
1260
+
TODO:claude *)
1261
+
let get_email conn ~account_id ~email_id =
1262
+
let request = {
1263
+
using = ["urn:ietf:params:jmap:core"; Types.capability_mail];
1264
+
method_calls = [
1265
+
{
1266
+
name = "Email/get";
1267
+
arguments = `O [
1268
+
("accountId", `String account_id);
1269
+
("ids", `A [`String email_id]);
1270
+
];
1271
+
method_call_id = "m1";
1272
+
}
1273
+
];
1274
+
created_ids = None;
1275
+
} in
1276
+
1277
+
let* response_result = make_request conn.config request in
1278
+
match response_result with
1279
+
| Ok response ->
1280
+
let result =
1281
+
try
1282
+
let method_response = List.find (fun (inv : Ezjsonm.value Jmap.Types.invocation) ->
1283
+
inv.name = "Email/get") response.method_responses in
1284
+
let args = method_response.arguments in
1285
+
match Ezjsonm.find_opt args ["list"] with
1286
+
| Some (`A [email]) -> email_of_json email
1287
+
| Some (`A []) -> Error (Parse_error ("Email not found: " ^ email_id))
1288
+
| _ -> Error (Parse_error "Expected single email in response")
1289
+
with
1290
+
| Not_found -> Error (Parse_error "Email/get method response not found")
1291
+
| e -> Error (Parse_error (Printexc.to_string e))
1292
+
in
1293
+
Lwt.return result
1294
+
| Error e -> Lwt.return (Error e)
+72
-7
lib/jmap_mail.mli
+72
-7
lib/jmap_mail.mli
···
843
843
844
844
(** {1 API functions} *)
845
845
846
-
(** TODO:claude - Need to implement API functions for interacting with the
847
-
mail-specific JMAP server endpoints. These would use the Jmap.Api module
848
-
to make HTTP requests and parse responses.
846
+
(** Authentication credentials for a JMAP server *)
847
+
type credentials = {
848
+
username: string;
849
+
password: string;
850
+
}
851
+
852
+
(** Connection to a JMAP mail server *)
853
+
type connection = {
854
+
session: Jmap.Types.session;
855
+
config: Jmap.Api.config;
856
+
}
857
+
858
+
(** Login to a JMAP server and establish a connection
859
+
@param uri The URI of the JMAP server
860
+
@param credentials Authentication credentials
861
+
@return A connection object if successful
862
+
863
+
TODO:claude *)
864
+
val login :
865
+
uri:string ->
866
+
credentials:credentials ->
867
+
(connection, Jmap.Api.error) result Lwt.t
868
+
869
+
(** Get all mailboxes for an account
870
+
@param conn The JMAP connection
871
+
@param account_id The account ID to get mailboxes for
872
+
@return A list of mailboxes if successful
873
+
874
+
TODO:claude *)
875
+
val get_mailboxes :
876
+
connection ->
877
+
account_id:Jmap.Types.id ->
878
+
(Types.mailbox list, Jmap.Api.error) result Lwt.t
879
+
880
+
(** Get a specific mailbox by ID
881
+
@param conn The JMAP connection
882
+
@param account_id The account ID
883
+
@param mailbox_id The mailbox ID to retrieve
884
+
@return The mailbox if found
849
885
850
-
The interface would include functions like:
886
+
TODO:claude *)
887
+
val get_mailbox :
888
+
connection ->
889
+
account_id:Jmap.Types.id ->
890
+
mailbox_id:Jmap.Types.id ->
891
+
(Types.mailbox, Jmap.Api.error) result Lwt.t
892
+
893
+
(** Get messages in a mailbox
894
+
@param conn The JMAP connection
895
+
@param account_id The account ID
896
+
@param mailbox_id The mailbox ID to get messages from
897
+
@param limit Optional limit on number of messages to return
898
+
@return The list of email messages if successful
851
899
852
-
val get_mailboxes : Jmap.Api.session -> ?ids:id list -> ?properties:string list -> account_id:id -> (mailbox_get_response, error) result Lwt.t
900
+
TODO:claude *)
901
+
val get_messages_in_mailbox :
902
+
connection ->
903
+
account_id:Jmap.Types.id ->
904
+
mailbox_id:Jmap.Types.id ->
905
+
?limit:int ->
906
+
unit ->
907
+
(Types.email list, Jmap.Api.error) result Lwt.t
908
+
909
+
(** Get a single email message by ID
910
+
@param conn The JMAP connection
911
+
@param account_id The account ID
912
+
@param email_id The email ID to retrieve
913
+
@return The email message if found
853
914
854
-
And similarly for all other API operations.
855
-
*)
915
+
TODO:claude *)
916
+
val get_email :
917
+
connection ->
918
+
account_id:Jmap.Types.id ->
919
+
email_id:Jmap.Types.id ->
920
+
(Types.email, Jmap.Api.error) result Lwt.t