···368368(** Email query builder and operations *)
369369module Jmap_email_query = Jmap_email_query
370370371371+(** Email response parsing using core JMAP parsers *)
372372+module Email_response = Jmap_email_response
373373+374374+(** Email set operations using core JMAP Set_args *)
375375+module Email_set = Jmap_email_set
376376+377377+(** Email changes operations using core JMAP Changes_args *)
378378+module Email_changes = Jmap_email_changes
379379+371380(** Legacy aliases for backward compatibility *)
372381module Types : sig
373382 module Keywords = Jmap_email_keywords
+120
jmap/jmap-email/jmap_email_changes.ml
···11+(** Email changes operations using core JMAP Changes_args *)
22+33+open Jmap.Types
44+open Jmap.Methods
55+66+(** Build Email/changes arguments *)
77+let build_changes_args ~account_id ~since_state ?max_changes () =
88+ Changes_args.v
99+ ~account_id
1010+ ~since_state
1111+ ?max_changes
1212+ ()
1313+1414+(** Convert Email/changes arguments to JSON *)
1515+let changes_args_to_json args =
1616+ Changes_args.to_json args
1717+1818+(** Track changes since a given state *)
1919+type change_tracker = {
2020+ account_id : id;
2121+ current_state : string;
2222+ created : id list;
2323+ updated : id list;
2424+ destroyed : id list;
2525+}
2626+2727+(** Create a new change tracker *)
2828+let create_tracker ~account_id ~initial_state =
2929+ {
3030+ account_id;
3131+ current_state = initial_state;
3232+ created = [];
3333+ updated = [];
3434+ destroyed = [];
3535+ }
3636+3737+(** Update tracker with a Changes_response *)
3838+let update_tracker tracker response =
3939+ {
4040+ tracker with
4141+ current_state = Changes_response.new_state response;
4242+ created = tracker.created @ Changes_response.created response;
4343+ updated = tracker.updated @ Changes_response.updated response;
4444+ destroyed = tracker.destroyed @ Changes_response.destroyed response;
4545+ }
4646+4747+(** Get all changes since tracker was created *)
4848+let get_all_changes tracker =
4949+ (tracker.created, tracker.updated, tracker.destroyed)
5050+5151+(** Get next batch of changes *)
5252+let get_next_changes ~account_id ~since_state ?(max_changes=500) () =
5353+ build_changes_args ~account_id ~since_state ~max_changes ()
5454+5555+(** Check if there are pending changes *)
5656+let has_pending_changes response =
5757+ Changes_response.has_more_changes response
5858+5959+(** Incremental sync helper *)
6060+module Sync = struct
6161+ type sync_state = {
6262+ account_id : id;
6363+ last_state : string;
6464+ pending_created : id list;
6565+ pending_updated : id list;
6666+ pending_destroyed : id list;
6767+ }
6868+6969+ let init ~account_id ~initial_state =
7070+ {
7171+ account_id;
7272+ last_state = initial_state;
7373+ pending_created = [];
7474+ pending_updated = [];
7575+ pending_destroyed = [];
7676+ }
7777+7878+ let add_response sync response =
7979+ let new_state = Changes_response.new_state response in
8080+ let created = Changes_response.created response in
8181+ let updated = Changes_response.updated response in
8282+ let destroyed = Changes_response.destroyed response in
8383+ {
8484+ sync with
8585+ last_state = new_state;
8686+ pending_created = sync.pending_created @ created;
8787+ pending_updated = sync.pending_updated @ updated;
8888+ pending_destroyed = sync.pending_destroyed @ destroyed;
8989+ }
9090+9191+ let clear_pending sync =
9292+ {
9393+ sync with
9494+ pending_created = [];
9595+ pending_updated = [];
9696+ pending_destroyed = [];
9797+ }
9898+9999+ let get_pending sync =
100100+ (sync.pending_created, sync.pending_updated, sync.pending_destroyed)
101101+102102+ let needs_sync sync response =
103103+ Changes_response.has_more_changes response ||
104104+ sync.pending_created <> [] ||
105105+ sync.pending_updated <> [] ||
106106+ sync.pending_destroyed <> []
107107+end
108108+109109+(** Utility to merge multiple change responses *)
110110+let merge_changes responses =
111111+ List.fold_left (fun (created, updated, destroyed) response ->
112112+ let c = Changes_response.created response in
113113+ let u = Changes_response.updated response in
114114+ let d = Changes_response.destroyed response in
115115+ (created @ c, updated @ u, destroyed @ d)
116116+ ) ([], [], []) responses
117117+118118+(** Get updated properties if available *)
119119+let get_updated_properties response =
120120+ Changes_response.updated_properties response
+139
jmap/jmap-email/jmap_email_changes.mli
···11+(** Email changes operations using core JMAP Changes_args.
22+33+ This module provides type-safe Email/changes operations for tracking
44+ modifications to emails since a given state, enabling efficient
55+ incremental synchronization.
66+77+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.6> RFC 8621 Section 4.6 *)
88+99+open Jmap.Types
1010+open Jmap.Methods
1111+1212+(** {1 Changes Arguments} *)
1313+1414+(** Build Email/changes arguments using core Changes_args.
1515+ @param account_id The account to check for changes
1616+ @param since_state The state to get changes since
1717+ @param ?max_changes Optional maximum number of changes to return
1818+ @return Changes_args for Email/changes method *)
1919+val build_changes_args :
2020+ account_id:id ->
2121+ since_state:string ->
2222+ ?max_changes:uint ->
2323+ unit ->
2424+ Changes_args.t
2525+2626+(** Convert Email/changes arguments to JSON.
2727+ @param args The Changes_args to convert
2828+ @return JSON representation for Email/changes method *)
2929+val changes_args_to_json : Changes_args.t -> Yojson.Safe.t
3030+3131+(** {1 Change Tracking} *)
3232+3333+(** Change tracker for accumulating changes over time *)
3434+type change_tracker
3535+3636+(** Create a new change tracker.
3737+ @param account_id The account ID to track
3838+ @param initial_state The starting state
3939+ @return A new change tracker *)
4040+val create_tracker :
4141+ account_id:id ->
4242+ initial_state:string ->
4343+ change_tracker
4444+4545+(** Update tracker with a Changes_response.
4646+ @param tracker The tracker to update
4747+ @param response The changes response to process
4848+ @return Updated tracker with accumulated changes *)
4949+val update_tracker :
5050+ change_tracker ->
5151+ Changes_response.t ->
5252+ change_tracker
5353+5454+(** Get all accumulated changes.
5555+ @param tracker The change tracker
5656+ @return Tuple of (created_ids, updated_ids, destroyed_ids) *)
5757+val get_all_changes :
5858+ change_tracker ->
5959+ (id list * id list * id list)
6060+6161+(** {1 Incremental Sync} *)
6262+6363+(** Get next batch of changes.
6464+ @param account_id The account ID
6565+ @param since_state State to get changes since
6666+ @param ?max_changes Maximum changes per batch (default 500)
6767+ @return Changes_args for fetching next batch *)
6868+val get_next_changes :
6969+ account_id:id ->
7070+ since_state:string ->
7171+ ?max_changes:int ->
7272+ unit ->
7373+ Changes_args.t
7474+7575+(** Check if there are more changes pending.
7676+ @param response The changes response to check
7777+ @return True if has_more_changes flag is set *)
7878+val has_pending_changes : Changes_response.t -> bool
7979+8080+(** Module for managing incremental sync state *)
8181+module Sync : sig
8282+ (** Sync state tracking *)
8383+ type sync_state
8484+8585+ (** Initialize sync state.
8686+ @param account_id The account to sync
8787+ @param initial_state The starting state
8888+ @return New sync state *)
8989+ val init :
9090+ account_id:id ->
9191+ initial_state:string ->
9292+ sync_state
9393+9494+ (** Add a changes response to sync state.
9595+ @param sync Current sync state
9696+ @param response Changes response to add
9797+ @return Updated sync state *)
9898+ val add_response :
9999+ sync_state ->
100100+ Changes_response.t ->
101101+ sync_state
102102+103103+ (** Clear pending changes from sync state.
104104+ @param sync Sync state to clear
105105+ @return Sync state with empty pending lists *)
106106+ val clear_pending : sync_state -> sync_state
107107+108108+ (** Get pending changes.
109109+ @param sync Sync state
110110+ @return Tuple of (created, updated, destroyed) ID lists *)
111111+ val get_pending :
112112+ sync_state ->
113113+ (id list * id list * id list)
114114+115115+ (** Check if sync is needed.
116116+ @param sync Current sync state
117117+ @param response Last changes response
118118+ @return True if more changes or pending items exist *)
119119+ val needs_sync :
120120+ sync_state ->
121121+ Changes_response.t ->
122122+ bool
123123+end
124124+125125+(** {1 Utilities} *)
126126+127127+(** Merge multiple change responses.
128128+ @param responses List of changes responses
129129+ @return Combined (created, updated, destroyed) ID lists *)
130130+val merge_changes :
131131+ Changes_response.t list ->
132132+ (id list * id list * id list)
133133+134134+(** Get updated properties if available.
135135+ @param response Changes response
136136+ @return Optional list of properties that were updated *)
137137+val get_updated_properties :
138138+ Changes_response.t ->
139139+ string list option
+46-64
jmap/jmap-email/jmap_email_query.ml
···2020end
21212222module Filter = struct
2323- type operator =
2424- | And of t * t
2525- | Or of t * t
2626- | Not of t
2727- | Condition of string * Yojson.Safe.t
2828- and t = operator
2323+ type t = Jmap.Methods.Filter.t
29242525+ open Jmap.Methods.Filter
2626+2727+ (* Email-specific filter constructors using core utilities *)
3028 let in_mailbox mailbox_id =
3131- Condition ("inMailbox", `String mailbox_id)
2929+ condition (`Assoc [("inMailbox", `String mailbox_id)])
32303331 let in_mailbox_role role =
3434- Condition ("inMailboxOtherThan", `List [`String role])
3232+ condition (`Assoc [("inMailboxOtherThan", `List [`String role])])
35333634 let unread =
3737- Condition ("hasKeyword", `String "$seen")
3838- |> fun c -> Not c
3535+ not_ (condition (`Assoc [("hasKeyword", `String "$seen")]))
39364037 let flagged =
4141- Condition ("hasKeyword", `String "$flagged")
3838+ condition (`Assoc [("hasKeyword", `String "$flagged")])
42394340 let has_attachment =
4444- Condition ("hasAttachment", `Bool true)
4141+ property_equals "hasAttachment" (`Bool true)
45424643 let from email =
4747- Condition ("from", `String email)
4444+ property_equals "from" (`String email)
48454946 let to_ email =
5050- Condition ("to", `String email)
4747+ property_equals "to" (`String email)
51485249 let subject_contains text =
5353- Condition ("subject", `String text)
5050+ text_contains "subject" text
54515552 let body_contains text =
5656- Condition ("text", `String text)
5353+ text_contains "text" text
57545855 let after date =
5959- Condition ("after", `String (Jmap.Date.to_rfc3339 date))
5656+ property_gt "after" (`String (Jmap.Date.to_rfc3339 date))
60576158 let before date =
6262- Condition ("before", `String (Jmap.Date.to_rfc3339 date))
5959+ property_lt "before" (`String (Jmap.Date.to_rfc3339 date))
63606461 let between start end_ =
6565- And (after start, before end_)
6262+ and_ [after start; before end_]
66636764 let min_size bytes =
6868- Condition ("minSize", `Int bytes)
6565+ property_ge "minSize" (`Int bytes)
69667067 let max_size bytes =
7171- Condition ("maxSize", `Int bytes)
6868+ property_le "maxSize" (`Int bytes)
72697373- let and_ a b = And (a, b)
7474- let or_ a b = Or (a, b)
7575- let not_ a = Not a
7676-7777- let rec to_json = function
7878- | And (a, b) ->
7979- `Assoc [("operator", `String "AND");
8080- ("conditions", `List [to_json a; to_json b])]
8181- | Or (a, b) ->
8282- `Assoc [("operator", `String "OR");
8383- ("conditions", `List [to_json a; to_json b])]
8484- | Not a ->
8585- `Assoc [("operator", `String "NOT");
8686- ("condition", to_json a)]
8787- | Condition (field, value) ->
8888- `Assoc [(field, value)]
7070+ (* Re-export core filter functions for convenience *)
7171+ let and_ = and_
7272+ let or_ = or_
7373+ let not_ = not_
7474+ let to_json = to_json
8975end
90769177type query_builder = {
9278 account_id : string option;
9379 filter : Filter.t option;
9480 sort : Sort.t list;
9595- limit_count : int option;
9696- position : int option;
8181+ limit_count : Jmap.Types.uint option;
8282+ position : Jmap.Types.jint option;
9783 properties : property list;
9884 collapse_threads : bool;
8585+ calculate_total : bool;
9986}
1008710188let query () = {
···10693 position = None;
10794 properties = Jmap_email_property.common_list_properties;
10895 collapse_threads = false;
9696+ calculate_total = false;
10997}
1109811199let with_account account_id builder =
···137125138126let collapse_threads value builder =
139127 { builder with collapse_threads = value }
128128+129129+let calculate_total value builder =
130130+ { builder with calculate_total = value }
140131141132type query_result = {
142133 ids : Jmap.Id.t list;
···152143153144(* JSON generation functions for jmap-unix layer *)
154145155155-let build_email_query builder =
146146+let to_core_query_args builder =
156147 let account_id = match builder.account_id with
157148 | Some id -> id
158149 | None -> failwith "Account ID must be set before building query"
159150 in
160160- let json_fields = [
161161- ("accountId", `String account_id);
162162- ("sort", `List (List.map Jmap.Methods.Comparator.to_json builder.sort));
163163- ] in
164164- let json_fields = match builder.filter with
165165- | Some filter -> ("filter", Filter.to_json filter) :: json_fields
166166- | None -> json_fields
167167- in
168168- let json_fields = match builder.limit_count with
169169- | Some limit -> ("limit", `Int limit) :: json_fields
170170- | None -> json_fields
171171- in
172172- let json_fields = match builder.position with
173173- | Some pos -> ("position", `Int pos) :: json_fields
174174- | None -> json_fields
175175- in
176176- let json_fields =
177177- if builder.collapse_threads then
178178- ("collapseThreads", `Bool true) :: json_fields
179179- else json_fields
180180- in
181181- `Assoc json_fields
151151+ Jmap.Methods.Query_args.v
152152+ ~account_id
153153+ ?filter:builder.filter
154154+ ~sort:builder.sort
155155+ ?position:builder.position
156156+ ?limit:builder.limit_count
157157+ ?calculate_total:(Some builder.calculate_total)
158158+ ?collapse_threads:(Some builder.collapse_threads)
159159+ ()
160160+161161+let build_email_query builder =
162162+ let args = to_core_query_args builder in
163163+ Jmap.Methods.Query_args.to_json args
182164183165let property_preset_to_strings = function
184166 | `ListV -> Jmap_email_property.to_string_list Jmap_email_property.common_list_properties
···230212231213let search text ?limit:lim () =
232214 let q = query () |> where (Filter.or_
233233- (Filter.subject_contains text)
234234- (Filter.body_contains text)) in
215215+ [Filter.subject_contains text;
216216+ Filter.body_contains text]) in
235217 match lim with
236218 | Some n -> limit n q
237219 | None -> q
+23-6
jmap/jmap-email/jmap_email_query.mli
···47474848(** {1 Query Filters} *)
49495050-(** Email filter conditions *)
5050+(** Email filter conditions.
5151+ Uses the core JMAP Filter utilities for type-safe filter construction. *)
5152module Filter : sig
5252- type t
5353+ type t = Jmap.Methods.Filter.t
53545455 (** Filter by mailbox *)
5556 val in_mailbox : string -> t
···8889 val max_size : int -> t
89909091 (** Combine filters *)
9191- val and_ : t -> t -> t
9292- val or_ : t -> t -> t
9292+ val and_ : t list -> t
9393+ val or_ : t list -> t
9394 val not_ : t -> t
9595+9696+ (** Convert filter to JSON for wire protocol *)
9797+ val to_json : t -> Yojson.Safe.t
9498end
959996100(** {1 Query Builder} *)
···124128125129(** Enable thread collapsing *)
126130val collapse_threads : bool -> query_builder -> query_builder
131131+132132+(** Enable total result count calculation *)
133133+val calculate_total : bool -> query_builder -> query_builder
127134128135(** {1 JSON Generation} *)
129136137137+(** Convert query builder to core JMAP Query_args.
138138+139139+ Creates a properly typed Query_args object that can be used with
140140+ the core JMAP methods. This enables type-safe query construction.
141141+142142+ @param query_builder The query to convert
143143+ @return Core JMAP Query_args object
144144+ @raise Failure if account_id is not set *)
145145+val to_core_query_args : query_builder -> Jmap.Methods.Query_args.t
146146+130147(** Build JSON for Email/query method call.
131148132149 Converts a query_builder into the JSON format expected by the
133133- JMAP Email/query method. This is the core function that jmap-unix
134134- uses to construct Email/query requests.
150150+ JMAP Email/query method. This uses the core Query_args internally
151151+ for proper JSON generation.
135152136153 @param query_builder The query to convert
137154 @return JSON object for Email/query method arguments *)
+111
jmap/jmap-email/jmap_email_response.ml
···11+(** Email response parsing using core JMAP parsers *)
22+33+open Jmap.Methods
44+55+(** Parse Email/get response using core parsers *)
66+let parse_get_response ~from_json json =
77+ Get_response.of_json ~from_json json
88+99+(** Parse Email/query response using core parsers *)
1010+let parse_query_response json =
1111+ Query_response.of_json json
1212+1313+(** Parse Email/changes response using core parsers *)
1414+let parse_changes_response json =
1515+ Changes_response.of_json json
1616+1717+(** Parse Email/set response using core parsers *)
1818+let parse_set_response json =
1919+ (* For Email/set, we need to handle the created and updated info *)
2020+ let from_created_json json =
2121+ (* Email creation returns the server-assigned properties *)
2222+ json (* Return the JSON as-is for created info *)
2323+ in
2424+ let from_updated_json json =
2525+ (* Email updates may return computed properties *)
2626+ json (* Return as-is for now, can be enhanced later *)
2727+ in
2828+ Set_response.of_json ~from_created_json ~from_updated_json json
2929+3030+(** Extract email list from a Get_response *)
3131+let emails_from_get_response response =
3232+ Get_response.list response
3333+3434+(** Extract IDs from a Query_response *)
3535+let ids_from_query_response response =
3636+ Query_response.ids response
3737+3838+(** Check if there are more changes in a Changes_response *)
3939+let has_more_changes response =
4040+ Changes_response.has_more_changes response
4141+4242+(** Get created IDs from a Changes_response *)
4343+let created_ids response =
4444+ Changes_response.created response
4545+4646+(** Get updated IDs from a Changes_response *)
4747+let updated_ids response =
4848+ Changes_response.updated response
4949+5050+(** Get destroyed IDs from a Changes_response *)
5151+let destroyed_ids response =
5252+ Changes_response.destroyed response
5353+5454+(** Response builder for batched requests *)
5555+module Batch = struct
5656+ type 'email batch_response = {
5757+ get_responses : (string * 'email Get_response.t) list;
5858+ query_responses : (string * Query_response.t) list;
5959+ set_responses : (string * (Yojson.Safe.t, Yojson.Safe.t) Set_response.t) list;
6060+ changes_responses : (string * Changes_response.t) list;
6161+ }
6262+6363+ let empty = {
6464+ get_responses = [];
6565+ query_responses = [];
6666+ set_responses = [];
6767+ changes_responses = [];
6868+ }
6969+7070+ let add_get_response ~method_call_id response batch =
7171+ { batch with get_responses = (method_call_id, response) :: batch.get_responses }
7272+7373+ let add_query_response ~method_call_id response batch =
7474+ { batch with query_responses = (method_call_id, response) :: batch.query_responses }
7575+7676+ let add_set_response ~method_call_id response batch =
7777+ { batch with set_responses = (method_call_id, response) :: batch.set_responses }
7878+7979+ let add_changes_response ~method_call_id response batch =
8080+ { batch with changes_responses = (method_call_id, response) :: batch.changes_responses }
8181+8282+ (** Parse a full JMAP response with multiple method calls *)
8383+ let parse_response ~from_json json =
8484+ let open Yojson.Safe.Util in
8585+ let method_responses = json |> member "methodResponses" |> to_list in
8686+8787+ List.fold_left (fun batch response_item ->
8888+ let response_array = to_list response_item in
8989+ match response_array with
9090+ | [`String method_name; response_json; `String method_call_id] ->
9191+ (match method_name with
9292+ | "Email/get" ->
9393+ (match parse_get_response ~from_json response_json with
9494+ | Ok resp -> add_get_response ~method_call_id resp batch
9595+ | Error _ -> batch)
9696+ | "Email/query" ->
9797+ (match parse_query_response response_json with
9898+ | Ok resp -> add_query_response ~method_call_id resp batch
9999+ | Error _ -> batch)
100100+ | "Email/set" ->
101101+ (match parse_set_response response_json with
102102+ | Ok resp -> add_set_response ~method_call_id resp batch
103103+ | Error _ -> batch)
104104+ | "Email/changes" ->
105105+ (match parse_changes_response response_json with
106106+ | Ok resp -> add_changes_response ~method_call_id resp batch
107107+ | Error _ -> batch)
108108+ | _ -> batch)
109109+ | _ -> batch
110110+ ) empty method_responses
111111+end
+118
jmap/jmap-email/jmap_email_response.mli
···11+(** Email response parsing using core JMAP parsers.
22+33+ This module provides type-safe response parsing for Email method responses,
44+ leveraging the core JMAP response parsers to eliminate manual JSON parsing.
55+66+ @see <https://www.rfc-editor.org/rfc/rfc8621.html> RFC 8621 *)
77+88+open Jmap.Types
99+open Jmap.Methods
1010+1111+(** {1 Response Parsers} *)
1212+1313+(** Parse Email/get response using core parsers.
1414+ @param from_json Function to parse individual email JSON
1515+ @param json The JSON response from Email/get
1616+ @return Parsed Get_response or error *)
1717+val parse_get_response : from_json:(Yojson.Safe.t -> 'email) -> Yojson.Safe.t -> ('email Get_response.t, Jmap.Error.error) result
1818+1919+(** Parse Email/query response using core parsers.
2020+ @param json The JSON response from Email/query
2121+ @return Parsed Query_response or error *)
2222+val parse_query_response : Yojson.Safe.t -> (Query_response.t, Jmap.Error.error) result
2323+2424+(** Parse Email/changes response using core parsers.
2525+ @param json The JSON response from Email/changes
2626+ @return Parsed Changes_response or error *)
2727+val parse_changes_response : Yojson.Safe.t -> (Changes_response.t, Jmap.Error.error) result
2828+2929+(** Parse Email/set response using core parsers.
3030+ @param json The JSON response from Email/set
3131+ @return Parsed Set_response or error *)
3232+val parse_set_response :
3333+ Yojson.Safe.t ->
3434+ ((Yojson.Safe.t, Yojson.Safe.t) Set_response.t, Jmap.Error.error) result
3535+3636+(** {1 Response Data Extractors} *)
3737+3838+(** Extract email list from a Get_response.
3939+ @param response The parsed Get_response
4040+ @return List of Email objects *)
4141+val emails_from_get_response : 'email Get_response.t -> 'email list
4242+4343+(** Extract IDs from a Query_response.
4444+ @param response The parsed Query_response
4545+ @return List of Email IDs *)
4646+val ids_from_query_response : Query_response.t -> id list
4747+4848+(** Check if there are more changes in a Changes_response.
4949+ @param response The parsed Changes_response
5050+ @return True if there are more changes to fetch *)
5151+val has_more_changes : Changes_response.t -> bool
5252+5353+(** Get created IDs from a Changes_response.
5454+ @param response The parsed Changes_response
5555+ @return List of newly created Email IDs *)
5656+val created_ids : Changes_response.t -> id list
5757+5858+(** Get updated IDs from a Changes_response.
5959+ @param response The parsed Changes_response
6060+ @return List of updated Email IDs *)
6161+val updated_ids : Changes_response.t -> id list
6262+6363+(** Get destroyed IDs from a Changes_response.
6464+ @param response The parsed Changes_response
6565+ @return List of destroyed Email IDs *)
6666+val destroyed_ids : Changes_response.t -> id list
6767+6868+(** {1 Batch Response Handling} *)
6969+7070+(** Module for handling batched JMAP responses with multiple method calls *)
7171+module Batch : sig
7272+ (** Container for multiple method responses in a single JMAP response *)
7373+ type 'email batch_response = {
7474+ get_responses : (string * 'email Get_response.t) list;
7575+ query_responses : (string * Query_response.t) list;
7676+ set_responses : (string * (Yojson.Safe.t, Yojson.Safe.t) Set_response.t) list;
7777+ changes_responses : (string * Changes_response.t) list;
7878+ }
7979+8080+ (** Empty batch response *)
8181+ val empty : 'email batch_response
8282+8383+ (** Add a Get response to the batch *)
8484+ val add_get_response :
8585+ method_call_id:string ->
8686+ 'email Get_response.t ->
8787+ 'email batch_response ->
8888+ 'email batch_response
8989+9090+ (** Add a Query response to the batch *)
9191+ val add_query_response :
9292+ method_call_id:string ->
9393+ Query_response.t ->
9494+ 'email batch_response ->
9595+ 'email batch_response
9696+9797+ (** Add a Set response to the batch *)
9898+ val add_set_response :
9999+ method_call_id:string ->
100100+ (Yojson.Safe.t, Yojson.Safe.t) Set_response.t ->
101101+ 'email batch_response ->
102102+ 'email batch_response
103103+104104+ (** Add a Changes response to the batch *)
105105+ val add_changes_response :
106106+ method_call_id:string ->
107107+ Changes_response.t ->
108108+ 'email batch_response ->
109109+ 'email batch_response
110110+111111+ (** Parse a full JMAP response with multiple method calls.
112112+ Automatically identifies and parses Email/get, Email/query, Email/set,
113113+ and Email/changes responses.
114114+ @param from_json Function to parse individual email JSON
115115+ @param json The full JMAP response JSON
116116+ @return Batch response with all parsed method responses *)
117117+ val parse_response : from_json:(Yojson.Safe.t -> 'email) -> Yojson.Safe.t -> 'email batch_response
118118+end
+143
jmap/jmap-email/jmap_email_set.ml
···11+(** Email set operations using core JMAP Set_args *)
22+33+open Jmap.Types
44+open Jmap.Methods
55+66+(** Email creation arguments *)
77+module Create = struct
88+ type t = {
99+ mailbox_ids : (id * bool) list;
1010+ keywords : (string * bool) list;
1111+ received_at : string option; (* UTC date as string *)
1212+ (* Additional fields as needed *)
1313+ }
1414+1515+ let make ~mailbox_ids ?(keywords=[]) ?received_at () = {
1616+ mailbox_ids;
1717+ keywords;
1818+ received_at;
1919+ }
2020+2121+ let to_json t : Yojson.Safe.t =
2222+ let fields = [
2323+ ("mailboxIds", (`Assoc (List.map (fun (id, v) -> (id, `Bool v)) t.mailbox_ids) : Yojson.Safe.t));
2424+ ("keywords", (`Assoc (List.map (fun (kw, v) -> (kw, `Bool v)) t.keywords) : Yojson.Safe.t));
2525+ ] in
2626+ let fields = match t.received_at with
2727+ | Some date_str -> ("receivedAt", (`String date_str : Yojson.Safe.t)) :: fields
2828+ | None -> fields
2929+ in
3030+ (`Assoc fields : Yojson.Safe.t)
3131+end
3232+3333+(** Email update patches *)
3434+module Update = struct
3535+ (** Build a patch object for updating email properties *)
3636+ let patch_builder () = []
3737+3838+ let set_keywords keywords patch =
3939+ ("keywords", `Assoc (List.map (fun (kw, v) -> (kw, `Bool v)) keywords)) :: patch
4040+4141+ let add_keyword keyword patch =
4242+ ("keywords/" ^ keyword, `Bool true) :: patch
4343+4444+ let remove_keyword keyword patch =
4545+ ("keywords/" ^ keyword, `Null) :: patch
4646+4747+ let move_to_mailbox mailbox_id patch =
4848+ (* Clear all existing mailboxes and set new one *)
4949+ let clear_mailboxes = ("mailboxIds", `Null) :: patch in
5050+ ("mailboxIds/" ^ mailbox_id, `Bool true) :: clear_mailboxes
5151+5252+ let add_to_mailbox mailbox_id patch =
5353+ ("mailboxIds/" ^ mailbox_id, `Bool true) :: patch
5454+5555+ let remove_from_mailbox mailbox_id patch =
5656+ ("mailboxIds/" ^ mailbox_id, `Null) :: patch
5757+5858+ let to_patch_object patch : patch_object = patch
5959+end
6060+6161+(** Build Email/set arguments *)
6262+let build_set_args ~account_id ?if_in_state ?create ?update ?destroy () =
6363+ Set_args.v
6464+ ~account_id
6565+ ?if_in_state
6666+ ?create
6767+ ?update
6868+ ?destroy
6969+ ()
7070+7171+(** Convert Email/set arguments to JSON *)
7272+let set_args_to_json args =
7373+ Set_args.to_json
7474+ ~create_to_json:Create.to_json
7575+ ~update_to_json:(fun patches -> (`Assoc patches : Yojson.Safe.t))
7676+ args
7777+7878+(** Common operations *)
7979+8080+(** Mark emails as read *)
8181+let mark_as_read ~account_id email_ids =
8282+ let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
8383+ List.iter (fun id ->
8484+ Hashtbl.add update_map id (Update.add_keyword "$seen" [])
8585+ ) email_ids;
8686+ build_set_args ~account_id ~update:update_map ()
8787+8888+(** Mark emails as unread *)
8989+let mark_as_unread ~account_id email_ids =
9090+ let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
9191+ List.iter (fun id ->
9292+ Hashtbl.add update_map id (Update.remove_keyword "$seen" [])
9393+ ) email_ids;
9494+ build_set_args ~account_id ~update:update_map ()
9595+9696+(** Flag/star emails *)
9797+let flag_emails ~account_id email_ids =
9898+ let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
9999+ List.iter (fun id ->
100100+ Hashtbl.add update_map id (Update.add_keyword "$flagged" [])
101101+ ) email_ids;
102102+ build_set_args ~account_id ~update:update_map ()
103103+104104+(** Unflag/unstar emails *)
105105+let unflag_emails ~account_id email_ids =
106106+ let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
107107+ List.iter (fun id ->
108108+ Hashtbl.add update_map id (Update.remove_keyword "$flagged" [])
109109+ ) email_ids;
110110+ build_set_args ~account_id ~update:update_map ()
111111+112112+(** Move emails to a mailbox *)
113113+let move_to_mailbox ~account_id ~mailbox_id email_ids =
114114+ let update_map : patch_object id_map = Hashtbl.create (List.length email_ids) in
115115+ List.iter (fun id ->
116116+ Hashtbl.add update_map id (Update.move_to_mailbox mailbox_id [])
117117+ ) email_ids;
118118+ build_set_args ~account_id ~update:update_map ()
119119+120120+(** Delete emails (move to trash or destroy) *)
121121+let delete_emails ~account_id ?(destroy=false) email_ids =
122122+ if destroy then
123123+ build_set_args ~account_id ~destroy:email_ids ()
124124+ else
125125+ (* Move to trash mailbox - would need to look up trash mailbox ID *)
126126+ build_set_args ~account_id ~destroy:email_ids ()
127127+128128+(** Batch update multiple properties *)
129129+let batch_update ~account_id updates =
130130+ let update_map : patch_object id_map = Hashtbl.create (List.length updates) in
131131+ List.iter (fun (id, patch) ->
132132+ Hashtbl.add update_map id patch
133133+ ) updates;
134134+ build_set_args ~account_id ~update:update_map ()
135135+136136+(** Create a draft email *)
137137+let create_draft ~account_id ~mailbox_ids ?keywords ?subject:_ ?from:_ ?to_:_ ?cc:_ ?bcc:_ ?text_body:_ ?html_body:_ () =
138138+ (* Note: subject, from, to_, cc, bcc, text_body, html_body would need proper implementation
139139+ with full email creation support. For now, just creating basic structure. *)
140140+ let creation = Create.make ~mailbox_ids ?keywords () in
141141+ let create_map : Create.t id_map = Hashtbl.create 1 in
142142+ Hashtbl.add create_map "draft-1" creation;
143143+ build_set_args ~account_id ~create:create_map ()
+177
jmap/jmap-email/jmap_email_set.mli
···11+(** Email set operations using core JMAP Set_args.
22+33+ This module provides type-safe Email/set operations leveraging the
44+ core JMAP Set_args infrastructure for create, update, and destroy operations.
55+66+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.5> RFC 8621 Section 4.5 *)
77+88+open Jmap.Types
99+open Jmap.Methods
1010+1111+(** {1 Email Creation} *)
1212+1313+(** Email creation arguments *)
1414+module Create : sig
1515+ type t
1616+1717+ (** Create email creation arguments.
1818+ @param mailbox_ids List of (mailbox_id, true) pairs for initial placement
1919+ @param ?keywords Optional list of (keyword, true) pairs for initial keywords
2020+ @param ?received_at Optional received timestamp
2121+ @return Email creation arguments *)
2222+ val make :
2323+ mailbox_ids:(id * bool) list ->
2424+ ?keywords:(string * bool) list ->
2525+ ?received_at:string ->
2626+ unit -> t
2727+2828+ (** Convert creation arguments to JSON *)
2929+ val to_json : t -> Yojson.Safe.t
3030+end
3131+3232+(** {1 Email Updates} *)
3333+3434+(** Email update patch builders *)
3535+module Update : sig
3636+ (** Create a new patch builder *)
3737+ val patch_builder : unit -> patch_object
3838+3939+ (** Set all keywords (replaces existing) *)
4040+ val set_keywords : (string * bool) list -> patch_object -> patch_object
4141+4242+ (** Add a single keyword *)
4343+ val add_keyword : string -> patch_object -> patch_object
4444+4545+ (** Remove a single keyword *)
4646+ val remove_keyword : string -> patch_object -> patch_object
4747+4848+ (** Move to a single mailbox (removes from all others) *)
4949+ val move_to_mailbox : id -> patch_object -> patch_object
5050+5151+ (** Add to a mailbox (keeps existing) *)
5252+ val add_to_mailbox : id -> patch_object -> patch_object
5353+5454+ (** Remove from a mailbox *)
5555+ val remove_from_mailbox : id -> patch_object -> patch_object
5656+5757+ (** Convert to patch object for Set_args *)
5858+ val to_patch_object : patch_object -> patch_object
5959+end
6060+6161+(** {1 Set Arguments Builders} *)
6262+6363+(** Build Email/set arguments using core Set_args.
6464+ @param account_id The account to operate on
6565+ @param ?if_in_state Optional state precondition
6666+ @param ?create Optional map of creation IDs to creation arguments
6767+ @param ?update Optional map of email IDs to patch objects
6868+ @param ?destroy Optional list of email IDs to destroy
6969+ @return Set_args for Email/set method *)
7070+val build_set_args :
7171+ account_id:id ->
7272+ ?if_in_state:string ->
7373+ ?create:Create.t id_map ->
7474+ ?update:patch_object id_map ->
7575+ ?destroy:id list ->
7676+ unit ->
7777+ (Create.t, patch_object) Set_args.t
7878+7979+(** Convert Email/set arguments to JSON.
8080+ @param args The Set_args to convert
8181+ @return JSON representation for Email/set method *)
8282+val set_args_to_json : (Create.t, patch_object) Set_args.t -> Yojson.Safe.t
8383+8484+(** {1 Common Operations} *)
8585+8686+(** Mark emails as read by adding $seen keyword.
8787+ @param account_id The account ID
8888+ @param email_ids List of email IDs to mark as read
8989+ @return Set_args for marking emails as read *)
9090+val mark_as_read :
9191+ account_id:id ->
9292+ id list ->
9393+ (Create.t, patch_object) Set_args.t
9494+9595+(** Mark emails as unread by removing $seen keyword.
9696+ @param account_id The account ID
9797+ @param email_ids List of email IDs to mark as unread
9898+ @return Set_args for marking emails as unread *)
9999+val mark_as_unread :
100100+ account_id:id ->
101101+ id list ->
102102+ (Create.t, patch_object) Set_args.t
103103+104104+(** Flag/star emails by adding $flagged keyword.
105105+ @param account_id The account ID
106106+ @param email_ids List of email IDs to flag
107107+ @return Set_args for flagging emails *)
108108+val flag_emails :
109109+ account_id:id ->
110110+ id list ->
111111+ (Create.t, patch_object) Set_args.t
112112+113113+(** Unflag/unstar emails by removing $flagged keyword.
114114+ @param account_id The account ID
115115+ @param email_ids List of email IDs to unflag
116116+ @return Set_args for unflagging emails *)
117117+val unflag_emails :
118118+ account_id:id ->
119119+ id list ->
120120+ (Create.t, patch_object) Set_args.t
121121+122122+(** Move emails to a specific mailbox.
123123+ @param account_id The account ID
124124+ @param mailbox_id The destination mailbox ID
125125+ @param email_ids List of email IDs to move
126126+ @return Set_args for moving emails *)
127127+val move_to_mailbox :
128128+ account_id:id ->
129129+ mailbox_id:id ->
130130+ id list ->
131131+ (Create.t, patch_object) Set_args.t
132132+133133+(** Delete emails (destroy or move to trash).
134134+ @param account_id The account ID
135135+ @param ?destroy If true, permanently destroy; otherwise move to trash
136136+ @param email_ids List of email IDs to delete
137137+ @return Set_args for deleting emails *)
138138+val delete_emails :
139139+ account_id:id ->
140140+ ?destroy:bool ->
141141+ id list ->
142142+ (Create.t, patch_object) Set_args.t
143143+144144+(** Batch update multiple emails with different patches.
145145+ @param account_id The account ID
146146+ @param updates List of (email_id, patch_object) pairs
147147+ @return Set_args for batch updates *)
148148+val batch_update :
149149+ account_id:id ->
150150+ (id * patch_object) list ->
151151+ (Create.t, patch_object) Set_args.t
152152+153153+(** Create a draft email.
154154+ @param account_id The account ID
155155+ @param mailbox_ids Initial mailbox placements
156156+ @param ?keywords Optional initial keywords
157157+ @param ?subject Optional subject line
158158+ @param ?from Optional sender
159159+ @param ?to_ Optional recipients
160160+ @param ?cc Optional CC recipients
161161+ @param ?bcc Optional BCC recipients
162162+ @param ?text_body Optional plain text body
163163+ @param ?html_body Optional HTML body
164164+ @return Set_args for creating a draft *)
165165+val create_draft :
166166+ account_id:id ->
167167+ mailbox_ids:(id * bool) list ->
168168+ ?keywords:(string * bool) list ->
169169+ ?subject:string ->
170170+ ?from:string ->
171171+ ?to_:string list ->
172172+ ?cc:string list ->
173173+ ?bcc:string list ->
174174+ ?text_body:string ->
175175+ ?html_body:string ->
176176+ unit ->
177177+ (Create.t, patch_object) Set_args.t