···11+(** Implementation of JMAP Email/import method (RFC 8621 Section 4.8) *)
22+33+(** {1 EmailImport Object} *)
44+55+type email_import = {
66+ blob_id : Jmap.Id.t;
77+ mailbox_ids : (Jmap.Id.t * bool) list;
88+ keywords : (string * bool) list;
99+ received_at : Jmap.Date.t option;
1010+}
1111+1212+let create_email_import ~blob_id ~mailbox_ids ?(keywords=[]) ?received_at () = {
1313+ blob_id;
1414+ mailbox_ids;
1515+ keywords;
1616+ received_at;
1717+}
1818+1919+(** JSON serialization for EmailImport objects *)
2020+let email_import_to_json ei =
2121+ let json_fields = [
2222+ ("blobId", `String (Jmap.Id.to_string ei.blob_id));
2323+ ("mailboxIds", `Assoc (List.map (fun (id, v) -> (Jmap.Id.to_string id, `Bool v)) ei.mailbox_ids));
2424+ ] in
2525+ let json_fields = if ei.keywords = [] then json_fields
2626+ else ("keywords", `Assoc (List.map (fun (k, v) -> (k, `Bool v)) ei.keywords)) :: json_fields
2727+ in
2828+ let json_fields = match ei.received_at with
2929+ | Some date -> ("receivedAt", `String (Jmap.Date.to_rfc3339 date)) :: json_fields
3030+ | None -> json_fields
3131+ in
3232+ `Assoc (List.rev json_fields)
3333+3434+let email_import_of_json json =
3535+ try
3636+ let open Yojson.Safe.Util in
3737+ let blob_id_str = json |> member "blobId" |> to_string in
3838+ let blob_id = match Jmap.Id.of_string blob_id_str with
3939+ | Ok id -> id
4040+ | Error _ -> failwith ("Invalid blobId: " ^ blob_id_str)
4141+ in
4242+ let mailbox_ids_assoc = json |> member "mailboxIds" |> to_assoc in
4343+ let mailbox_ids = List.map (fun (id_str, v) ->
4444+ let id = match Jmap.Id.of_string id_str with
4545+ | Ok id -> id
4646+ | Error _ -> failwith ("Invalid mailbox ID: " ^ id_str)
4747+ in
4848+ (id, to_bool v)
4949+ ) mailbox_ids_assoc in
5050+ let keywords = match json |> member "keywords" with
5151+ | `Null -> []
5252+ | keywords_json -> List.map (fun (k, v) -> (k, to_bool v)) (to_assoc keywords_json)
5353+ in
5454+ let received_at = match json |> member "receivedAt" with
5555+ | `Null -> None
5656+ | date_json -> match Jmap.Date.of_rfc3339 (to_string date_json) with
5757+ | Ok date -> Some date
5858+ | Error _ -> failwith "Invalid receivedAt date format"
5959+ in
6060+ Ok (create_email_import ~blob_id ~mailbox_ids ~keywords ?received_at ())
6161+ with
6262+ | exn -> Error ("Failed to parse EmailImport: " ^ Printexc.to_string exn)
6363+6464+(** {1 Email/import Arguments} *)
6565+6666+module Import_args = struct
6767+ type t = {
6868+ account_id : string;
6969+ if_in_state : string option;
7070+ emails : (string * email_import) list;
7171+ }
7272+7373+ let create ~account_id ?if_in_state ~emails () = {
7474+ account_id;
7575+ if_in_state;
7676+ emails;
7777+ }
7878+7979+ let account_id t = t.account_id
8080+ let if_in_state t = t.if_in_state
8181+ let emails t = t.emails
8282+8383+ let to_json t =
8484+ let json_fields = [
8585+ ("accountId", `String t.account_id);
8686+ ("emails", `Assoc (List.map (fun (creation_id, ei) -> (creation_id, email_import_to_json ei)) t.emails));
8787+ ] in
8888+ let json_fields = match t.if_in_state with
8989+ | Some state -> ("ifInState", `String state) :: json_fields
9090+ | None -> json_fields
9191+ in
9292+ `Assoc (List.rev json_fields)
9393+9494+ let of_json json =
9595+ try
9696+ let open Yojson.Safe.Util in
9797+ let account_id = json |> member "accountId" |> to_string in
9898+ let if_in_state = json |> member "ifInState" |> to_string_option in
9999+ let emails_assoc = json |> member "emails" |> to_assoc in
100100+ let emails = List.map (fun (creation_id, ei_json) ->
101101+ match email_import_of_json ei_json with
102102+ | Ok ei -> (creation_id, ei)
103103+ | Error err -> failwith err
104104+ ) emails_assoc in
105105+ Ok (create ~account_id ?if_in_state ~emails ())
106106+ with
107107+ | exn -> Error ("Failed to parse Email/import args: " ^ Printexc.to_string exn)
108108+end
109109+110110+(** {1 Email/import Response} *)
111111+112112+type email_creation_result = {
113113+ id : Jmap.Id.t;
114114+ blob_id : Jmap.Id.t;
115115+ thread_id : Jmap.Id.t;
116116+ size : int;
117117+}
118118+119119+let email_creation_result_to_json ecr =
120120+ `Assoc [
121121+ ("id", `String (Jmap.Id.to_string ecr.id));
122122+ ("blobId", `String (Jmap.Id.to_string ecr.blob_id));
123123+ ("threadId", `String (Jmap.Id.to_string ecr.thread_id));
124124+ ("size", `Int ecr.size);
125125+ ]
126126+127127+let email_creation_result_of_json json =
128128+ try
129129+ let open Yojson.Safe.Util in
130130+ let id_str = json |> member "id" |> to_string in
131131+ let id = match Jmap.Id.of_string id_str with
132132+ | Ok id -> id
133133+ | Error _ -> failwith ("Invalid id: " ^ id_str)
134134+ in
135135+ let blob_id_str = json |> member "blobId" |> to_string in
136136+ let blob_id = match Jmap.Id.of_string blob_id_str with
137137+ | Ok id -> id
138138+ | Error _ -> failwith ("Invalid blobId: " ^ blob_id_str)
139139+ in
140140+ let thread_id_str = json |> member "threadId" |> to_string in
141141+ let thread_id = match Jmap.Id.of_string thread_id_str with
142142+ | Ok id -> id
143143+ | Error _ -> failwith ("Invalid threadId: " ^ thread_id_str)
144144+ in
145145+ let size = json |> member "size" |> to_int in
146146+ Ok {id; blob_id; thread_id; size}
147147+ with
148148+ | exn -> Error ("Failed to parse EmailCreationResult: " ^ Printexc.to_string exn)
149149+150150+module Import_response = struct
151151+ type response = {
152152+ account_id : string;
153153+ old_state : string option;
154154+ new_state : string option;
155155+ created : (string * email_creation_result) list;
156156+ not_created : (string * Jmap.Error.Set_error.t) list;
157157+ }
158158+159159+ let create ~account_id ?old_state ?new_state ?(created=[]) ?(not_created=[]) () = {
160160+ account_id;
161161+ old_state;
162162+ new_state;
163163+ created;
164164+ not_created;
165165+ }
166166+167167+ let account_id t = t.account_id
168168+ let old_state t = t.old_state
169169+ let new_state t = t.new_state
170170+ let created t = t.created
171171+ let not_created t = t.not_created
172172+173173+ let to_json t =
174174+ let json_fields = [
175175+ ("accountId", `String t.account_id);
176176+ ] in
177177+ let json_fields = match t.old_state with
178178+ | Some state -> ("oldState", `String state) :: json_fields
179179+ | None -> json_fields
180180+ in
181181+ let json_fields = match t.new_state with
182182+ | Some state -> ("newState", `String state) :: json_fields
183183+ | None -> json_fields
184184+ in
185185+ let json_fields = if t.created = [] then
186186+ ("created", `Null) :: json_fields
187187+ else
188188+ ("created", `Assoc (List.map (fun (cid, ecr) -> (cid, email_creation_result_to_json ecr)) t.created)) :: json_fields
189189+ in
190190+ let json_fields = if t.not_created = [] then
191191+ ("notCreated", `Null) :: json_fields
192192+ else
193193+ ("notCreated", `Assoc (List.map (fun (cid, err) -> (cid, Jmap.Error.Set_error.to_json err)) t.not_created)) :: json_fields
194194+ in
195195+ `Assoc (List.rev json_fields)
196196+197197+ let of_json json =
198198+ try
199199+ let open Yojson.Safe.Util in
200200+ let account_id = json |> member "accountId" |> to_string in
201201+ let old_state = json |> member "oldState" |> to_string_option in
202202+ let new_state = json |> member "newState" |> to_string_option in
203203+ let created = match json |> member "created" with
204204+ | `Null -> []
205205+ | created_json -> List.map (fun (cid, ecr_json) ->
206206+ match email_creation_result_of_json ecr_json with
207207+ | Ok ecr -> (cid, ecr)
208208+ | Error err -> failwith err
209209+ ) (to_assoc created_json)
210210+ in
211211+ let not_created = match json |> member "notCreated" with
212212+ | `Null -> []
213213+ | not_created_json -> List.map (fun (cid, err_json) ->
214214+ match Jmap.Error.Set_error.of_json err_json with
215215+ | Ok err -> (cid, err)
216216+ | Error err_msg -> failwith err_msg
217217+ ) (to_assoc not_created_json)
218218+ in
219219+ Ok (create ~account_id ?old_state ?new_state ~created ~not_created ())
220220+ with
221221+ | exn -> Error ("Failed to parse Email/import response: " ^ Printexc.to_string exn)
222222+end
+129
jmap/jmap-email/email_import.mli
···11+(** JMAP Email/import method implementation as defined in RFC 8621 Section 4.8.
22+33+ The Email/import method adds messages (RFC 5322) to the set of Emails
44+ in an account. Messages must first be uploaded as blobs using the
55+ standard upload mechanism.
66+77+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.8> RFC 8621, Section 4.8 *)
88+99+(** {1 EmailImport Object} *)
1010+1111+(** An EmailImport object specifies how to import a single email from a blob.
1212+1313+ Each Email to import is considered an atomic unit that may succeed or
1414+ fail individually. Importing successfully creates a new Email object
1515+ from the data referenced by the blobId.
1616+1717+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.8> RFC 8621, Section 4.8 *)
1818+type email_import = {
1919+ blob_id : Jmap.Id.t; (** The blob containing the raw RFC 5322 message *)
2020+ mailbox_ids : (Jmap.Id.t * bool) list; (** Mailboxes to assign this Email to (at least one required) *)
2121+ keywords : (string * bool) list; (** Keywords to apply to the Email *)
2222+ received_at : Jmap.Date.t option; (** The receivedAt date (defaults to most recent Received header) *)
2323+}
2424+2525+(** Create an EmailImport object.
2626+ @param blob_id The blob containing the raw message
2727+ @param mailbox_ids List of (mailbox_id, true) pairs - at least one required
2828+ @param ?keywords Optional keywords to apply (defaults to empty)
2929+ @param ?received_at Optional received date (defaults to server calculation)
3030+ @return A new EmailImport object *)
3131+val create_email_import :
3232+ blob_id:Jmap.Id.t ->
3333+ mailbox_ids:(Jmap.Id.t * bool) list ->
3434+ ?keywords:(string * bool) list ->
3535+ ?received_at:Jmap.Date.t ->
3636+ unit ->
3737+ email_import
3838+3939+(** {1 Email/import Arguments} *)
4040+4141+(** Arguments for Email/import method calls.
4242+4343+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.8> RFC 8621, Section 4.8 *)
4444+module Import_args : sig
4545+ type t
4646+4747+ (** Create Email/import arguments.
4848+ @param account_id The account ID to use
4949+ @param ?if_in_state Optional state string for optimistic concurrency
5050+ @param emails Map of creation IDs to EmailImport objects
5151+ @return Email/import arguments object *)
5252+ val create :
5353+ account_id:string ->
5454+ ?if_in_state:string ->
5555+ emails:(string * email_import) list ->
5656+ unit ->
5757+ t
5858+5959+ (** Get the account ID.
6060+ @return The account ID for this request *)
6161+ val account_id : t -> string
6262+6363+ (** Get the if-in-state value.
6464+ @return The state string for optimistic concurrency, or None if not set *)
6565+ val if_in_state : t -> string option
6666+6767+ (** Get the emails to import.
6868+ @return List of (creation_id, email_import) pairs *)
6969+ val emails : t -> (string * email_import) list
7070+7171+ (** JSON serialization for Email/import arguments *)
7272+ include Jmap_sigs.JSONABLE with type t := t
7373+end
7474+7575+(** {1 Email/import Response} *)
7676+7777+(** Email creation result for successfully imported emails *)
7878+type email_creation_result = {
7979+ id : Jmap.Id.t; (** The new Email ID *)
8080+ blob_id : Jmap.Id.t; (** The blob ID of the raw message *)
8181+ thread_id : Jmap.Id.t; (** The Thread ID this Email belongs to *)
8282+ size : int; (** Size of the Email in octets *)
8383+}
8484+8585+(** Response for Email/import method calls.
8686+8787+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.8> RFC 8621, Section 4.8 *)
8888+module Import_response : sig
8989+ type response
9090+9191+ (** Create an Email/import response.
9292+ @param account_id The account ID used for the call
9393+ @param ?old_state The state before the changes (if applicable)
9494+ @param ?new_state The state after the changes (if applicable)
9595+ @param ?created Optional map of creation IDs to successfully created emails
9696+ @param ?not_created Optional map of creation IDs to SetError objects for failed creations
9797+ @return Email/import response object *)
9898+ val create :
9999+ account_id:string ->
100100+ ?old_state:string ->
101101+ ?new_state:string ->
102102+ ?created:(string * email_creation_result) list ->
103103+ ?not_created:(string * Jmap.Error.Set_error.t) list ->
104104+ unit ->
105105+ response
106106+107107+ (** Get the account ID.
108108+ @return The account ID used for the call *)
109109+ val account_id : response -> string
110110+111111+ (** Get the old state.
112112+ @return The state before changes, or None if not applicable *)
113113+ val old_state : response -> string option
114114+115115+ (** Get the new state.
116116+ @return The state after changes, or None if not applicable *)
117117+ val new_state : response -> string option
118118+119119+ (** Get the created emails.
120120+ @return Map of creation IDs to created email results, or empty list if none *)
121121+ val created : response -> (string * email_creation_result) list
122122+123123+ (** Get the not created emails.
124124+ @return Map of creation IDs to SetError objects for failed creations, or empty list if none *)
125125+ val not_created : response -> (string * Jmap.Error.Set_error.t) list
126126+127127+ (** JSON serialization for Email/import responses *)
128128+ include Jmap_sigs.JSONABLE with type t := response
129129+end
+166
jmap/jmap-email/email_parse.ml
···11+(** Implementation of JMAP Email/parse method (RFC 8621 Section 4.9) *)
22+33+(** {1 Email/parse Arguments} *)
44+55+module Parse_args = struct
66+ type t = {
77+ account_id : string;
88+ blob_ids : Jmap.Id.t list;
99+ properties : string list option;
1010+ body_properties : string list option;
1111+ fetch_text_body_values : bool;
1212+ fetch_html_body_values : bool;
1313+ fetch_all_body_values : bool;
1414+ max_body_value_bytes : int;
1515+ }
1616+1717+ let create ~account_id ~blob_ids ?properties ?body_properties
1818+ ?(fetch_text_body_values=false) ?(fetch_html_body_values=false)
1919+ ?(fetch_all_body_values=false) ?(max_body_value_bytes=0) () = {
2020+ account_id;
2121+ blob_ids;
2222+ properties;
2323+ body_properties;
2424+ fetch_text_body_values;
2525+ fetch_html_body_values;
2626+ fetch_all_body_values;
2727+ max_body_value_bytes;
2828+ }
2929+3030+ let account_id t = t.account_id
3131+ let blob_ids t = t.blob_ids
3232+ let properties t = t.properties
3333+ let body_properties t = t.body_properties
3434+ let fetch_text_body_values t = t.fetch_text_body_values
3535+ let fetch_html_body_values t = t.fetch_html_body_values
3636+ let fetch_all_body_values t = t.fetch_all_body_values
3737+ let max_body_value_bytes t = t.max_body_value_bytes
3838+3939+ let to_json t =
4040+ let json_fields = [
4141+ ("accountId", `String t.account_id);
4242+ ("blobIds", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.blob_ids));
4343+ ("fetchTextBodyValues", `Bool t.fetch_text_body_values);
4444+ ("fetchHTMLBodyValues", `Bool t.fetch_html_body_values);
4545+ ("fetchAllBodyValues", `Bool t.fetch_all_body_values);
4646+ ("maxBodyValueBytes", `Int t.max_body_value_bytes);
4747+ ] in
4848+ let json_fields = match t.properties with
4949+ | Some props -> ("properties", `List (List.map (fun p -> `String p) props)) :: json_fields
5050+ | None -> json_fields
5151+ in
5252+ let json_fields = match t.body_properties with
5353+ | Some props -> ("bodyProperties", `List (List.map (fun p -> `String p) props)) :: json_fields
5454+ | None -> json_fields
5555+ in
5656+ `Assoc (List.rev json_fields)
5757+5858+ let of_json json =
5959+ try
6060+ let open Yojson.Safe.Util in
6161+ let account_id = json |> member "accountId" |> to_string in
6262+ let blob_ids_json = json |> member "blobIds" |> to_list in
6363+ let blob_ids = List.map (fun id_json ->
6464+ let id_str = to_string id_json in
6565+ match Jmap.Id.of_string id_str with
6666+ | Ok id -> id
6767+ | Error _ -> failwith ("Invalid blob ID: " ^ id_str)
6868+ ) blob_ids_json in
6969+ let properties = match json |> member "properties" with
7070+ | `Null -> None
7171+ | props_json -> Some (List.map to_string (to_list props_json))
7272+ in
7373+ let body_properties = match json |> member "bodyProperties" with
7474+ | `Null -> None
7575+ | props_json -> Some (List.map to_string (to_list props_json))
7676+ in
7777+ let fetch_text_body_values = json |> member "fetchTextBodyValues" |> to_bool_option |> Option.value ~default:false in
7878+ let fetch_html_body_values = json |> member "fetchHTMLBodyValues" |> to_bool_option |> Option.value ~default:false in
7979+ let fetch_all_body_values = json |> member "fetchAllBodyValues" |> to_bool_option |> Option.value ~default:false in
8080+ let max_body_value_bytes = json |> member "maxBodyValueBytes" |> to_int_option |> Option.value ~default:0 in
8181+ Ok (create ~account_id ~blob_ids ?properties ?body_properties
8282+ ~fetch_text_body_values ~fetch_html_body_values
8383+ ~fetch_all_body_values ~max_body_value_bytes ())
8484+ with
8585+ | exn -> Error ("Failed to parse Email/parse args: " ^ Printexc.to_string exn)
8686+end
8787+8888+(** {1 Email/parse Response} *)
8989+9090+module Parse_response = struct
9191+ type response = {
9292+ account_id : string;
9393+ parsed : (Jmap.Id.t * Yojson.Safe.t) list; (* Map of blob IDs to Email objects *)
9494+ not_parsable : Jmap.Id.t list;
9595+ not_found : Jmap.Id.t list;
9696+ }
9797+9898+ let create ~account_id ?(parsed=[]) ?(not_parsable=[]) ?(not_found=[]) () = {
9999+ account_id;
100100+ parsed;
101101+ not_parsable;
102102+ not_found;
103103+ }
104104+105105+ let account_id t = t.account_id
106106+ let parsed t = t.parsed
107107+ let not_parsable t = t.not_parsable
108108+ let not_found t = t.not_found
109109+110110+ let to_json t =
111111+ let json_fields = [
112112+ ("accountId", `String t.account_id);
113113+ ] in
114114+ let json_fields = if t.parsed = [] then
115115+ ("parsed", `Null) :: json_fields
116116+ else
117117+ ("parsed", `Assoc (List.map (fun (id, email) -> (Jmap.Id.to_string id, email)) t.parsed)) :: json_fields
118118+ in
119119+ let json_fields = if t.not_parsable = [] then
120120+ ("notParsable", `Null) :: json_fields
121121+ else
122122+ ("notParsable", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.not_parsable)) :: json_fields
123123+ in
124124+ let json_fields = if t.not_found = [] then
125125+ ("notFound", `Null) :: json_fields
126126+ else
127127+ ("notFound", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.not_found)) :: json_fields
128128+ in
129129+ `Assoc (List.rev json_fields)
130130+131131+ let of_json json =
132132+ try
133133+ let open Yojson.Safe.Util in
134134+ let account_id = json |> member "accountId" |> to_string in
135135+ let parsed = match json |> member "parsed" with
136136+ | `Null -> []
137137+ | parsed_json -> List.map (fun (blob_id_str, email_json) ->
138138+ let blob_id = match Jmap.Id.of_string blob_id_str with
139139+ | Ok id -> id
140140+ | Error _ -> failwith ("Invalid blob ID in parsed: " ^ blob_id_str)
141141+ in
142142+ (blob_id, email_json)
143143+ ) (to_assoc parsed_json)
144144+ in
145145+ let not_parsable = match json |> member "notParsable" with
146146+ | `Null -> []
147147+ | ids_json -> List.map (fun id_json ->
148148+ let id_str = to_string id_json in
149149+ match Jmap.Id.of_string id_str with
150150+ | Ok id -> id
151151+ | Error _ -> failwith ("Invalid blob ID in notParsable: " ^ id_str)
152152+ ) (to_list ids_json)
153153+ in
154154+ let not_found = match json |> member "notFound" with
155155+ | `Null -> []
156156+ | ids_json -> List.map (fun id_json ->
157157+ let id_str = to_string id_json in
158158+ match Jmap.Id.of_string id_str with
159159+ | Ok id -> id
160160+ | Error _ -> failwith ("Invalid blob ID in notFound: " ^ id_str)
161161+ ) (to_list ids_json)
162162+ in
163163+ Ok (create ~account_id ~parsed ~not_parsable ~not_found ())
164164+ with
165165+ | exn -> Error ("Failed to parse Email/parse response: " ^ Printexc.to_string exn)
166166+end
+118
jmap/jmap-email/email_parse.mli
···11+(** JMAP Email/parse method implementation as defined in RFC 8621 Section 4.9.
22+33+ The Email/parse method allows parsing blobs as messages (RFC 5322) to
44+ get Email objects. This can be used to parse and display attached messages
55+ without importing them as top-level Email objects.
66+77+ Note: The following metadata properties will be null if requested:
88+ - id, mailboxIds, keywords, receivedAt
99+1010+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.9> RFC 8621, Section 4.9 *)
1111+1212+(** {1 Email/parse Arguments} *)
1313+1414+(** Arguments for Email/parse method calls.
1515+1616+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.9> RFC 8621, Section 4.9 *)
1717+module Parse_args : sig
1818+ type t
1919+2020+ (** Create Email/parse arguments.
2121+ @param account_id The account ID to use
2222+ @param blob_ids Array of blob IDs to parse
2323+ @param ?properties Optional list of properties to return for each Email (defaults to standard set)
2424+ @param ?body_properties Optional list of properties to fetch for each EmailBodyPart
2525+ @param ?fetch_text_body_values Whether to include text/* parts in bodyValues (default: false)
2626+ @param ?fetch_html_body_values Whether to include HTML parts in bodyValues (default: false)
2727+ @param ?fetch_all_body_values Whether to include all text/* parts in bodyValues (default: false)
2828+ @param ?max_body_value_bytes Maximum bytes for bodyValues (0 = no limit, default: 0)
2929+ @return Email/parse arguments object *)
3030+ val create :
3131+ account_id:string ->
3232+ blob_ids:Jmap.Id.t list ->
3333+ ?properties:string list ->
3434+ ?body_properties:string list ->
3535+ ?fetch_text_body_values:bool ->
3636+ ?fetch_html_body_values:bool ->
3737+ ?fetch_all_body_values:bool ->
3838+ ?max_body_value_bytes:int ->
3939+ unit ->
4040+ t
4141+4242+ (** Get the account ID.
4343+ @return The account ID for this request *)
4444+ val account_id : t -> string
4545+4646+ (** Get the blob IDs to parse.
4747+ @return List of blob IDs to parse *)
4848+ val blob_ids : t -> Jmap.Id.t list
4949+5050+ (** Get the properties list.
5151+ @return List of properties to return, or None for default *)
5252+ val properties : t -> string list option
5353+5454+ (** Get the body properties list.
5555+ @return List of body properties to fetch, or None for default *)
5656+ val body_properties : t -> string list option
5757+5858+ (** Get fetch text body values flag.
5959+ @return Whether to include text body values *)
6060+ val fetch_text_body_values : t -> bool
6161+6262+ (** Get fetch HTML body values flag.
6363+ @return Whether to include HTML body values *)
6464+ val fetch_html_body_values : t -> bool
6565+6666+ (** Get fetch all body values flag.
6767+ @return Whether to include all body values *)
6868+ val fetch_all_body_values : t -> bool
6969+7070+ (** Get max body value bytes limit.
7171+ @return Maximum bytes for body values (0 = no limit) *)
7272+ val max_body_value_bytes : t -> int
7373+7474+ (** JSON serialization for Email/parse arguments *)
7575+ include Jmap_sigs.JSONABLE with type t := t
7676+end
7777+7878+(** {1 Email/parse Response} *)
7979+8080+(** Response for Email/parse method calls.
8181+8282+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.9> RFC 8621, Section 4.9 *)
8383+module Parse_response : sig
8484+ type response
8585+8686+ (** Create an Email/parse response.
8787+ @param account_id The account ID used for the call
8888+ @param ?parsed Optional map of blob IDs to successfully parsed Email objects
8989+ @param ?not_parsable Optional list of blob IDs that could not be parsed
9090+ @param ?not_found Optional list of blob IDs that could not be found
9191+ @return Email/parse response object *)
9292+ val create :
9393+ account_id:string ->
9494+ ?parsed:(Jmap.Id.t * Yojson.Safe.t) list -> (* Using Yojson.Safe.t for Email objects for now *)
9595+ ?not_parsable:Jmap.Id.t list ->
9696+ ?not_found:Jmap.Id.t list ->
9797+ unit ->
9898+ response
9999+100100+ (** Get the account ID.
101101+ @return The account ID used for the call *)
102102+ val account_id : response -> string
103103+104104+ (** Get the parsed emails.
105105+ @return Map of blob IDs to successfully parsed Email objects, or empty list if none *)
106106+ val parsed : response -> (Jmap.Id.t * Yojson.Safe.t) list
107107+108108+ (** Get the not parsable blob IDs.
109109+ @return List of blob IDs that could not be parsed, or empty list if none *)
110110+ val not_parsable : response -> Jmap.Id.t list
111111+112112+ (** Get the not found blob IDs.
113113+ @return List of blob IDs that could not be found, or empty list if none *)
114114+ val not_found : response -> Jmap.Id.t list
115115+116116+ (** JSON serialization for Email/parse responses *)
117117+ include Jmap_sigs.JSONABLE with type t := response
118118+end
+153
jmap/jmap-email/search_snippet.ml
···11+(** Implementation of JMAP SearchSnippet objects (RFC 8621 Section 5) *)
22+33+type t = {
44+ email_id : Jmap.Id.t;
55+ subject : string option;
66+ preview : string option;
77+}
88+99+(** {1 SearchSnippet Construction} *)
1010+1111+let create ~email_id ?subject ?preview () = {
1212+ email_id;
1313+ subject;
1414+ preview;
1515+}
1616+1717+(** {1 Field Access} *)
1818+1919+let email_id t = t.email_id
2020+let subject t = t.subject
2121+let preview t = t.preview
2222+2323+(** {1 JSON Serialization} *)
2424+2525+let to_json t =
2626+ let json_fields = [
2727+ ("emailId", `String (Jmap.Id.to_string t.email_id));
2828+ ] in
2929+ let json_fields = match t.subject with
3030+ | Some s -> ("subject", `String s) :: json_fields
3131+ | None -> ("subject", `Null) :: json_fields
3232+ in
3333+ let json_fields = match t.preview with
3434+ | Some p -> ("preview", `String p) :: json_fields
3535+ | None -> ("preview", `Null) :: json_fields
3636+ in
3737+ `Assoc (List.rev json_fields)
3838+3939+let of_json json =
4040+ try
4141+ let open Yojson.Safe.Util in
4242+ let email_id_str = json |> member "emailId" |> to_string in
4343+ let email_id = match Jmap.Id.of_string email_id_str with
4444+ | Ok id -> id
4545+ | Error _ -> failwith ("Invalid emailId: " ^ email_id_str)
4646+ in
4747+ let subject = json |> member "subject" |> to_string_option in
4848+ let preview = json |> member "preview" |> to_string_option in
4949+ Ok (create ~email_id ?subject ?preview ())
5050+ with
5151+ | exn -> Error ("Failed to parse SearchSnippet: " ^ Printexc.to_string exn)
5252+5353+5454+5555+(** {1 SearchSnippet/get Method Support} *)
5656+5757+module Get_args = struct
5858+ type t = {
5959+ account_id : string;
6060+ filter : Yojson.Safe.t; (* Use raw JSON for now since Filter module doesn't have of_json *)
6161+ email_ids : Jmap.Id.t list;
6262+ }
6363+6464+ let create ~account_id ~filter ~email_ids () = {
6565+ account_id;
6666+ filter;
6767+ email_ids;
6868+ }
6969+7070+ let account_id t = t.account_id
7171+ let filter t = t.filter
7272+ let email_ids t = t.email_ids
7373+7474+ let to_json t =
7575+ `Assoc [
7676+ ("accountId", `String t.account_id);
7777+ ("filter", t.filter);
7878+ ("emailIds", `List (List.map (fun id -> `String (Jmap.Id.to_string id)) t.email_ids));
7979+ ]
8080+8181+ let of_json json =
8282+ try
8383+ let open Yojson.Safe.Util in
8484+ let account_id = json |> member "accountId" |> to_string in
8585+ let filter = json |> member "filter" in
8686+ let email_ids_json = json |> member "emailIds" |> to_list in
8787+ let email_ids = List.map (fun id_json ->
8888+ let id_str = to_string id_json in
8989+ match Jmap.Id.of_string id_str with
9090+ | Ok id -> id
9191+ | Error _ -> failwith ("Invalid email ID: " ^ id_str)
9292+ ) email_ids_json in
9393+ Ok (create ~account_id ~filter ~email_ids ())
9494+ with
9595+ | exn -> Error ("Failed to parse SearchSnippet/get args: " ^ Printexc.to_string exn)
9696+9797+9898+ end
9999+100100+module Get_response = struct
101101+ type snippet = t (* Reference to the outer SearchSnippet.t *)
102102+103103+ type response = {
104104+ account_id : string;
105105+ list : snippet list;
106106+ not_found : Jmap.Id.t list;
107107+ }
108108+109109+ let create ~account_id ~list ?(not_found=[]) () = {
110110+ account_id;
111111+ list;
112112+ not_found;
113113+ }
114114+115115+ let account_id t = t.account_id
116116+ let list t = t.list
117117+ let not_found t = t.not_found
118118+119119+ let to_json t =
120120+ `Assoc [
121121+ ("accountId", `String t.account_id);
122122+ ("list", `List (List.map to_json t.list));
123123+ ("notFound", match t.not_found with
124124+ | [] -> `Null
125125+ | ids -> `List (List.map (fun id -> `String (Jmap.Id.to_string id)) ids));
126126+ ]
127127+128128+ let of_json json =
129129+ try
130130+ let open Yojson.Safe.Util in
131131+ let account_id = json |> member "accountId" |> to_string in
132132+ let list_json = json |> member "list" |> to_list in
133133+ let list = List.map (fun snippet_json ->
134134+ match of_json snippet_json with
135135+ | Ok snippet -> snippet
136136+ | Error err -> failwith err
137137+ ) list_json in
138138+ let not_found = match json |> member "notFound" with
139139+ | `Null -> []
140140+ | `List ids -> List.map (fun id_json ->
141141+ let id_str = to_string id_json in
142142+ match Jmap.Id.of_string id_str with
143143+ | Ok id -> id
144144+ | Error _ -> failwith ("Invalid not found ID: " ^ id_str)
145145+ ) ids
146146+ | _ -> failwith "notFound must be null or array"
147147+ in
148148+ Ok (create ~account_id ~list ~not_found ())
149149+ with
150150+ | exn -> Error ("Failed to parse SearchSnippet/get response: " ^ Printexc.to_string exn)
151151+152152+153153+ end
+131
jmap/jmap-email/search_snippet.mli
···11+(** JMAP SearchSnippet objects as defined in RFC 8621 Section 5.
22+33+ SearchSnippets represent search result highlights showing relevant sections
44+ of email body that match a search query, with highlighted terms in both
55+ subject and preview text.
66+77+ Unlike other JMAP objects, SearchSnippets do NOT have an 'id' property
88+ since they are derived from search operations and are not persistent.
99+1010+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-5> RFC 8621, Section 5 *)
1111+1212+(** {1 SearchSnippet Object} *)
1313+1414+(** A SearchSnippet object represents search result highlights for an email.
1515+1616+ The SearchSnippet shows relevant sections of the message body that match
1717+ the search query, with matching terms highlighted using HTML-like markup.
1818+1919+ What constitutes a "relevant section" is server-defined. If the server
2020+ cannot determine search snippets, it returns null for both subject and
2121+ preview properties.
2222+2323+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-5> RFC 8621, Section 5 *)
2424+type t = {
2525+ email_id : Jmap.Id.t; (** The Email ID the snippet applies to *)
2626+ subject : string option; (** Subject line with highlighted search terms, or null *)
2727+ preview : string option; (** Body preview with highlighted search terms, or null *)
2828+}
2929+3030+(** {1 SearchSnippet Construction} *)
3131+3232+(** Create a SearchSnippet object.
3333+ @param email_id The Email ID this snippet applies to
3434+ @param ?subject Optional subject with highlighted terms (null if server cannot determine)
3535+ @param ?preview Optional preview text with highlighted terms (null if server cannot determine)
3636+ @return A new SearchSnippet object *)
3737+val create :
3838+ email_id:Jmap.Id.t ->
3939+ ?subject:string ->
4040+ ?preview:string ->
4141+ unit ->
4242+ t
4343+4444+(** {1 Field Access} *)
4545+4646+(** Get the Email ID.
4747+ @return The Email ID this snippet applies to *)
4848+val email_id : t -> Jmap.Id.t
4949+5050+(** Get the highlighted subject.
5151+ @return The subject with search terms highlighted, or None if not available *)
5252+val subject : t -> string option
5353+5454+(** Get the preview text.
5555+ @return The preview text with search terms highlighted, or None if not available *)
5656+val preview : t -> string option
5757+5858+(** {1 JSON Serialization} *)
5959+6060+(** SearchSnippet objects implement the JSONABLE interface for protocol validation and formatting. *)
6161+include Jmap_sigs.JSONABLE with type t := t
6262+6363+(** {1 SearchSnippet/get Method Support} *)
6464+6565+(** Arguments for SearchSnippet/get method calls.
6666+6767+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-5.1> RFC 8621, Section 5.1 *)
6868+module Get_args : sig
6969+ type t
7070+7171+ (** Create SearchSnippet/get arguments.
7272+ @param account_id The account ID to use
7373+ @param filter The same filter as passed to Email/query (for search context)
7474+ @param email_ids Array of Email IDs to get search snippets for
7575+ @return SearchSnippet get arguments object *)
7676+ val create :
7777+ account_id:string ->
7878+ filter:Yojson.Safe.t ->
7979+ email_ids:Jmap.Id.t list ->
8080+ unit ->
8181+ t
8282+8383+ (** Get the account ID.
8484+ @return The account ID for this request *)
8585+ val account_id : t -> string
8686+8787+ (** Get the search filter.
8888+ @return The filter used for search context *)
8989+ val filter : t -> Yojson.Safe.t
9090+9191+ (** Get the Email IDs.
9292+ @return List of Email IDs to get snippets for *)
9393+ val email_ids : t -> Jmap.Id.t list
9494+9595+ (** JSON serialization for SearchSnippet/get arguments *)
9696+ include Jmap_sigs.JSONABLE with type t := t
9797+end
9898+9999+(** Response for SearchSnippet/get method calls.
100100+101101+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-5.1> RFC 8621, Section 5.1 *)
102102+module Get_response : sig
103103+ type response (* The response type *)
104104+105105+ (** Create a SearchSnippet/get response.
106106+ @param account_id The account ID used for the call
107107+ @param list Array of SearchSnippet objects for the requested Email IDs
108108+ @param ?not_found Optional array of Email IDs that could not be found
109109+ @return SearchSnippet get response object *)
110110+ val create :
111111+ account_id:string ->
112112+ list:t list -> (* t refers to the outer SearchSnippet.t *)
113113+ ?not_found:Jmap.Id.t list ->
114114+ unit ->
115115+ response
116116+117117+ (** Get the account ID.
118118+ @return The account ID used for the call *)
119119+ val account_id : response -> string
120120+121121+ (** Get the SearchSnippet objects.
122122+ @return Array of SearchSnippet objects (may not be in same order as request) *)
123123+ val list : response -> t list (* t refers to the outer SearchSnippet.t *)
124124+125125+ (** Get the not found Email IDs.
126126+ @return Array of Email IDs that could not be found, or empty list if all found *)
127127+ val not_found : response -> Jmap.Id.t list
128128+129129+ (** JSON serialization for SearchSnippet/get responses *)
130130+ include Jmap_sigs.JSONABLE with type t := response
131131+end
···11-open Address
22-open Keywords
33-44-let%expect_test "email_address_json_roundtrip" =
55- let addr = match create ~name:"John Doe" ~email:"john@example.com" () with
66- | Ok a -> a
77- | Error e -> failwith e
88- in
99- let json = to_json addr in
1010- let parsed = match of_json json with
1111- | Ok p -> p
1212- | Error e -> failwith e
1313- in
1414- Printf.printf "Original: name=%s email=%s\n"
1515- (match name addr with Some n -> n | None -> "None")
1616- (email addr);
1717- Printf.printf "Parsed: name=%s email=%s\n"
1818- (match name parsed with Some n -> n | None -> "None")
1919- (email parsed);
2020- [%expect {|
2121- Original: name=John Doe email=john@example.com
2222- Parsed: name=John Doe email=john@example.com |}]
2323-2424-let%expect_test "email_address_no_name_json" =
2525- let addr = Email_address.v ~email:"jane@example.com" () in
2626- let json = Email_address.to_json addr in
2727- Printf.printf "JSON: %s\n" (Yojson.Safe.to_string json);
2828- let parsed = Email_address.of_json json in
2929- Printf.printf "Email: %s\n" (Email_address.email parsed);
3030- [%expect {|
3131- JSON: {"email":"jane@example.com"}
3232- Email: jane@example.com |}]
3333-3434-let%expect_test "keywords_json_roundtrip" =
3535- let keywords = Keywords.[Draft; Seen; Flagged; Custom "custom-label"] in
3636- let json = Keywords.to_json keywords in
3737- Printf.printf "JSON: %s\n" (Yojson.Safe.to_string json);
3838- let parsed = Keywords.of_json json in
3939- Printf.printf "Is draft: %b\n" (Keywords.is_draft parsed);
4040- Printf.printf "Is seen: %b\n" (Keywords.is_seen parsed);
4141- Printf.printf "Is flagged: %b\n" (Keywords.is_flagged parsed);
4242- Printf.printf "Custom keywords: %s\n"
4343- (String.concat "; " (Keywords.custom_keywords parsed));
4444- [%expect {|
4545- JSON: {"custom-label":true,"$flagged":true,"$seen":true,"$draft":true}
4646- Is draft: true
4747- Is seen: true
4848- Is flagged: true
4949- Custom keywords: custom-label
5050- |}]
5151-5252-let%expect_test "email_header_json" =
5353- let header = Email_header.v ~name:"Subject" ~value:"Test Email" () in
5454- let json = Email_header.to_json header in
5555- Printf.printf "JSON: %s\n" (Yojson.Safe.to_string json);
5656- let parsed = Email_header.of_json json in
5757- Printf.printf "Header: %s = %s\n"
5858- (Email_header.name parsed) (Email_header.value parsed);
5959- [%expect {|
6060- JSON: {"name":"Subject","value":"Test Email"}
6161- Header: Subject = Test Email |}]
6262-6363-let%expect_test "body_part_json_simple" =
6464- let headers = [Email_header.v ~name:"Content-Type" ~value:"text/plain" ()] in
6565- let body_part = Email_body_part.v
6666- ~id:"1"
6767- ~blob_id:"G123"
6868- ~size:1234
6969- ~headers
7070- ~mime_type:"text/plain"
7171- ~charset:"utf-8"
7272- () in
7373- let json = Email_body_part.to_json body_part in
7474- Printf.printf "JSON keys: ";
7575- (match json with
7676- | `Assoc fields ->
7777- List.iter (fun (k, _) -> Printf.printf "%s " k) fields
7878- | _ -> Printf.printf "not an object");
7979- Printf.printf "\n";
8080- let parsed = Email_body_part.of_json json in
8181- Printf.printf "Part ID: %s\n"
8282- (match Email_body_part.id parsed with Some id -> id | None -> "None");
8383- Printf.printf "MIME type: %s\n" (Email_body_part.mime_type parsed);
8484- Printf.printf "Size: %d\n" (Email_body_part.size parsed);
8585- [%expect {|
8686- JSON keys: charset blobId partId size headers type
8787- Part ID: 1
8888- MIME type: text/plain
8989- Size: 1234
9090- |}]
9191-9292-let%expect_test "email_json_comprehensive" =
9393- let from_addr = Email_address.v ~name:"Alice" ~email:"alice@example.com" () in
9494- let to_addr = Email_address.v ~name:"Bob" ~email:"bob@example.com" () in
9595- let keywords = Keywords.[Seen; Flagged] in
9696- let mailbox_ids = Hashtbl.create 2 in
9797- Hashtbl.add mailbox_ids "inbox" true;
9898- Hashtbl.add mailbox_ids "important" true;
9999-100100- let email = Email.create
101101- ~id:"M123"
102102- ~blob_id:"B456"
103103- ~thread_id:"T789"
104104- ~mailbox_ids
105105- ~keywords
106106- ~size:5432
107107- ~received_at:1697376600.0
108108- ~subject:"Important Message"
109109- ~preview:"This is a preview of the message..."
110110- ~from:[from_addr]
111111- ~to_:[to_addr]
112112- ~message_id:["<msg123@example.com>"]
113113- ~has_attachment:false
114114- () in
115115-116116- let json = Email.to_json email in
117117- Printf.printf "Email has ID: %b\n" (Email.id email <> None);
118118- Printf.printf "Email subject: %s\n"
119119- (match Email.subject email with Some s -> s | None -> "None");
120120-121121- let parsed = Email.of_json json in
122122- Printf.printf "Parsed ID: %s\n"
123123- (match Email.id parsed with Some id -> id | None -> "None");
124124- Printf.printf "Parsed subject: %s\n"
125125- (match Email.subject parsed with Some s -> s | None -> "None");
126126- Printf.printf "From count: %d\n"
127127- (match Email.from parsed with Some addrs -> List.length addrs | None -> 0);
128128- Printf.printf "Keywords seen: %b\n"
129129- (match Email.keywords parsed with
130130- | Some kws -> Keywords.is_seen kws
131131- | None -> false);
132132- [%expect {|
133133- Email has ID: true
134134- Email subject: Important Message
135135- Parsed ID: M123
136136- Parsed subject: Important Message
137137- From count: 1
138138- Keywords seen: true |}]
139139-140140-let%expect_test "jmap_email_example" =
141141- (* Example based on RFC 8621 Section 4.1.1 *)
142142- let json_string = {|
143143- {
144144- "id": "Mf5d1a9e0be7234627fac9ad32cc8c25a63e96db08",
145145- "blobId": "Gd2f30c5cfbc95fb81dd0aa2c8b0d4bd52c0feec8a",
146146- "threadId": "T8bc7a2bf2c41d1b78eaa1dd0e0c1e35ad50b8e6e",
147147- "mailboxIds": {
148148- "Mf2cc7a1bb1a6b68c0c244bbda2bb9b4a7b9d0123": true,
149149- "M2cc7a1bb1a6b68c0c244bbda2bb9b4a7b9d0456": true
150150- },
151151- "keywords": {
152152- "$seen": true,
153153- "$flagged": true
154154- },
155155- "size": 2048,
156156- "receivedAt": 1634307225.0,
157157- "messageId": ["<msgid1@example.org>"],
158158- "subject": "Dinner Party Invitation",
159159- "from": [
160160- {
161161- "name": "Joe Bloggs",
162162- "email": "joe@example.com"
163163- }
164164- ],
165165- "to": [
166166- {
167167- "name": "John Smith",
168168- "email": "john@example.com"
169169- }
170170- ],
171171- "hasAttachment": false,
172172- "preview": "You are invited to a dinner party..."
173173- }
174174- |} in
175175-176176- let json = Yojson.Safe.from_string json_string in
177177- let email = Email.of_json json in
178178-179179- Printf.printf "Email ID: %s\n"
180180- (match Email.id email with Some id -> id | None -> "None");
181181- Printf.printf "Subject: %s\n"
182182- (match Email.subject email with Some s -> s | None -> "None");
183183- Printf.printf "Size: %d\n"
184184- (match Email.size email with Some s -> s | None -> 0);
185185- Printf.printf "From name: %s\n"
186186- (match Email.from email with
187187- | Some [addr] ->
188188- (match Email_address.name addr with Some n -> n | None -> "None")
189189- | _ -> "None");
190190- Printf.printf "Has attachment: %b\n"
191191- (match Email.has_attachment email with Some b -> b | None -> false);
192192- Printf.printf "Mailbox count: %d\n"
193193- (match Email.mailbox_ids email with
194194- | Some ids -> Hashtbl.length ids
195195- | None -> 0);
196196- [%expect {|
197197- Email ID: Mf5d1a9e0be7234627fac9ad32cc8c25a63e96db08
198198- Subject: Dinner Party Invitation
199199- Size: 2048
200200- From name: Joe Bloggs
201201- Has attachment: false
202202- Mailbox count: 2 |}]
203203-204204-(* EmailSubmission tests *)
205205-(* Access submission module through the main Jmap_email module *)
206206-module Submission = Submission
207207-open Jmap.Methods
208208-209209-let%expect_test "email_submission_filter_identity_ids" =
210210- let filter = Submission.Email_submission_filter.identity_ids ["id1"; "id2"] in
211211- let json = Filter.to_json filter in
212212- Printf.printf "Filter JSON: %s\n" (Yojson.Safe.to_string json);
213213- [%expect {|
214214- Filter JSON: {"identityId":{"in":["id1","id2"]}}
215215- |}]
216216-217217-let%expect_test "email_submission_filter_undo_status" =
218218- let filter = Submission.Email_submission_filter.undo_status `Pending in
219219- let json = Filter.to_json filter in
220220- Printf.printf "Filter JSON: %s\n" (Yojson.Safe.to_string json);
221221- [%expect {|
222222- Filter JSON: {"undoStatus":"pending"}
223223- |}]
224224-225225-let%expect_test "email_submission_filter_date_range" =
226226- let filter = Submission.Email_submission_filter.date_range ~after_date:1634307200.0 ~before_date:1634393600.0 in
227227- let json = Filter.to_json filter in
228228- Printf.printf "Filter JSON: %s\n" (Yojson.Safe.to_string json);
229229- [%expect {| Filter JSON: {"operator":"AND","conditions":[{"sendAt":{"gt":1634307200.0}},{"sendAt":{"lt":1634393600.0}}]} |}]
230230-231231-let%expect_test "email_submission_sort_newest_first" =
232232- let sort = Submission.Email_submission_sort.send_newest_first () in
233233- let json = Comparator.to_json sort in
234234- Printf.printf "Sort JSON: %s\n" (Yojson.Safe.to_string json);
235235- [%expect {| Sort JSON: {"isAscending":false,"property":"sendAt"} |}]
236236-237237-let%expect_test "email_submission_json_envelope_address" =
238238- let addr = {
239239- Submission.env_addr_email = "user@example.com";
240240- Submission.env_addr_parameters = None;
241241- } in
242242- let json = Submission.Json.envelope_address_to_json addr in
243243- Printf.printf "Envelope address JSON: %s\n" (Yojson.Safe.to_string json);
244244- [%expect {|
245245- Envelope address JSON: {"email":"user@example.com"}
246246- |}]
247247-248248-let%expect_test "email_submission_json_envelope_address_with_params" =
249249- let params = Hashtbl.create 2 in
250250- Hashtbl.add params "SIZE" (`String "1024");
251251- Hashtbl.add params "BODY" (`String "8BITMIME");
252252- let addr = {
253253- Submission.env_addr_email = "user@example.com";
254254- Submission.env_addr_parameters = Some params;
255255- } in
256256- let json = Submission.Json.envelope_address_to_json addr in
257257- Printf.printf "Envelope address with params: %s\n" (Yojson.Safe.to_string json);
258258- [%expect {|
259259- Envelope address with params: {"parameters":{"BODY":"8BITMIME","SIZE":"1024"},"email":"user@example.com"}
260260- |}]
261261-262262-let%expect_test "email_submission_json_envelope" =
263263- let mail_from = {
264264- Submission.env_addr_email = "sender@example.com";
265265- Submission.env_addr_parameters = None;
266266- } in
267267- let rcpt_to = [
268268- { Submission.env_addr_email = "user1@example.com"; Submission.env_addr_parameters = None };
269269- { Submission.env_addr_email = "user2@example.com"; Submission.env_addr_parameters = None };
270270- ] in
271271- let envelope = {
272272- Submission.env_mail_from = mail_from;
273273- Submission.env_rcpt_to = rcpt_to;
274274- } in
275275- let json = Submission.Json.envelope_to_json envelope in
276276- Printf.printf "Envelope JSON: %s\n" (Yojson.Safe.to_string json);
277277- [%expect {|
278278- Envelope JSON: {"mailFrom":{"email":"sender@example.com"},"rcptTo":[{"email":"user1@example.com"},{"email":"user2@example.com"}]}
279279- |}]
280280-281281-let%expect_test "email_submission_json_delivery_status" =
282282- let status = {
283283- Submission.delivery_smtp_reply = "250 OK";
284284- Submission.delivery_delivered = `Yes;
285285- Submission.delivery_displayed = `Unknown;
286286- } in
287287- let json = Submission.Json.delivery_status_to_json status in
288288- Printf.printf "Delivery status JSON: %s\n" (Yojson.Safe.to_string json);
289289- [%expect {|
290290- Delivery status JSON: {"smtpReply":"250 OK","delivered":"yes","displayed":"unknown"}
291291- |}]
292292-293293-let%expect_test "email_submission_json_create" =
294294- let create = {
295295- Submission.email_sub_create_identity_id = "identity123";
296296- Submission.email_sub_create_email_id = "email456";
297297- Submission.email_sub_create_envelope = None;
298298- } in
299299- let json = Submission.Json.email_submission_create_to_json create in
300300- Printf.printf "EmailSubmission create JSON: %s\n" (Yojson.Safe.to_string json);
301301- [%expect {|
302302- EmailSubmission create JSON: {"identityId":"identity123","emailId":"email456"}
303303- |}]
304304-305305-let%expect_test "email_submission_json_full" =
306306- let envelope = {
307307- Submission.env_mail_from = { Submission.env_addr_email = "sender@company.com"; Submission.env_addr_parameters = None };
308308- Submission.env_rcpt_to = [{ Submission.env_addr_email = "recipient@example.com"; Submission.env_addr_parameters = None }];
309309- } in
310310- let delivery_status_map = Hashtbl.create 1 in
311311- let delivery_status = {
312312- Submission.delivery_smtp_reply = "250 2.0.0 OK";
313313- Submission.delivery_delivered = `Yes;
314314- Submission.delivery_displayed = `Unknown;
315315- } in
316316- Hashtbl.add delivery_status_map "recipient@example.com" delivery_status;
317317-318318- let submission = {
319319- Submission.email_sub_id = "sub123";
320320- Submission.identity_id = "identity456";
321321- Submission.email_id = "email789";
322322- Submission.thread_id = "thread012";
323323- Submission.envelope = Some envelope;
324324- Submission.send_at = 1634307225.0;
325325- Submission.undo_status = `Final;
326326- Submission.delivery_status = Some delivery_status_map;
327327- Submission.dsn_blob_ids = [];
328328- Submission.mdn_blob_ids = [];
329329- } in
330330- let json = Submission.Json.email_submission_to_json submission in
331331- Printf.printf "Full EmailSubmission JSON has expected fields:\n";
332332- (match json with
333333- | `Assoc fields ->
334334- List.iter (fun (k, _) -> Printf.printf " %s\n" k)
335335- (List.sort (fun (a, _) (b, _) -> String.compare a b) fields)
336336- | _ -> Printf.printf " not an object\n");
337337- [%expect {|
338338- Full EmailSubmission JSON has expected fields:
339339- deliveryStatus
340340- dsnBlobIds
341341- emailId
342342- envelope
343343- id
344344- identityId
345345- mdnBlobIds
346346- sendAt
347347- threadId
348348- undoStatus
349349- |}]
350350-351351-let%expect_test "email_submission_query_args_json" =
352352- let filter = Submission.Email_submission_filter.undo_status `Pending in
353353- let sort = [Submission.Email_submission_sort.send_newest_first ()] in
354354- let query_args = Query_args.v
355355- ~account_id:"account123"
356356- ~filter
357357- ~sort
358358- ~limit:10
359359- () in
360360- let json = Submission.Email_submission_query_args.to_json query_args in
361361- Printf.printf "Query args JSON contains keys: ";
362362- (match json with
363363- | `Assoc fields ->
364364- let keys = List.map fst fields |> List.sort String.compare in
365365- Printf.printf "%s\n" (String.concat ", " keys)
366366- | _ -> Printf.printf "not an object\n");
367367- [%expect {|
368368- Query args JSON contains keys: accountId, filter, limit, sort
369369- |}]
370370-371371-let%expect_test "email_submission_get_args_json" =
372372- let get_args = Get_args.v
373373- ~account_id:"account123"
374374- ~ids:["sub1"; "sub2"]
375375- ~properties:["id"; "emailId"; "undoStatus"]
376376- () in
377377- let json = Submission.Email_submission_get_args.to_json get_args in
378378- Printf.printf "Get args JSON contains keys: ";
379379- (match json with
380380- | `Assoc fields ->
381381- let keys = List.map fst fields |> List.sort String.compare in
382382- Printf.printf "%s\n" (String.concat ", " keys)
383383- | _ -> Printf.printf "not an object\n");
384384- [%expect {|
385385- Get args JSON contains keys: accountId, ids, properties
386386- |}]
+263
jmap/jmap/blob.ml
···11+(** Implementation of JMAP Blob operations for binary data management *)
22+33+(** {1 Blob Object} *)
44+55+type t = {
66+ id : Id.t;
77+ size : int;
88+ type_ : string option;
99+}
1010+1111+let create ~id ~size ?type_ () = {
1212+ id;
1313+ size;
1414+ type_;
1515+}
1616+1717+let id t = t.id
1818+let size t = t.size
1919+let type_ t = t.type_
2020+2121+(** JSON serialization for Blob objects *)
2222+let to_json t =
2323+ let json_fields = [
2424+ ("id", `String (Id.to_string t.id));
2525+ ("size", `Int t.size);
2626+ ] in
2727+ let json_fields = match t.type_ with
2828+ | Some mime_type -> ("type", `String mime_type) :: json_fields
2929+ | None -> json_fields
3030+ in
3131+ `Assoc (List.rev json_fields)
3232+3333+let of_json json =
3434+ try
3535+ let open Yojson.Safe.Util in
3636+ let id_str = json |> member "id" |> to_string in
3737+ let id = match Id.of_string id_str with
3838+ | Ok id -> id
3939+ | Error _ -> failwith ("Invalid blob ID: " ^ id_str)
4040+ in
4141+ let size = json |> member "size" |> to_int in
4242+ let type_ = json |> member "type" |> to_string_option in
4343+ Ok (create ~id ~size ?type_ ())
4444+ with
4545+ | exn -> Error ("Failed to parse Blob: " ^ Printexc.to_string exn)
4646+4747+(** {1 Blob/get Method Support} *)
4848+4949+module Get_args = struct
5050+ type t = {
5151+ account_id : string;
5252+ blob_ids : Id.t list;
5353+ }
5454+5555+ let create ~account_id ~blob_ids () = {
5656+ account_id;
5757+ blob_ids;
5858+ }
5959+6060+ let account_id t = t.account_id
6161+ let blob_ids t = t.blob_ids
6262+6363+ let to_json t =
6464+ `Assoc [
6565+ ("accountId", `String t.account_id);
6666+ ("blobIds", `List (List.map (fun id -> `String (Id.to_string id)) t.blob_ids));
6767+ ]
6868+6969+ let of_json json =
7070+ try
7171+ let open Yojson.Safe.Util in
7272+ let account_id = json |> member "accountId" |> to_string in
7373+ let blob_ids_json = json |> member "blobIds" |> to_list in
7474+ let blob_ids = List.map (fun id_json ->
7575+ let id_str = to_string id_json in
7676+ match Id.of_string id_str with
7777+ | Ok id -> id
7878+ | Error _ -> failwith ("Invalid blob ID: " ^ id_str)
7979+ ) blob_ids_json in
8080+ Ok (create ~account_id ~blob_ids ())
8181+ with
8282+ | exn -> Error ("Failed to parse Blob/get args: " ^ Printexc.to_string exn)
8383+end
8484+8585+module Get_response = struct
8686+ type response = {
8787+ account_id : string;
8888+ list : t list;
8989+ not_found : Id.t list;
9090+ }
9191+9292+ let create ~account_id ?(list=[]) ?(not_found=[]) () = {
9393+ account_id;
9494+ list;
9595+ not_found;
9696+ }
9797+9898+ let account_id t = t.account_id
9999+ let list t = t.list
100100+ let not_found t = t.not_found
101101+102102+ let to_json t =
103103+ let json_fields = [
104104+ ("accountId", `String t.account_id);
105105+ ] in
106106+ let json_fields = if t.list = [] then
107107+ ("list", `Null) :: json_fields
108108+ else
109109+ ("list", `List (List.map to_json t.list)) :: json_fields
110110+ in
111111+ let json_fields = if t.not_found = [] then
112112+ ("notFound", `Null) :: json_fields
113113+ else
114114+ ("notFound", `List (List.map (fun id -> `String (Id.to_string id)) t.not_found)) :: json_fields
115115+ in
116116+ `Assoc (List.rev json_fields)
117117+118118+ let of_json json =
119119+ try
120120+ let open Yojson.Safe.Util in
121121+ let account_id = json |> member "accountId" |> to_string in
122122+ let list = match json |> member "list" with
123123+ | `Null -> []
124124+ | list_json -> List.map (fun blob_json ->
125125+ match of_json blob_json with
126126+ | Ok blob -> blob
127127+ | Error err -> failwith err
128128+ ) (to_list list_json)
129129+ in
130130+ let not_found = match json |> member "notFound" with
131131+ | `Null -> []
132132+ | ids_json -> List.map (fun id_json ->
133133+ let id_str = to_string id_json in
134134+ match Id.of_string id_str with
135135+ | Ok id -> id
136136+ | Error _ -> failwith ("Invalid blob ID in notFound: " ^ id_str)
137137+ ) (to_list ids_json)
138138+ in
139139+ Ok (create ~account_id ~list ~not_found ())
140140+ with
141141+ | exn -> Error ("Failed to parse Blob/get response: " ^ Printexc.to_string exn)
142142+end
143143+144144+(** {1 Blob/copy Method Support} *)
145145+146146+module Copy_args = struct
147147+ type t = {
148148+ from_account_id : string;
149149+ account_id : string;
150150+ blob_ids : Id.t list;
151151+ }
152152+153153+ let create ~from_account_id ~account_id ~blob_ids () = {
154154+ from_account_id;
155155+ account_id;
156156+ blob_ids;
157157+ }
158158+159159+ let from_account_id t = t.from_account_id
160160+ let account_id t = t.account_id
161161+ let blob_ids t = t.blob_ids
162162+163163+ let to_json t =
164164+ `Assoc [
165165+ ("fromAccountId", `String t.from_account_id);
166166+ ("accountId", `String t.account_id);
167167+ ("blobIds", `List (List.map (fun id -> `String (Id.to_string id)) t.blob_ids));
168168+ ]
169169+170170+ let of_json json =
171171+ try
172172+ let open Yojson.Safe.Util in
173173+ let from_account_id = json |> member "fromAccountId" |> to_string in
174174+ let account_id = json |> member "accountId" |> to_string in
175175+ let blob_ids_json = json |> member "blobIds" |> to_list in
176176+ let blob_ids = List.map (fun id_json ->
177177+ let id_str = to_string id_json in
178178+ match Id.of_string id_str with
179179+ | Ok id -> id
180180+ | Error _ -> failwith ("Invalid blob ID: " ^ id_str)
181181+ ) blob_ids_json in
182182+ Ok (create ~from_account_id ~account_id ~blob_ids ())
183183+ with
184184+ | exn -> Error ("Failed to parse Blob/copy args: " ^ Printexc.to_string exn)
185185+end
186186+187187+module Copy_response = struct
188188+ type response = {
189189+ from_account_id : string;
190190+ account_id : string;
191191+ copied : (Id.t * Id.t) list; (* old_id -> new_id mappings *)
192192+ not_copied : (Id.t * Error.Set_error.t) list;
193193+ }
194194+195195+ let create ~from_account_id ~account_id ?(copied=[]) ?(not_copied=[]) () = {
196196+ from_account_id;
197197+ account_id;
198198+ copied;
199199+ not_copied;
200200+ }
201201+202202+ let from_account_id t = t.from_account_id
203203+ let account_id t = t.account_id
204204+ let copied t = t.copied
205205+ let not_copied t = t.not_copied
206206+207207+ let to_json t =
208208+ let json_fields = [
209209+ ("fromAccountId", `String t.from_account_id);
210210+ ("accountId", `String t.account_id);
211211+ ] in
212212+ let json_fields = if t.copied = [] then
213213+ ("copied", `Null) :: json_fields
214214+ else
215215+ ("copied", `Assoc (List.map (fun (old_id, new_id) ->
216216+ (Id.to_string old_id, `String (Id.to_string new_id))
217217+ ) t.copied)) :: json_fields
218218+ in
219219+ let json_fields = if t.not_copied = [] then
220220+ ("notCopied", `Null) :: json_fields
221221+ else
222222+ ("notCopied", `Assoc (List.map (fun (id, err) ->
223223+ (Id.to_string id, Error.Set_error.to_json err)
224224+ ) t.not_copied)) :: json_fields
225225+ in
226226+ `Assoc (List.rev json_fields)
227227+228228+ let of_json json =
229229+ try
230230+ let open Yojson.Safe.Util in
231231+ let from_account_id = json |> member "fromAccountId" |> to_string in
232232+ let account_id = json |> member "accountId" |> to_string in
233233+ let copied = match json |> member "copied" with
234234+ | `Null -> []
235235+ | copied_json -> List.map (fun (old_id_str, new_id_json) ->
236236+ let old_id = match Id.of_string old_id_str with
237237+ | Ok id -> id
238238+ | Error _ -> failwith ("Invalid old blob ID in copied: " ^ old_id_str)
239239+ in
240240+ let new_id_str = to_string new_id_json in
241241+ let new_id = match Id.of_string new_id_str with
242242+ | Ok id -> id
243243+ | Error _ -> failwith ("Invalid new blob ID in copied: " ^ new_id_str)
244244+ in
245245+ (old_id, new_id)
246246+ ) (to_assoc copied_json)
247247+ in
248248+ let not_copied = match json |> member "notCopied" with
249249+ | `Null -> []
250250+ | not_copied_json -> List.map (fun (id_str, err_json) ->
251251+ let id = match Id.of_string id_str with
252252+ | Ok id -> id
253253+ | Error _ -> failwith ("Invalid blob ID in notCopied: " ^ id_str)
254254+ in
255255+ match Error.Set_error.of_json err_json with
256256+ | Ok err -> (id, err)
257257+ | Error err_msg -> failwith err_msg
258258+ ) (to_assoc not_copied_json)
259259+ in
260260+ Ok (create ~from_account_id ~account_id ~copied ~not_copied ())
261261+ with
262262+ | exn -> Error ("Failed to parse Blob/copy response: " ^ Printexc.to_string exn)
263263+end
+183
jmap/jmap/blob.mli
···11+(** JMAP Blob operations for binary data management.
22+33+ Blobs represent arbitrary binary data in JMAP. This module handles
44+ blob metadata operations. Actual blob content is downloaded via
55+ HTTP endpoints, not through JMAP method calls.
66+77+ @see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6> RFC 8620, Section 6 *)
88+99+(** {1 Blob Object} *)
1010+1111+(** A Blob object represents metadata about binary data.
1212+1313+ Note: The actual binary content is accessed via download URLs,
1414+ not through this object. This provides metadata only.
1515+1616+ @see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6> RFC 8620, Section 6 *)
1717+type t = {
1818+ id : Id.t; (** The blob ID *)
1919+ size : int; (** Size of the blob in octets *)
2020+ type_ : string option; (** MIME type of the blob, if known *)
2121+}
2222+2323+(** Create a Blob object.
2424+ @param id The blob ID
2525+ @param size Size in octets
2626+ @param ?type_ Optional MIME type
2727+ @return A new Blob object *)
2828+val create : id:Id.t -> size:int -> ?type_:string -> unit -> t
2929+3030+(** Get the blob ID.
3131+ @return The blob ID *)
3232+val id : t -> Id.t
3333+3434+(** Get the blob size.
3535+ @return Size in octets *)
3636+val size : t -> int
3737+3838+(** Get the blob MIME type.
3939+ @return MIME type if known, None otherwise *)
4040+val type_ : t -> string option
4141+4242+(** JSON serialization for Blob objects *)
4343+include Jmap_sigs.JSONABLE with type t := t
4444+4545+(** {1 Blob/get Method Support} *)
4646+4747+(** Arguments for Blob/get method calls.
4848+4949+ Get metadata for one or more blobs by their IDs. *)
5050+module Get_args : sig
5151+ type t
5252+5353+ (** Create Blob/get arguments.
5454+ @param account_id The account ID to use
5555+ @param blob_ids Array of blob IDs to get metadata for
5656+ @return Blob/get arguments object *)
5757+ val create :
5858+ account_id:string ->
5959+ blob_ids:Id.t list ->
6060+ unit ->
6161+ t
6262+6363+ (** Get the account ID.
6464+ @return The account ID for this request *)
6565+ val account_id : t -> string
6666+6767+ (** Get the blob IDs.
6868+ @return List of blob IDs to get metadata for *)
6969+ val blob_ids : t -> Id.t list
7070+7171+ (** JSON serialization for Blob/get arguments *)
7272+ include Jmap_sigs.JSONABLE with type t := t
7373+end
7474+7575+(** Response for Blob/get method calls. *)
7676+module Get_response : sig
7777+ type response
7878+7979+ (** Create a Blob/get response.
8080+ @param account_id The account ID used for the call
8181+ @param ?list Optional array of successfully retrieved Blob objects
8282+ @param ?not_found Optional array of blob IDs that could not be found
8383+ @return Blob/get response object *)
8484+ val create :
8585+ account_id:string ->
8686+ ?list:t list ->
8787+ ?not_found:Id.t list ->
8888+ unit ->
8989+ response
9090+9191+ (** Get the account ID.
9292+ @return The account ID used for the call *)
9393+ val account_id : response -> string
9494+9595+ (** Get the blob objects.
9696+ @return Array of Blob objects, or empty list if none *)
9797+ val list : response -> t list
9898+9999+ (** Get the not found blob IDs.
100100+ @return Array of blob IDs that could not be found, or empty list if none *)
101101+ val not_found : response -> Id.t list
102102+103103+ (** JSON serialization for Blob/get responses *)
104104+ include Jmap_sigs.JSONABLE with type t := response
105105+end
106106+107107+(** {1 Blob/copy Method Support} *)
108108+109109+(** Arguments for Blob/copy method calls.
110110+111111+ Copy blobs between different accounts without downloading and re-uploading.
112112+113113+ @see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6.3> RFC 8620, Section 6.3 *)
114114+module Copy_args : sig
115115+ type t
116116+117117+ (** Create Blob/copy arguments.
118118+ @param from_account_id The account ID to copy blobs from
119119+ @param account_id The account ID to copy blobs to
120120+ @param blob_ids Array of blob IDs to copy
121121+ @return Blob/copy arguments object *)
122122+ val create :
123123+ from_account_id:string ->
124124+ account_id:string ->
125125+ blob_ids:Id.t list ->
126126+ unit ->
127127+ t
128128+129129+ (** Get the source account ID.
130130+ @return The account ID to copy blobs from *)
131131+ val from_account_id : t -> string
132132+133133+ (** Get the destination account ID.
134134+ @return The account ID to copy blobs to *)
135135+ val account_id : t -> string
136136+137137+ (** Get the blob IDs to copy.
138138+ @return List of blob IDs to copy *)
139139+ val blob_ids : t -> Id.t list
140140+141141+ (** JSON serialization for Blob/copy arguments *)
142142+ include Jmap_sigs.JSONABLE with type t := t
143143+end
144144+145145+(** Response for Blob/copy method calls.
146146+147147+ @see <https://www.rfc-editor.org/rfc/rfc8620.html#section-6.3> RFC 8620, Section 6.3 *)
148148+module Copy_response : sig
149149+ type response
150150+151151+ (** Create a Blob/copy response.
152152+ @param from_account_id The account ID blobs were copied from
153153+ @param account_id The account ID blobs were copied to
154154+ @param ?copied Optional map of old blob IDs to new blob IDs for successful copies
155155+ @param ?not_copied Optional map of blob IDs to SetError objects for failed copies
156156+ @return Blob/copy response object *)
157157+ val create :
158158+ from_account_id:string ->
159159+ account_id:string ->
160160+ ?copied:(Id.t * Id.t) list ->
161161+ ?not_copied:(Id.t * Error.Set_error.t) list ->
162162+ unit ->
163163+ response
164164+165165+ (** Get the source account ID.
166166+ @return The account ID blobs were copied from *)
167167+ val from_account_id : response -> string
168168+169169+ (** Get the destination account ID.
170170+ @return The account ID blobs were copied to *)
171171+ val account_id : response -> string
172172+173173+ (** Get the successfully copied blobs.
174174+ @return Map of old blob IDs to new blob IDs for successful copies, or empty list if none *)
175175+ val copied : response -> (Id.t * Id.t) list
176176+177177+ (** Get the not copied blobs.
178178+ @return Map of blob IDs to SetError objects for failed copies, or empty list if none *)
179179+ val not_copied : response -> (Id.t * Error.Set_error.t) list
180180+181181+ (** JSON serialization for Blob/copy responses *)
182182+ include Jmap_sigs.JSONABLE with type t := response
183183+end
···1919 | Email_set_data of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
2020 | Email_changes_data of Methods.Changes_response.t
2121 (* | Email_query_changes_data of Methods.Query_changes_response.t (* Not yet implemented *) *)
2222+ | Email_import_data of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
2323+ | Email_parse_data of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
2424+ | Blob_get_data of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
2525+ | Blob_copy_data of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
2226 | Mailbox_get_data of Yojson.Safe.t Methods.Get_response.t
2327 | Mailbox_set_data of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
2428 | Mailbox_query_data of Methods.Query_response.t
···3438 | Email_submission_changes_data of Methods.Changes_response.t
3539 | Vacation_response_get_data of Yojson.Safe.t Methods.Get_response.t
3640 | Vacation_response_set_data of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
4141+ | SearchSnippet_get_data of Yojson.Safe.t Methods.Get_response.t
3742 | Error_data of Error.error
38433944type t = {
···5055 | Email_set_response of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
5156 | Email_changes_response of Methods.Changes_response.t
5257 (* | Email_query_changes_response of Methods.Query_changes_response.t (* Not yet implemented *) *)
5858+ | Email_import_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
5959+ | Email_parse_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
6060+ | Blob_get_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
6161+ | Blob_copy_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
5362 | Mailbox_get_response of Yojson.Safe.t Methods.Get_response.t
5463 | Mailbox_set_response of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
5564 | Mailbox_query_response of Methods.Query_response.t
···6574 | Email_submission_changes_response of Methods.Changes_response.t
6675 | Vacation_response_get_response of Yojson.Safe.t Methods.Get_response.t
6776 | Vacation_response_set_response of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
7777+ | SearchSnippet_get_response of Yojson.Safe.t Methods.Get_response.t
68786979(** {1 Response Creation} *)
7080···239249 | Ok set_resp -> Ok (Vacation_response_set_data set_resp)
240250 | Error err -> Error (Error.error_to_string err))
241251252252+ | Some `SearchSnippet_get ->
253253+ parse_stage "SearchSnippet/get response" (fun j ->
254254+ match Methods.Get_response.of_json ~from_json:(fun j -> j) j with
255255+ | Ok get_resp -> Ok (SearchSnippet_get_data get_resp)
256256+ | Error err -> Error (Error.error_to_string err))
257257+258258+ | Some `Email_import ->
259259+ (* Using raw JSON for now - will implement proper parsing later *)
260260+ Ok (Email_import_data json)
261261+262262+ | Some `Email_parse ->
263263+ (* Using raw JSON for now - will implement proper parsing later *)
264264+ Ok (Email_parse_data json)
265265+266266+ | Some `Blob_get ->
267267+ (* Using raw JSON for now - will implement proper parsing later *)
268268+ Ok (Blob_get_data json)
269269+270270+ | Some `Blob_copy ->
271271+ (* Using raw JSON for now - will implement proper parsing later *)
272272+ Ok (Blob_copy_data json)
273273+242274 (* Not yet implemented methods - return error for now *)
243243- | Some (`Blob_get | `Blob_lookup | `Email_parse | `Email_copy | `SearchSnippet_get
244244- | `Thread_query | `Email_import | `Blob_copy) ->
275275+ | Some (`Blob_lookup | `Email_copy
276276+ | `Thread_query) ->
245277 let ctx = Error_context.create ~method_name ?call_id
246278 ~response_data:json ~parsing_stage:"method validation" () in
247279 Error (Error_context.to_string ctx ^ ": method not implemented")
···292324 | Email_set_data data -> Email_set_response data
293325 | Email_changes_data data -> Email_changes_response data
294326 (* | Email_query_changes_data data -> Email_query_changes_response data (* Not yet implemented *) *)
327327+ | Email_import_data data -> Email_import_response data
328328+ | Email_parse_data data -> Email_parse_response data
329329+ | Blob_get_data data -> Blob_get_response data
330330+ | Blob_copy_data data -> Blob_copy_response data
295331 | Mailbox_get_data data -> Mailbox_get_response data
296332 | Mailbox_set_data data -> Mailbox_set_response data
297333 | Mailbox_query_data data -> Mailbox_query_response data
···307343 | Email_submission_changes_data data -> Email_submission_changes_response data
308344 | Vacation_response_get_data data -> Vacation_response_get_response data
309345 | Vacation_response_set_data data -> Vacation_response_set_response data
346346+ | SearchSnippet_get_data data -> SearchSnippet_get_response data
310347 | Error_data _ -> failwith "Error response does not have a response_type"
311348312349let method_name t = t.method_name
···537574 let new_state t = Methods.Changes_response.new_state t
538575end
539576577577+module Email_import = struct
578578+ type t = Yojson.Safe.t (* Placeholder - using raw JSON for now *)
579579+ type account_id = string
580580+ type state = string
581581+582582+ let to_json t = t (* Pass through since t is already JSON *)
583583+584584+ let of_json json = Ok json (* Pass through since using raw JSON *)
585585+586586+ let pp fmt t =
587587+ Format.fprintf fmt "Email_import: %s" (Yojson.Safe.pretty_to_string t)
588588+ let pp_hum = pp
589589+590590+ let account_id t =
591591+ let open Yojson.Safe.Util in
592592+ t |> member "accountId" |> to_string
593593+594594+ let state t =
595595+ let open Yojson.Safe.Util in
596596+ t |> member "newState" |> to_string_option
597597+598598+ let is_error _ = false
599599+600600+ let created t =
601601+ let open Yojson.Safe.Util in
602602+ match t |> member "created" with
603603+ | `Null -> []
604604+ | json -> to_assoc json
605605+606606+ let not_created t =
607607+ let open Yojson.Safe.Util in
608608+ match t |> member "notCreated" with
609609+ | `Null -> []
610610+ | json -> to_assoc json (* Return raw JSON for now *)
611611+612612+ let old_state t =
613613+ let open Yojson.Safe.Util in
614614+ t |> member "oldState" |> to_string_option
615615+616616+ let new_state t =
617617+ let open Yojson.Safe.Util in
618618+ t |> member "newState" |> to_string_option
619619+end
620620+621621+module Email_parse = struct
622622+ type t = Yojson.Safe.t (* Placeholder - using raw JSON for now *)
623623+ type account_id = string
624624+ type state = string
625625+626626+ let to_json t = t (* Pass through since t is already JSON *)
627627+628628+ let of_json json = Ok json (* Pass through since using raw JSON *)
629629+630630+ let pp fmt t =
631631+ Format.fprintf fmt "Email_parse: %s" (Yojson.Safe.pretty_to_string t)
632632+ let pp_hum = pp
633633+634634+ let account_id t =
635635+ let open Yojson.Safe.Util in
636636+ t |> member "accountId" |> to_string
637637+638638+ let state _t = None (* Email/parse doesn't use state *)
639639+640640+ let is_error _ = false
641641+642642+ let parsed t =
643643+ let open Yojson.Safe.Util in
644644+ match t |> member "parsed" with
645645+ | `Null -> []
646646+ | json -> to_assoc json (* Return as string, Yojson.Safe.t pairs *)
647647+648648+ let not_parsable t =
649649+ let open Yojson.Safe.Util in
650650+ match t |> member "notParsable" with
651651+ | `Null -> []
652652+ | ids_json -> List.map to_string (to_list ids_json) (* Return as strings *)
653653+654654+ let not_found t =
655655+ let open Yojson.Safe.Util in
656656+ match t |> member "notFound" with
657657+ | `Null -> []
658658+ | ids_json -> List.map to_string (to_list ids_json) (* Return as strings *)
659659+end
660660+661661+module Blob_get = struct
662662+ type t = Yojson.Safe.t (* Placeholder - using raw JSON for now *)
663663+ type account_id = string
664664+ type state = string
665665+666666+ let to_json t = t (* Pass through since t is already JSON *)
667667+668668+ let of_json json = Ok json (* Pass through since using raw JSON *)
669669+670670+ let pp fmt t =
671671+ Format.fprintf fmt "Blob_get: %s" (Yojson.Safe.pretty_to_string t)
672672+ let pp_hum = pp
673673+674674+ let account_id t =
675675+ let open Yojson.Safe.Util in
676676+ t |> member "accountId" |> to_string
677677+678678+ let state _t = None (* Blob/get doesn't use state *)
679679+680680+ let is_error _ = false
681681+682682+ let list t =
683683+ let open Yojson.Safe.Util in
684684+ match t |> member "list" with
685685+ | `Null -> []
686686+ | json -> to_assoc json (* Return as string, Yojson.Safe.t pairs *)
687687+688688+ let not_found t =
689689+ let open Yojson.Safe.Util in
690690+ match t |> member "notFound" with
691691+ | `Null -> []
692692+ | ids_json -> List.map to_string (to_list ids_json) (* Return as strings *)
693693+end
694694+695695+module Blob_copy = struct
696696+ type t = Yojson.Safe.t (* Placeholder - using raw JSON for now *)
697697+ type account_id = string
698698+ type state = string
699699+700700+ let to_json t = t (* Pass through since t is already JSON *)
701701+702702+ let of_json json = Ok json (* Pass through since using raw JSON *)
703703+704704+ let pp fmt t =
705705+ Format.fprintf fmt "Blob_copy: %s" (Yojson.Safe.pretty_to_string t)
706706+ let pp_hum = pp
707707+708708+ let account_id t =
709709+ let open Yojson.Safe.Util in
710710+ t |> member "accountId" |> to_string
711711+712712+ let from_account_id t =
713713+ let open Yojson.Safe.Util in
714714+ t |> member "fromAccountId" |> to_string
715715+716716+ let state _t = None (* Blob/copy doesn't use state *)
717717+718718+ let is_error _ = false
719719+720720+ let copied t =
721721+ let open Yojson.Safe.Util in
722722+ match t |> member "copied" with
723723+ | `Null -> []
724724+ | json -> List.map (fun (old_id, new_id_json) ->
725725+ (old_id, to_string new_id_json)
726726+ ) (to_assoc json) (* Return as string pairs *)
727727+728728+ let not_copied t =
729729+ let open Yojson.Safe.Util in
730730+ match t |> member "notCopied" with
731731+ | `Null -> []
732732+ | json -> to_assoc json (* Return as string, Yojson.Safe.t pairs *)
733733+end
734734+540735module Mailbox_get = struct
541736 type t = Yojson.Safe.t Methods.Get_response.t
542737 type account_id = string
···10651260 let new_state t = Methods.Set_response.new_state t
10661261end
1067126212631263+module SearchSnippet_get = struct
12641264+ type t = Yojson.Safe.t Methods.Get_response.t
12651265+ type account_id = string
12661266+ type state = string
12671267+12681268+ let to_json t =
12691269+ `Assoc [
12701270+ ("accountId", `String (Methods.Get_response.account_id t));
12711271+ ("list", `List (Methods.Get_response.list t));
12721272+ ("notFound", `List (List.map (fun s -> `String s) (Methods.Get_response.not_found t)));
12731273+ ]
12741274+12751275+ let of_json json =
12761276+ match Methods.Get_response.of_json ~from_json:(fun j -> j) json with
12771277+ | Ok t -> Ok t
12781278+ | Error err -> Error ("Failed to parse SearchSnippet_get response: " ^ error_message err)
12791279+12801280+ let pp fmt t =
12811281+ let json = to_json t in
12821282+ Format.fprintf fmt "SearchSnippet_get: %s" (Yojson.Safe.pretty_to_string json)
12831283+ let pp_hum = pp
12841284+12851285+ let account_id t = Methods.Get_response.account_id t
12861286+ let state t = Some (Methods.Get_response.state t)
12871287+ let is_error _ = false
12881288+12891289+ let list t = Methods.Get_response.list t
12901290+ let not_found t = Methods.Get_response.not_found t
12911291+end
12921292+10681293(** {1 Response Data Extraction Functions} *)
1069129410701295(** Extract typed response data from the main response type *)
···11681393 | Vacation_response_set_data data -> Some data
11691394 | _ -> None
1170139513961396+let get_search_snippet_get t : SearchSnippet_get.t option =
13971397+ match t.data with
13981398+ | SearchSnippet_get_data data -> Some data
13991399+ | _ -> None
14001400+14011401+let get_email_import t : Email_import.t option =
14021402+ match t.data with
14031403+ | Email_import_data data -> Some data
14041404+ | _ -> None
14051405+14061406+let get_email_parse t : Email_parse.t option =
14071407+ match t.data with
14081408+ | Email_parse_data data -> Some data
14091409+ | _ -> None
14101410+14111411+let get_blob_get t : Blob_get.t option =
14121412+ match t.data with
14131413+ | Blob_get_data data -> Some data
14141414+ | _ -> None
14151415+14161416+let get_blob_copy t : Blob_copy.t option =
14171417+ match t.data with
14181418+ | Blob_copy_data data -> Some data
14191419+ | _ -> None
14201420+14211421+let _ = get_search_snippet_get (* Acknowledge usage for extraction functions *)
14221422+let _ = get_email_import (* Acknowledge usage for extraction functions *)
14231423+let _ = get_email_parse (* Acknowledge usage for extraction functions *)
14241424+let _ = get_blob_get (* Acknowledge usage for extraction functions *)
14251425+let _ = get_blob_copy (* Acknowledge usage for extraction functions *)
14261426+11711427(** {1 Method Chaining Support} *)
1172142811731429(** Batch response processing for method chains *)
···13491605 | Email_get_data _ -> method_to_string `Email_get
13501606 | Email_set_data _ -> method_to_string `Email_set
13511607 | Email_changes_data _ -> method_to_string `Email_changes
16081608+ | Email_import_data _ -> method_to_string `Email_import
16091609+ | Email_parse_data _ -> method_to_string `Email_parse
16101610+ | Blob_get_data _ -> method_to_string `Blob_get
16111611+ | Blob_copy_data _ -> method_to_string `Blob_copy
13521612 | Mailbox_get_data _ -> method_to_string `Mailbox_get
13531613 | Mailbox_query_data _ -> method_to_string `Mailbox_query
13541614 | Mailbox_set_data _ -> method_to_string `Mailbox_set
···13641624 | Email_submission_changes_data _ -> method_to_string `EmailSubmission_changes
13651625 | Vacation_response_get_data _ -> method_to_string `VacationResponse_get
13661626 | Vacation_response_set_data _ -> method_to_string `VacationResponse_set
16271627+ | SearchSnippet_get_data _ -> method_to_string `SearchSnippet_get
13671628 | Error_data _ -> "Error"
13681629 );
13691630 (match error t with
···14261687 ((match t.data with Vacation_response_get_data _ -> true | _ -> false), "VacationResponse/get")
14271688 | Some `VacationResponse_set ->
14281689 ((match t.data with Vacation_response_set_data _ -> true | _ -> false), "VacationResponse/set")
16901690+ | Some `SearchSnippet_get ->
16911691+ ((match t.data with SearchSnippet_get_data _ -> true | _ -> false), "SearchSnippet/get")
16921692+ | Some `Email_import ->
16931693+ ((match t.data with Email_import_data _ -> true | _ -> false), "Email/import")
16941694+ | Some `Email_parse ->
16951695+ ((match t.data with Email_parse_data _ -> true | _ -> false), "Email/parse")
16961696+ | Some `Blob_get ->
16971697+ ((match t.data with Blob_get_data _ -> true | _ -> false), "Blob/get")
16981698+ | Some `Blob_copy ->
16991699+ ((match t.data with Blob_copy_data _ -> true | _ -> false), "Blob/copy")
14291700 (* Not yet implemented methods *)
14301430- | Some (`Blob_get | `Blob_lookup | `Email_parse | `Email_copy | `SearchSnippet_get
14311431- | `Thread_query | `Email_import | `Blob_copy) ->
17011701+ | Some (`Blob_lookup | `Email_copy
17021702+ | `Thread_query) ->
14321703 (false, "unimplemented method")
14331704 | None ->
14341705 ((match t.data with Error_data _ -> true | _ -> false), "error response")
···14551726 | Email_submission_changes_data _ -> "EmailSubmission/changes"
14561727 | Vacation_response_get_data _ -> "VacationResponse/get"
14571728 | Vacation_response_set_data _ -> "VacationResponse/set"
17291729+ | SearchSnippet_get_data _ -> "SearchSnippet/get"
17301730+ | Email_import_data _ -> "Email/import"
17311731+ | Email_parse_data _ -> "Email/parse"
17321732+ | Blob_get_data _ -> "Blob/get"
17331733+ | Blob_copy_data _ -> "Blob/copy"
14581734 | Error_data _ -> "error"
14591735 in
14601736 Error (Printf.sprintf "Response data type mismatch: method '%s' expects %s but got %s"
+72
jmap/jmap/response.mli
···4646 | Email_set_response of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
4747 | Email_changes_response of Methods.Changes_response.t
4848 (* | Email_query_changes_response of Methods.Query_changes_response.t (* Not yet implemented *) *)
4949+ | Email_import_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
5050+ | Email_parse_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
5151+ | Blob_get_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
5252+ | Blob_copy_response of Yojson.Safe.t (* Placeholder - using raw JSON for now *)
4953 | Mailbox_get_response of Yojson.Safe.t Methods.Get_response.t
5054 | Mailbox_set_response of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
5155 | Mailbox_query_response of Methods.Query_response.t
···6165 | Email_submission_changes_response of Methods.Changes_response.t
6266 | Vacation_response_get_response of Yojson.Safe.t Methods.Get_response.t
6367 | Vacation_response_set_response of (Yojson.Safe.t, Yojson.Safe.t) Methods.Set_response.t
6868+ | SearchSnippet_get_response of Yojson.Safe.t Methods.Get_response.t
64696570(** {1 Response Creation} *)
6671···194199 val new_state : t -> string
195200end
196201202202+(** Email/import response - implements METHOD_RESPONSE for import operations *)
203203+module Email_import : sig
204204+ include Jmap_sigs.METHOD_RESPONSE with type t = Yojson.Safe.t (* Using placeholder for now *)
205205+206206+ (** Extract created emails from response *)
207207+ val created : t -> (string * Yojson.Safe.t) list (* Using placeholder for now *)
208208+209209+ (** Extract not created emails from response *)
210210+ val not_created : t -> (string * Yojson.Safe.t) list (* Using placeholder for now *)
211211+212212+ (** Extract old state from response *)
213213+ val old_state : t -> string option
214214+215215+ (** Extract new state from response *)
216216+ val new_state : t -> string option
217217+end
218218+219219+(** Email/parse response - implements METHOD_RESPONSE for parse operations *)
220220+module Email_parse : sig
221221+ include Jmap_sigs.METHOD_RESPONSE with type t = Yojson.Safe.t (* Using placeholder for now *)
222222+223223+ (** Extract parsed emails from response *)
224224+ val parsed : t -> (string * Yojson.Safe.t) list (* Using string IDs for now *)
225225+226226+ (** Extract not parsable blob IDs from response *)
227227+ val not_parsable : t -> string list
228228+229229+ (** Extract not found blob IDs from response *)
230230+ val not_found : t -> string list
231231+end
232232+233233+(** Blob/get response - implements METHOD_RESPONSE for blob metadata operations *)
234234+module Blob_get : sig
235235+ include Jmap_sigs.METHOD_RESPONSE with type t = Yojson.Safe.t (* Using placeholder for now *)
236236+237237+ (** Extract blob objects from response *)
238238+ val list : t -> (string * Yojson.Safe.t) list (* Using placeholder for now *)
239239+240240+ (** Extract not found blob IDs from response *)
241241+ val not_found : t -> string list
242242+end
243243+244244+(** Blob/copy response - implements METHOD_RESPONSE for blob copy operations *)
245245+module Blob_copy : sig
246246+ include Jmap_sigs.METHOD_RESPONSE with type t = Yojson.Safe.t (* Using placeholder for now *)
247247+248248+ (** Extract source account ID from response *)
249249+ val from_account_id : t -> string
250250+251251+ (** Extract successfully copied blobs from response *)
252252+ val copied : t -> (string * string) list (* Using string IDs for now *)
253253+254254+ (** Extract not copied blob IDs from response *)
255255+ val not_copied : t -> (string * Yojson.Safe.t) list (* Using placeholders for now *)
256256+end
257257+197258(** Mailbox/get response - implements METHOD_RESPONSE for get operations *)
198259module Mailbox_get : sig
199260 include Jmap_sigs.METHOD_RESPONSE with type t = Yojson.Safe.t Methods.Get_response.t
···384445385446 (** Extract new state from response *)
386447 val new_state : t -> string
448448+end
449449+450450+(** SearchSnippet/get response - implements METHOD_RESPONSE for get operations *)
451451+module SearchSnippet_get : sig
452452+ include Jmap_sigs.METHOD_RESPONSE with type t = Yojson.Safe.t Methods.Get_response.t
453453+454454+ (** Extract search snippet objects from get response *)
455455+ val list : t -> Yojson.Safe.t list
456456+457457+ (** Extract not found IDs from response *)
458458+ val not_found : t -> string list
387459end
388460389461(** {1 Response Data Extraction Functions} *)