···11+(** Batch operations for JMAP Email.
22+33+ This module provides efficient batch operations for common
44+ email management tasks, minimizing round-trips to the server.
55+66+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.6> RFC 8621 Section 4.6 *)
77+88+(** {1 Batch Operations} *)
99+1010+(** Batch operation builder *)
1111+type t
1212+1313+(** Create a new batch operation *)
1414+val create : unit -> t
1515+1616+(** {2 Email Operations} *)
1717+1818+(** Mark emails as read *)
1919+val mark_read : Jmap.Id.t list -> t -> t
2020+2121+(** Mark emails as unread *)
2222+val mark_unread : Jmap.Id.t list -> t -> t
2323+2424+(** Add flag/star to emails *)
2525+val add_flag : Jmap.Id.t list -> t -> t
2626+2727+(** Remove flag/star from emails *)
2828+val remove_flag : Jmap.Id.t list -> t -> t
2929+3030+(** Add custom keyword to emails *)
3131+val add_keyword : string -> Jmap.Id.t list -> t -> t
3232+3333+(** Remove custom keyword from emails *)
3434+val remove_keyword : string -> Jmap.Id.t list -> t -> t
3535+3636+(** Move emails to a mailbox *)
3737+val move_to_mailbox : mailbox_id:string -> Jmap.Id.t list -> t -> t
3838+3939+(** Copy emails to a mailbox *)
4040+val copy_to_mailbox : mailbox_id:string -> Jmap.Id.t list -> t -> t
4141+4242+(** Remove emails from a mailbox *)
4343+val remove_from_mailbox : mailbox_id:string -> Jmap.Id.t list -> t -> t
4444+4545+(** Move emails to trash *)
4646+val move_to_trash : Jmap.Id.t list -> t -> t
4747+4848+(** Permanently delete emails *)
4949+val delete : Jmap.Id.t list -> t -> t
5050+5151+(** Archive emails (remove from inbox, keep in other mailboxes) *)
5252+val archive : Jmap.Id.t list -> t -> t
5353+5454+(** {2 Mailbox Operations} *)
5555+5656+(** Create a new mailbox *)
5757+val create_mailbox :
5858+ name:string ->
5959+ ?parent_id:string ->
6060+ ?role:string ->
6161+ t -> t
6262+6363+(** Rename a mailbox *)
6464+val rename_mailbox : Jmap.Id.t -> string -> t -> t
6565+6666+(** Delete a mailbox *)
6767+val delete_mailbox : Jmap.Id.t -> t -> t
6868+6969+(** {2 Execution} *)
7070+7171+(** Batch operation result *)
7272+type result = {
7373+ created : (string * Jmap.Id.t) list; (** Temporary ID -> Server ID mapping *)
7474+ updated : Jmap.Id.t list; (** Successfully updated IDs *)
7575+ destroyed : Jmap.Id.t list; (** Successfully destroyed IDs *)
7676+ not_created : (string * Jmap.Protocol.Error.Set_error.t) list;
7777+ not_updated : (Jmap.Id.t * Jmap.Protocol.Error.Set_error.t) list;
7878+ not_destroyed : (Jmap.Id.t * Jmap.Protocol.Error.Set_error.t) list;
7979+}
8080+8181+(** Execute batch operations *)
8282+val execute :
8383+ env:Eio_unix.Stdenv.base ->
8484+ ctx:Jmap_unix.context ->
8585+ session:Jmap.Protocol.Session.Session.t ->
8686+ ?account_id:string ->
8787+ t ->
8888+ (result, Jmap.Protocol.Error.error) result
8989+9090+(** {1 Common Workflows} *)
9191+9292+(** Process inbox - mark as read and archive *)
9393+val process_inbox :
9494+ env:Eio_unix.Stdenv.base ->
9595+ ctx:Jmap_unix.context ->
9696+ session:Jmap.Protocol.Session.Session.t ->
9797+ email_ids:Jmap.Id.t list ->
9898+ (result, Jmap.Protocol.Error.error) result
9999+100100+(** Bulk delete spam/trash emails older than N days *)
101101+val cleanup_old_emails :
102102+ env:Eio_unix.Stdenv.base ->
103103+ ctx:Jmap_unix.context ->
104104+ session:Jmap.Protocol.Session.Session.t ->
105105+ mailbox_role:string -> (* "spam" or "trash" *)
106106+ older_than_days:int ->
107107+ (result, Jmap.Protocol.Error.error) result
108108+109109+(** Organize emails by sender into mailboxes *)
110110+val organize_by_sender :
111111+ env:Eio_unix.Stdenv.base ->
112112+ ctx:Jmap_unix.context ->
113113+ session:Jmap.Protocol.Session.Session.t ->
114114+ rules:(string * string) list -> (* sender email -> mailbox name *)
115115+ (result, Jmap.Protocol.Error.error) result
116116+117117+(** {1 Progress Tracking} *)
118118+119119+(** Progress callback for long operations *)
120120+type progress = {
121121+ current : int;
122122+ total : int;
123123+ message : string;
124124+}
125125+126126+(** Execute with progress reporting *)
127127+val execute_with_progress :
128128+ env:Eio_unix.Stdenv.base ->
129129+ ctx:Jmap_unix.context ->
130130+ session:Jmap.Protocol.Session.Session.t ->
131131+ ?account_id:string ->
132132+ progress_fn:(progress -> unit) ->
133133+ t ->
134134+ (result, Jmap.Protocol.Error.error) result
+199
jmap/jmap-email/jmap_email_methods.mli
···11+(** JMAP Email method names and builders.
22+33+ This module provides type-safe method names and high-level
44+ builders for JMAP Email methods.
55+66+ @see <https://www.rfc-editor.org/rfc/rfc8621.html> RFC 8621 *)
77+88+(** {1 Method Names} *)
99+1010+(** Email method names as a polymorphic variant type *)
1111+type t = [
1212+ | `Email_get
1313+ | `Email_query
1414+ | `Email_queryChanges
1515+ | `Email_set
1616+ | `Email_copy
1717+ | `Email_import
1818+ | `Email_parse
1919+ | `EmailSubmission_get
2020+ | `EmailSubmission_query
2121+ | `EmailSubmission_queryChanges
2222+ | `EmailSubmission_set
2323+ | `Thread_get
2424+ | `Thread_query
2525+ | `Mailbox_get
2626+ | `Mailbox_query
2727+ | `Mailbox_queryChanges
2828+ | `Mailbox_set
2929+ | `Identity_get
3030+ | `Identity_query
3131+ | `Identity_set
3232+ | `VacationResponse_get
3333+ | `VacationResponse_set
3434+]
3535+3636+(** Convert method name to string for wire protocol *)
3737+val to_string : t -> string
3838+3939+(** Parse method name from string *)
4040+val of_string : string -> t option
4141+4242+(** Pretty-print a method name *)
4343+val pp : Format.formatter -> t -> unit
4444+4545+(** {1 Method Call ID Management} *)
4646+4747+(** Method call ID generator *)
4848+module CallId : sig
4949+ type t
5050+5151+ (** Create a call ID generator *)
5252+ val create : unit -> t
5353+5454+ (** Generate next call ID with optional prefix *)
5555+ val next : ?prefix:string -> t -> string
5656+5757+ (** Reset the generator *)
5858+ val reset : t -> unit
5959+end
6060+6161+(** {1 Request Builders} *)
6262+6363+(** High-level request builder that manages method chaining *)
6464+module RequestBuilder : sig
6565+ type t
6666+6767+ (** Create a new request builder *)
6868+ val create : Jmap_unix.context -> t
6969+7070+ (** Add Email/query method *)
7171+ val email_query :
7272+ ?account_id:string ->
7373+ ?filter:Jmap_email_query.Filter.t ->
7474+ ?sort:Jmap_email_query.Sort.t list ->
7575+ ?limit:int ->
7676+ ?position:int ->
7777+ t -> t
7878+7979+ (** Add Email/get method with automatic result reference *)
8080+ val email_get :
8181+ ?account_id:string ->
8282+ ?ids:Jmap.Id.t list ->
8383+ ?properties:Jmap_email_query.property list ->
8484+ ?reference_from:string -> (* Call ID to reference *)
8585+ t -> t
8686+8787+ (** Add Email/set method *)
8888+ val email_set :
8989+ ?account_id:string ->
9090+ ?create:(string * Jmap_email.t) list ->
9191+ ?update:(Jmap.Id.t * Jmap.Patch.t) list ->
9292+ ?destroy:Jmap.Id.t list ->
9393+ t -> t
9494+9595+ (** Add Thread/get method *)
9696+ val thread_get :
9797+ ?account_id:string ->
9898+ ?ids:Jmap.Id.t list ->
9999+ t -> t
100100+101101+ (** Add Mailbox/query method *)
102102+ val mailbox_query :
103103+ ?account_id:string ->
104104+ ?filter:Yojson.Safe.t ->
105105+ ?sort:Jmap.Methods.Comparator.t list ->
106106+ t -> t
107107+108108+ (** Add Mailbox/get method *)
109109+ val mailbox_get :
110110+ ?account_id:string ->
111111+ ?ids:Jmap.Id.t list ->
112112+ t -> t
113113+114114+ (** Execute the built request *)
115115+ val execute :
116116+ env:Eio_unix.Stdenv.base ->
117117+ session:Jmap.Protocol.Session.Session.t ->
118118+ t ->
119119+ (Jmap.Protocol.Response.t, Jmap.Protocol.Error.error) result
120120+121121+ (** Get specific method response by type *)
122122+ val get_response :
123123+ method_:t ->
124124+ ?call_id:string ->
125125+ Jmap.Protocol.Response.t ->
126126+ (Yojson.Safe.t, Jmap.Protocol.Error.error) result
127127+end
128128+129129+(** {1 Response Parsers} *)
130130+131131+(** Type-safe response extraction *)
132132+module Response : sig
133133+ (** Extract and parse Email/query response *)
134134+ val parse_email_query :
135135+ ?call_id:string ->
136136+ Jmap.Protocol.Response.t ->
137137+ (Jmap_email_query.query_result, Jmap.Protocol.Error.error) result
138138+139139+ (** Extract and parse Email/get response *)
140140+ val parse_email_get :
141141+ ?call_id:string ->
142142+ Jmap.Protocol.Response.t ->
143143+ (Jmap_email.t list, Jmap.Protocol.Error.error) result
144144+145145+ (** Extract and parse Thread/get response *)
146146+ val parse_thread_get :
147147+ ?call_id:string ->
148148+ Jmap.Protocol.Response.t ->
149149+ (Jmap_thread.t list, Jmap.Protocol.Error.error) result
150150+151151+ (** Extract and parse Mailbox/get response *)
152152+ val parse_mailbox_get :
153153+ ?call_id:string ->
154154+ Jmap.Protocol.Response.t ->
155155+ (Jmap_mailbox.t list, Jmap.Protocol.Error.error) result
156156+end
157157+158158+(** {1 Common Patterns} *)
159159+160160+(** Execute Email/query and automatically chain Email/get *)
161161+val query_and_fetch :
162162+ env:Eio_unix.Stdenv.base ->
163163+ ctx:Jmap_unix.context ->
164164+ session:Jmap.Protocol.Session.Session.t ->
165165+ ?account_id:string ->
166166+ ?filter:Jmap_email_query.Filter.t ->
167167+ ?sort:Jmap_email_query.Sort.t list ->
168168+ ?limit:int ->
169169+ ?properties:Jmap_email_query.property list ->
170170+ unit ->
171171+ (Jmap_email.t list, Jmap.Protocol.Error.error) result
172172+173173+(** Get emails by IDs *)
174174+val get_emails_by_ids :
175175+ env:Eio_unix.Stdenv.base ->
176176+ ctx:Jmap_unix.context ->
177177+ session:Jmap.Protocol.Session.Session.t ->
178178+ ?account_id:string ->
179179+ ?properties:Jmap_email_query.property list ->
180180+ Jmap.Id.t list ->
181181+ (Jmap_email.t list, Jmap.Protocol.Error.error) result
182182+183183+(** Get all mailboxes *)
184184+val get_mailboxes :
185185+ env:Eio_unix.Stdenv.base ->
186186+ ctx:Jmap_unix.context ->
187187+ session:Jmap.Protocol.Session.Session.t ->
188188+ ?account_id:string ->
189189+ unit ->
190190+ (Jmap_mailbox.t list, Jmap.Protocol.Error.error) result
191191+192192+(** Find mailbox by role (e.g., "inbox", "sent", "drafts") *)
193193+val find_mailbox_by_role :
194194+ env:Eio_unix.Stdenv.base ->
195195+ ctx:Jmap_unix.context ->
196196+ session:Jmap.Protocol.Session.Session.t ->
197197+ ?account_id:string ->
198198+ string ->
199199+ (Jmap_mailbox.t option, Jmap.Protocol.Error.error) result
+378
jmap/jmap-email/jmap_email_query.ml
···11+(** High-level Email query implementation *)
22+33+type property = [
44+ | `Id | `BlobId | `ThreadId | `MailboxIds | `Keywords | `Size
55+ | `ReceivedAt | `MessageId | `InReplyTo | `References | `Sender
66+ | `From | `To | `Cc | `Bcc | `ReplyTo | `Subject | `SentAt
77+ | `HasAttachment | `Preview | `BodyStructure | `BodyValues
88+ | `TextBody | `HtmlBody | `Attachments
99+]
1010+1111+let property_to_string = function
1212+ | `Id -> "id"
1313+ | `BlobId -> "blobId"
1414+ | `ThreadId -> "threadId"
1515+ | `MailboxIds -> "mailboxIds"
1616+ | `Keywords -> "keywords"
1717+ | `Size -> "size"
1818+ | `ReceivedAt -> "receivedAt"
1919+ | `MessageId -> "messageId"
2020+ | `InReplyTo -> "inReplyTo"
2121+ | `References -> "references"
2222+ | `Sender -> "sender"
2323+ | `From -> "from"
2424+ | `To -> "to"
2525+ | `Cc -> "cc"
2626+ | `Bcc -> "bcc"
2727+ | `ReplyTo -> "replyTo"
2828+ | `Subject -> "subject"
2929+ | `SentAt -> "sentAt"
3030+ | `HasAttachment -> "hasAttachment"
3131+ | `Preview -> "preview"
3232+ | `BodyStructure -> "bodyStructure"
3333+ | `BodyValues -> "bodyValues"
3434+ | `TextBody -> "textBody"
3535+ | `HtmlBody -> "htmlBody"
3636+ | `Attachments -> "attachments"
3737+3838+module PropertySets = struct
3939+ let list_view = [`Id; `Subject; `From; `ReceivedAt; `Preview; `Keywords]
4040+ let preview = [`Id; `Subject; `From; `To; `ReceivedAt; `Preview; `HasAttachment]
4141+ let full = [`Id; `BlobId; `ThreadId; `MailboxIds; `Keywords; `Size;
4242+ `ReceivedAt; `From; `To; `Cc; `Bcc; `Subject; `Preview;
4343+ `TextBody; `HtmlBody; `HasAttachment; `Attachments]
4444+ let threading = [`Id; `ThreadId; `Subject; `From; `ReceivedAt]
4545+end
4646+4747+module Sort = struct
4848+ type t = Jmap.Methods.Comparator.t
4949+5050+ let by_date_desc = Jmap.Methods.Comparator.v ~property:"receivedAt" ~is_ascending:false ()
5151+ let by_date_asc = Jmap.Methods.Comparator.v ~property:"receivedAt" ~is_ascending:true ()
5252+ let by_size_desc = Jmap.Methods.Comparator.v ~property:"size" ~is_ascending:false ()
5353+ let by_from = Jmap.Methods.Comparator.v ~property:"from" ~is_ascending:true ()
5454+ let by_subject = Jmap.Methods.Comparator.v ~property:"subject" ~is_ascending:true ()
5555+5656+ let custom ~property ~is_ascending =
5757+ Jmap.Methods.Comparator.v ~property ~is_ascending ()
5858+5959+ let combine sorts = sorts
6060+end
6161+6262+module Filter = struct
6363+ type operator =
6464+ | And of t * t
6565+ | Or of t * t
6666+ | Not of t
6767+ | Condition of string * Yojson.Safe.t
6868+ and t = operator
6969+7070+ let in_mailbox mailbox_id =
7171+ Condition ("inMailbox", `String mailbox_id)
7272+7373+ let in_mailbox_role role =
7474+ Condition ("inMailboxOtherThan", `List [`String role])
7575+7676+ let unread =
7777+ Condition ("hasKeyword", `String "$seen")
7878+ |> fun c -> Not c
7979+8080+ let flagged =
8181+ Condition ("hasKeyword", `String "$flagged")
8282+8383+ let has_attachment =
8484+ Condition ("hasAttachment", `Bool true)
8585+8686+ let from email =
8787+ Condition ("from", `String email)
8888+8989+ let to_ email =
9090+ Condition ("to", `String email)
9191+9292+ let subject_contains text =
9393+ Condition ("subject", `String text)
9494+9595+ let body_contains text =
9696+ Condition ("text", `String text)
9797+9898+ let after date =
9999+ Condition ("after", `String (Jmap.Date.to_rfc3339 date))
100100+101101+ let before date =
102102+ Condition ("before", `String (Jmap.Date.to_rfc3339 date))
103103+104104+ let between start end_ =
105105+ And (after start, before end_)
106106+107107+ let min_size bytes =
108108+ Condition ("minSize", `Int bytes)
109109+110110+ let max_size bytes =
111111+ Condition ("maxSize", `Int bytes)
112112+113113+ let and_ a b = And (a, b)
114114+ let or_ a b = Or (a, b)
115115+ let not_ a = Not a
116116+117117+ let rec to_json = function
118118+ | And (a, b) ->
119119+ `Assoc [("operator", `String "AND");
120120+ ("conditions", `List [to_json a; to_json b])]
121121+ | Or (a, b) ->
122122+ `Assoc [("operator", `String "OR");
123123+ ("conditions", `List [to_json a; to_json b])]
124124+ | Not a ->
125125+ `Assoc [("operator", `String "NOT");
126126+ ("condition", to_json a)]
127127+ | Condition (field, value) ->
128128+ `Assoc [(field, value)]
129129+end
130130+131131+type query_builder = {
132132+ account_id : string option;
133133+ filter : Filter.t option;
134134+ sort : Sort.t list;
135135+ limit_count : int option;
136136+ position : int option;
137137+ properties : property list;
138138+ collapse_threads : bool;
139139+}
140140+141141+let query () = {
142142+ account_id = None;
143143+ filter = None;
144144+ sort = [Sort.by_date_desc];
145145+ limit_count = None;
146146+ position = None;
147147+ properties = PropertySets.list_view;
148148+ collapse_threads = false;
149149+}
150150+151151+let with_account account_id builder =
152152+ { builder with account_id = Some account_id }
153153+154154+let where filter builder =
155155+ { builder with filter = Some filter }
156156+157157+let order_by sort builder =
158158+ { builder with sort = [sort] }
159159+160160+let limit n builder =
161161+ { builder with limit_count = Some n }
162162+163163+let offset n builder =
164164+ { builder with position = Some n }
165165+166166+let select properties builder =
167167+ { builder with properties }
168168+169169+let select_preset preset builder =
170170+ let properties = match preset with
171171+ | `ListV -> PropertySets.list_view
172172+ | `Preview -> PropertySets.preview
173173+ | `Full -> PropertySets.full
174174+ | `Threading -> PropertySets.threading
175175+ in
176176+ { builder with properties }
177177+178178+let collapse_threads value builder =
179179+ { builder with collapse_threads = value }
180180+181181+type query_result = {
182182+ ids : Jmap.Id.t list;
183183+ total : int option;
184184+ position : int;
185185+ can_calculate_changes : bool;
186186+}
187187+188188+type fetch_result = {
189189+ emails : Jmap_email.t list;
190190+ total : int option;
191191+}
192192+193193+(* Helper to get account_id from session if not specified *)
194194+let resolve_account_id builder session =
195195+ match builder.account_id with
196196+ | Some id -> id
197197+ | None -> Jmap_unix.Session_utils.get_primary_mail_account session
198198+199199+let execute_query ~env ~ctx ~session builder =
200200+ let open Jmap.Protocol.Error in
201201+ try
202202+ let account_id = resolve_account_id builder session in
203203+204204+ (* Build the request *)
205205+ let req_builder = Jmap_unix.build ctx in
206206+ let req_builder = Jmap_unix.using req_builder [`Core; `Mail] in
207207+208208+ (* Create query arguments *)
209209+ let query_args =
210210+ let base = Jmap.Methods.Query_args.v ~account_id ~sort:builder.sort () in
211211+ let base = match builder.filter with
212212+ | Some f -> base (* TODO: Add filter support to Query_args *)
213213+ | None -> base
214214+ in
215215+ let base = match builder.limit_count with
216216+ | Some n -> Jmap.Methods.Query_args.v ~account_id ~sort:builder.sort ~limit:n ()
217217+ | None -> base
218218+ in
219219+ base
220220+ in
221221+222222+ let query_json = Jmap.Methods.Query_args.to_json query_args in
223223+ let req_builder = Jmap_unix.add_method_call req_builder "Email/query" query_json "q1" in
224224+225225+ (* Execute and parse response *)
226226+ match Jmap_unix.execute env req_builder with
227227+ | Ok response ->
228228+ (match Jmap_unix.Response.extract_method ~method_name:"Email/query" ~method_call_id:"q1" response with
229229+ | Ok json ->
230230+ (match Jmap.Methods.Query_response.of_json json with
231231+ | Ok qr ->
232232+ Ok {
233233+ ids = Jmap.Methods.Query_response.ids qr;
234234+ total = Jmap.Methods.Query_response.total qr;
235235+ position = Jmap.Methods.Query_response.position qr;
236236+ can_calculate_changes = Jmap.Methods.Query_response.can_calculate_changes qr;
237237+ }
238238+ | Error e -> Error (Protocol e))
239239+ | Error e -> Error e)
240240+ | Error e -> Error e
241241+ with exn ->
242242+ Error (Protocol (Printf.sprintf "Query execution failed: %s" (Printexc.to_string exn)))
243243+244244+let execute_with_fetch ~env ~ctx ~session builder =
245245+ let open Jmap.Protocol.Error in
246246+ try
247247+ let account_id = resolve_account_id builder session in
248248+249249+ (* Build the chained request *)
250250+ let req_builder = Jmap_unix.build ctx in
251251+ let req_builder = Jmap_unix.using req_builder [`Core; `Mail] in
252252+253253+ (* Add Email/query *)
254254+ let query_args =
255255+ let base = Jmap.Methods.Query_args.v ~account_id ~sort:builder.sort () in
256256+ let base = match builder.limit_count with
257257+ | Some n -> Jmap.Methods.Query_args.v ~account_id ~sort:builder.sort ~limit:n ()
258258+ | None -> base
259259+ in
260260+ base
261261+ in
262262+263263+ let query_json = Jmap.Methods.Query_args.to_json query_args in
264264+ let req_builder = Jmap_unix.add_method_call req_builder "Email/query" query_json "q1" in
265265+266266+ (* Add Email/get with result reference *)
267267+ let properties = List.map property_to_string builder.properties in
268268+ let get_args = Jmap.Methods.Get_args.v ~account_id ~properties () in
269269+ let (get_args_with_ref, result_ref_json) = Jmap.Methods.Get_args.with_result_reference
270270+ get_args
271271+ ~result_of:"q1"
272272+ ~name:"Email/query"
273273+ ~path:"/ids"
274274+ in
275275+ let get_json = Jmap.Methods.Get_args.to_json ~result_reference_ids:(Some result_ref_json) get_args_with_ref in
276276+ let req_builder = Jmap_unix.add_method_call req_builder "Email/get" get_json "g1" in
277277+278278+ (* Execute and parse response *)
279279+ match Jmap_unix.execute env req_builder with
280280+ | Ok response ->
281281+ (* Extract query response for total count *)
282282+ let total =
283283+ match Jmap_unix.Response.extract_method ~method_name:"Email/query" ~method_call_id:"q1" response with
284284+ | Ok json ->
285285+ (match Jmap.Methods.Query_response.of_json json with
286286+ | Ok qr -> Jmap.Methods.Query_response.total qr
287287+ | Error _ -> None)
288288+ | Error _ -> None
289289+ in
290290+291291+ (* Extract email data *)
292292+ (match Jmap_unix.Response.extract_method ~method_name:"Email/get" ~method_call_id:"g1" response with
293293+ | Ok json ->
294294+ let email_from_json j =
295295+ match Jmap_email.of_json j with
296296+ | Ok e -> e
297297+ | Error err -> failwith err
298298+ in
299299+ (match Jmap.Methods.Get_response.of_json ~from_json:email_from_json json with
300300+ | Ok gr ->
301301+ Ok {
302302+ emails = Jmap.Methods.Get_response.list gr;
303303+ total = total;
304304+ }
305305+ | Error e -> Error (Protocol e))
306306+ | Error e -> Error e)
307307+ | Error e -> Error e
308308+ with exn ->
309309+ Error (Protocol (Printf.sprintf "Fetch execution failed: %s" (Printexc.to_string exn)))
310310+311311+(* Common query builders *)
312312+let inbox ?limit () =
313313+ let q = query () |> where (Filter.in_mailbox_role "inbox") in
314314+ match limit with
315315+ | Some n -> limit n q
316316+ | None -> q
317317+318318+let unread ?limit () =
319319+ let q = query () |> where Filter.unread in
320320+ match limit with
321321+ | Some n -> limit n q
322322+ | None -> q
323323+324324+let recent ?limit () =
325325+ let yesterday = Jmap.Date.of_timestamp (Unix.time () -. 86400.) in
326326+ let q = query () |> where (Filter.after yesterday) in
327327+ match limit with
328328+ | Some n -> limit n q
329329+ | None -> q
330330+331331+let from_sender sender ?limit () =
332332+ let q = query () |> where (Filter.from sender) in
333333+ match limit with
334334+ | Some n -> limit n q
335335+ | None -> q
336336+337337+let search text ?limit () =
338338+ let q = query () |> where (Filter.or_
339339+ (Filter.subject_contains text)
340340+ (Filter.body_contains text)) in
341341+ match limit with
342342+ | Some n -> limit n q
343343+ | None -> q
344344+345345+let flagged ?limit () =
346346+ let q = query () |> where Filter.flagged in
347347+ match limit with
348348+ | Some n -> limit n q
349349+ | None -> q
350350+351351+let with_attachments ?limit () =
352352+ let q = query () |> where Filter.has_attachment in
353353+ match limit with
354354+ | Some n -> limit n q
355355+ | None -> q
356356+357357+(* Pretty printing *)
358358+let pp_email ppf email =
359359+ let open Format in
360360+ fprintf ppf "@[<v 2>Email:@,";
361361+ (match Jmap_email.subject email with
362362+ | Some s -> fprintf ppf "Subject: %s@," s
363363+ | None -> ());
364364+ (match Jmap_email.from email with
365365+ | Some addrs when addrs <> [] ->
366366+ let addr = List.hd addrs in
367367+ fprintf ppf "From: %s@," (Jmap_email.Email_address.email addr)
368368+ | _ -> ());
369369+ (match Jmap_email.received_at email with
370370+ | Some ts ->
371371+ let date = Jmap.Date.of_timestamp ts in
372372+ fprintf ppf "Date: %s@," (Jmap.Date.to_rfc3339 date)
373373+ | None -> ());
374374+ fprintf ppf "@]"
375375+376376+let pp_email_list ppf emails =
377377+ Format.fprintf ppf "@[<v>%a@]"
378378+ (Format.pp_print_list ~pp_sep:Format.pp_print_cut pp_email) emails
+226
jmap/jmap-email/jmap_email_query.mli
···11+(** High-level Email query interface for JMAP Email.
22+33+ This module provides a fluent, type-safe API for constructing
44+ and executing Email queries with automatic result chaining.
55+66+ @see <https://www.rfc-editor.org/rfc/rfc8621.html#section-4.4> RFC 8621 Section 4.4 *)
77+88+(** {1 Email Properties} *)
99+1010+(** Type-safe email property selectors *)
1111+type property = [
1212+ | `Id
1313+ | `BlobId
1414+ | `ThreadId
1515+ | `MailboxIds
1616+ | `Keywords
1717+ | `Size
1818+ | `ReceivedAt
1919+ | `MessageId
2020+ | `InReplyTo
2121+ | `References
2222+ | `Sender
2323+ | `From
2424+ | `To
2525+ | `Cc
2626+ | `Bcc
2727+ | `ReplyTo
2828+ | `Subject
2929+ | `SentAt
3030+ | `HasAttachment
3131+ | `Preview
3232+ | `BodyStructure
3333+ | `BodyValues
3434+ | `TextBody
3535+ | `HtmlBody
3636+ | `Attachments
3737+]
3838+3939+(** Convert property to its string representation *)
4040+val property_to_string : property -> string
4141+4242+(** Standard property sets for common use cases *)
4343+module PropertySets : sig
4444+ (** Minimal properties for list views *)
4545+ val list_view : property list
4646+4747+ (** Properties for email preview *)
4848+ val preview : property list
4949+5050+ (** Properties for full email display *)
5151+ val full : property list
5252+5353+ (** Properties for threading *)
5454+ val threading : property list
5555+end
5656+5757+(** {1 Sort Options} *)
5858+5959+(** Common sort configurations *)
6060+module Sort : sig
6161+ type t
6262+6363+ (** Sort by received date, newest first *)
6464+ val by_date_desc : t
6565+6666+ (** Sort by received date, oldest first *)
6767+ val by_date_asc : t
6868+6969+ (** Sort by size, largest first *)
7070+ val by_size_desc : t
7171+7272+ (** Sort by sender name *)
7373+ val by_from : t
7474+7575+ (** Sort by subject *)
7676+ val by_subject : t
7777+7878+ (** Create custom sort *)
7979+ val custom : property:string -> is_ascending:bool -> t
8080+8181+ (** Combine multiple sort criteria *)
8282+ val combine : t list -> t list
8383+end
8484+8585+(** {1 Query Filters} *)
8686+8787+(** Email filter conditions *)
8888+module Filter : sig
8989+ type t
9090+9191+ (** Filter by mailbox *)
9292+ val in_mailbox : string -> t
9393+9494+ (** Filter by mailbox role (e.g., "inbox", "sent", "drafts") *)
9595+ val in_mailbox_role : string -> t
9696+9797+ (** Filter for unread emails *)
9898+ val unread : t
9999+100100+ (** Filter for flagged/starred emails *)
101101+ val flagged : t
102102+103103+ (** Filter for emails with attachments *)
104104+ val has_attachment : t
105105+106106+ (** Filter by sender *)
107107+ val from : string -> t
108108+109109+ (** Filter by recipient (to, cc, or bcc) *)
110110+ val to_ : string -> t
111111+112112+ (** Filter by subject containing text *)
113113+ val subject_contains : string -> t
114114+115115+ (** Filter by body text *)
116116+ val body_contains : string -> t
117117+118118+ (** Filter by date range *)
119119+ val after : Jmap.Date.t -> t
120120+ val before : Jmap.Date.t -> t
121121+ val between : Jmap.Date.t -> Jmap.Date.t -> t
122122+123123+ (** Filter by size *)
124124+ val min_size : int -> t
125125+ val max_size : int -> t
126126+127127+ (** Combine filters *)
128128+ val and_ : t -> t -> t
129129+ val or_ : t -> t -> t
130130+ val not_ : t -> t
131131+end
132132+133133+(** {1 Query Builder} *)
134134+135135+(** Fluent query builder *)
136136+type query_builder
137137+138138+(** Create a new query builder *)
139139+val query : unit -> query_builder
140140+141141+(** Set the account ID (uses primary mail account if not set) *)
142142+val with_account : string -> query_builder -> query_builder
143143+144144+(** Add a filter condition *)
145145+val where : Filter.t -> query_builder -> query_builder
146146+147147+(** Set sort order *)
148148+val order_by : Sort.t -> query_builder -> query_builder
149149+150150+(** Limit the number of results *)
151151+val limit : int -> query_builder -> query_builder
152152+153153+(** Set position/offset for pagination *)
154154+val offset : int -> query_builder -> query_builder
155155+156156+(** Select which properties to fetch *)
157157+val select : property list -> query_builder -> query_builder
158158+159159+(** Use a predefined property set *)
160160+val select_preset : [`ListV | `Preview | `Full | `Threading] -> query_builder -> query_builder
161161+162162+(** Enable thread collapsing *)
163163+val collapse_threads : bool -> query_builder -> query_builder
164164+165165+(** {1 Execution} *)
166166+167167+(** Query result containing email IDs *)
168168+type query_result = {
169169+ ids : Jmap.Id.t list;
170170+ total : int option;
171171+ position : int;
172172+ can_calculate_changes : bool;
173173+}
174174+175175+(** Execute just the query (returns IDs only) *)
176176+val execute_query :
177177+ env:Eio_unix.Stdenv.base ->
178178+ ctx:Jmap_unix.context ->
179179+ session:Jmap.Protocol.Session.Session.t ->
180180+ query_builder ->
181181+ (query_result, Jmap.Protocol.Error.error) result
182182+183183+(** Query result with full email data *)
184184+type fetch_result = {
185185+ emails : Jmap_email.t list;
186186+ total : int option;
187187+}
188188+189189+(** Execute query and automatically fetch email data *)
190190+val execute_with_fetch :
191191+ env:Eio_unix.Stdenv.base ->
192192+ ctx:Jmap_unix.context ->
193193+ session:Jmap.Protocol.Session.Session.t ->
194194+ query_builder ->
195195+ (fetch_result, Jmap.Protocol.Error.error) result
196196+197197+(** {1 Common Queries} *)
198198+199199+(** Inbox emails, newest first *)
200200+val inbox : ?limit:int -> unit -> query_builder
201201+202202+(** Unread emails across all mailboxes *)
203203+val unread : ?limit:int -> unit -> query_builder
204204+205205+(** Recent emails (last 24 hours) *)
206206+val recent : ?limit:int -> unit -> query_builder
207207+208208+(** Emails from a specific sender *)
209209+val from_sender : string -> ?limit:int -> unit -> query_builder
210210+211211+(** Search emails by text across subject and body *)
212212+val search : string -> ?limit:int -> unit -> query_builder
213213+214214+(** Flagged/starred emails *)
215215+val flagged : ?limit:int -> unit -> query_builder
216216+217217+(** Emails with attachments *)
218218+val with_attachments : ?limit:int -> unit -> query_builder
219219+220220+(** {1 Pretty Printing} *)
221221+222222+(** Pretty-print an email for display *)
223223+val pp_email : Format.formatter -> Jmap_email.t -> unit
224224+225225+(** Pretty-print a list of emails as a summary *)
226226+val pp_email_list : Format.formatter -> Jmap_email.t list -> unit
+2
jmap/jmap-unix/jmap_unix.ml
···279279 ("methodCalls", `List method_calls_json);
280280 ] in
281281 let request_body = Yojson.Safe.to_string request_json in
282282+ let pretty_request = Yojson.Safe.pretty_to_string request_json in
283283+ Format.printf "DEBUG: Sending JMAP request:\n%s\n%!" pretty_request;
282284283285 let headers = [] in
284286 (match http_request env builder.ctx ~meth:`POST ~uri:api_uri ~headers ~body:(Some request_body) with
···8888 @return True if date1 is after date2. *)
8989val is_after : t -> t -> bool
90909191+(** Pretty-print a Date in RFC3339 format.
9292+ @param ppf The formatter.
9393+ @param date The Date to print. *)
9494+val pp : Format.formatter -> t -> unit
9595+9696+(** Pretty-print a Date for debugging.
9797+ @param ppf The formatter.
9898+ @param date The Date to format. *)
9999+val pp_debug : Format.formatter -> t -> unit
100100+91101(** Convert a Date to a human-readable string for debugging.
92102 @param date The Date to format.
93103 @return A debug string representation. *)
+2
jmap/jmap/jmap_error.ml
···339339 | Auth msg -> Printf.sprintf "Auth error: %s" msg
340340 | ServerError msg -> Printf.sprintf "Server error: %s" msg
341341342342+let pp ppf error = Fmt.string ppf (error_to_string error)
343343+342344let map_error res f =
343345 match res with
344346 | Ok _ as ok -> ok
+5
jmap/jmap/jmap_error.mli
···321321(** Get a human-readable description of an error *)
322322val error_to_string : error -> string
323323324324+(** Pretty-print an error.
325325+ @param ppf The formatter.
326326+ @param error The error to print. *)
327327+val pp : Format.formatter -> error -> unit
328328+324329(** {2 Result Handling} *)
325330326331(** Map an error with additional context *)
+4
jmap/jmap/jmap_id.ml
···28282929let to_string id = id
30303131+let pp ppf id = Fmt.string ppf id
3232+3133let validate id =
3234 if is_valid_string id then Ok ()
3335 else Error "Invalid Id format"
···3537let equal = String.equal
36383739let compare = String.compare
4040+4141+let pp_debug ppf id = Fmt.pf ppf "Id(%s)" id
38423943let to_string_debug id = Printf.sprintf "Id(%s)" id
4044
+10
jmap/jmap/jmap_id.mli
···3838 @return The string representation. *)
3939val to_string : t -> string
40404141+(** Pretty-print an Id.
4242+ @param ppf The formatter.
4343+ @param id The Id to print. *)
4444+val pp : Format.formatter -> t -> unit
4545+4146(** {1 Validation} *)
42474348(** Check if a string is a valid JMAP Id.
···6368 @param id2 Second Id.
6469 @return Negative if id1 < id2, zero if equal, positive if id1 > id2. *)
6570val compare : t -> t -> int
7171+7272+(** Pretty-print an Id for debugging.
7373+ @param ppf The formatter.
7474+ @param id The Id to format. *)
7575+val pp_debug : Format.formatter -> t -> unit
66766777(** Convert an Id to a human-readable string for debugging.
6878 @param id The Id to format.
+2
jmap/jmap/jmap_protocol.ml
···3030 | `Submission -> "urn:ietf:params:jmap:submission"
3131 | `VacationResponse -> "urn:ietf:params:jmap:vacationresponse"
32323333+ let pp ppf capability = Fmt.string ppf (to_string capability)
3434+3335 let of_string = function
3436 | "urn:ietf:params:jmap:core" -> Some `Core
3537 | "urn:ietf:params:jmap:mail" -> Some `Mail
+5
jmap/jmap/jmap_protocol.mli
···9393 @return The corresponding URN string *)
9494 val to_string : t -> string
95959696+ (** Pretty-print a capability.
9797+ @param ppf The formatter.
9898+ @param capability The capability to print. *)
9999+ val pp : Format.formatter -> t -> unit
100100+96101 (** Parse URN string to capability variant.
97102 @param urn The URN string to parse
98103 @return Some capability if recognized, None otherwise *)
+4
jmap/jmap/jmap_uint.ml
···24242525let to_string uint = string_of_int uint
26262727+let pp ppf uint = Fmt.int ppf uint
2828+2729(* Constants *)
2830let zero = 0
2931let one = 1
···6062let min uint1 uint2 = if uint1 <= uint2 then uint1 else uint2
61636264let max uint1 uint2 = if uint1 >= uint2 then uint1 else uint2
6565+6666+let pp_debug ppf uint = Fmt.pf ppf "UInt(%d)" uint
63676468let to_string_debug uint = Printf.sprintf "UInt(%d)" uint
6569
+10
jmap/jmap/jmap_uint.mli
···4949 @return The string representation. *)
5050val to_string : t -> string
51515252+(** Pretty-print an UnsignedInt.
5353+ @param ppf The formatter.
5454+ @param uint The UnsignedInt to print. *)
5555+val pp : Format.formatter -> t -> unit
5656+5257(** {1 Constants} *)
53585459(** Zero value. *)
···117122 @param uint2 Second UnsignedInt.
118123 @return The larger value. *)
119124val max : t -> t -> t
125125+126126+(** Pretty-print an UnsignedInt for debugging.
127127+ @param ppf The formatter.
128128+ @param uint The UnsignedInt to format. *)
129129+val pp_debug : Format.formatter -> t -> unit
120130121131(** Convert an UnsignedInt to a human-readable string for debugging.
122132 @param uint The UnsignedInt to format.