My agentic slop goes here. Not intended for anyone else!
1(** High-level Email query implementation *) 2 3type property = Property.t 4 5 6 7module Sort = struct 8 type t = Jmap.Methods.Comparator.t 9 10 let by_date_desc = Jmap.Methods.Comparator.v ~property:"receivedAt" ~is_ascending:false () 11 let by_date_asc = Jmap.Methods.Comparator.v ~property:"receivedAt" ~is_ascending:true () 12 let by_size_desc = Jmap.Methods.Comparator.v ~property:"size" ~is_ascending:false () 13 let by_from = Jmap.Methods.Comparator.v ~property:"from" ~is_ascending:true () 14 let by_subject = Jmap.Methods.Comparator.v ~property:"subject" ~is_ascending:true () 15 16 let custom ~property ~is_ascending = 17 Jmap.Methods.Comparator.v ~property ~is_ascending () 18 19 let combine sorts = sorts 20end 21 22module Filter = struct 23 type t = Jmap.Methods.Filter.t 24 25 open Jmap.Methods.Filter 26 27 (* Email-specific filter constructors using core utilities *) 28 let in_mailbox (mailbox_id : Jmap.Id.t) = 29 condition (`Assoc [("inMailbox", `String (Jmap.Id.to_string mailbox_id))]) 30 31 let in_mailbox_role role = 32 condition (`Assoc [("inMailboxOtherThan", `List [`String role])]) 33 34 let unread = 35 not_ (condition (`Assoc [("hasKeyword", `String "$seen")])) 36 37 let flagged = 38 condition (`Assoc [("hasKeyword", `String "$flagged")]) 39 40 let has_attachment = 41 property_equals "hasAttachment" (`Bool true) 42 43 let from email = 44 property_equals "from" (`String email) 45 46 let to_ email = 47 property_equals "to" (`String email) 48 49 let subject_contains text = 50 text_contains "subject" text 51 52 let body_contains text = 53 text_contains "text" text 54 55 let after date = 56 property_gt "after" (`String (Jmap.Date.to_rfc3339 date)) 57 58 let before date = 59 property_lt "before" (`String (Jmap.Date.to_rfc3339 date)) 60 61 let between start end_ = 62 and_ [after start; before end_] 63 64 let min_size bytes = 65 property_ge "minSize" (`Int bytes) 66 67 let max_size bytes = 68 property_le "maxSize" (`Int bytes) 69 70 (* Re-export core filter functions for convenience *) 71 let and_ = and_ 72 let or_ = or_ 73 let not_ = not_ 74 let to_json = to_json 75end 76 77type query_builder = { 78 account_id : string option; 79 filter : Filter.t option; 80 sort : Sort.t list; 81 limit_count : Jmap.UInt.t option; 82 position : int option; 83 properties : property list; 84 collapse_threads : bool; 85 calculate_total : bool; 86} 87 88let query () = { 89 account_id = None; 90 filter = None; 91 sort = [Sort.by_date_desc]; 92 limit_count = None; 93 position = None; 94 properties = Property.common_list_properties; 95 collapse_threads = false; 96 calculate_total = false; 97} 98 99let with_account account_id builder = 100 { builder with account_id = Some (Jmap.Id.to_string account_id) } 101 102let where filter builder = 103 { builder with filter = Some filter } 104 105let order_by sort builder = 106 { builder with sort = [sort] } 107 108let limit n builder = 109 match Jmap.UInt.of_int n with 110 | Ok uint -> { builder with limit_count = Some uint } 111 | Error _ -> failwith ("Invalid limit value: " ^ string_of_int n) 112 113let offset n builder = 114 { builder with position = Some n } 115 116let select properties builder = 117 { builder with properties } 118 119let select_preset preset builder = 120 let properties = match preset with 121 | `ListV -> Property.common_list_properties 122 | `Preview -> Property.for_preview () 123 | `Full -> Property.for_reading () 124 | `Threading -> Property.minimal_for_query () 125 in 126 { builder with properties } 127 128let collapse_threads value builder = 129 { builder with collapse_threads = value } 130 131let calculate_total value builder = 132 { builder with calculate_total = value } 133 134type query_result = { 135 ids : Jmap.Id.t list; 136 total : int option; 137 position : int; 138 can_calculate_changes : bool; 139} 140 141type 'email fetch_result = { 142 emails : 'email list; 143 total : int option; 144} 145 146(* JSON generation functions for jmap-unix layer *) 147 148let to_core_query_args builder = 149 let account_id = match builder.account_id with 150 | Some id -> id 151 | None -> failwith "Account ID must be set before building query" 152 in 153 Jmap.Methods.Query_args.v 154 ~account_id 155 ?filter:builder.filter 156 ~sort:builder.sort 157 ?position:builder.position 158 ?limit:(Option.map Jmap.UInt.to_int builder.limit_count) 159 ?calculate_total:(Some builder.calculate_total) 160 ?collapse_threads:(Some builder.collapse_threads) 161 () 162 163let build_email_query builder = 164 let args = to_core_query_args builder in 165 Jmap.Methods.Query_args.to_json args 166 167let property_preset_to_strings = function 168 | `ListV -> Property.to_string_list Property.common_list_properties 169 | `Preview -> Property.to_string_list (Property.for_preview ()) 170 | `Full -> Property.to_string_list (Property.for_reading ()) 171 | `Threading -> Property.to_string_list (Property.minimal_for_query ()) 172 173let build_email_get_with_ref ~account_id ~properties ~result_of = 174 let property_strings = Property.to_string_list properties in 175 `Assoc [ 176 ("accountId", `String (Jmap.Id.to_string account_id)); 177 ("properties", `List (List.map (fun s -> `String s) property_strings)); 178 ("#ids", `Assoc [ 179 ("resultOf", `String result_of); 180 ("name", `String "Email/query"); 181 ("path", `String "/ids") 182 ]) 183 ] 184 185let properties_to_strings properties = 186 Property.to_string_list properties 187 188 189(* Common query builders *) 190let inbox ?limit:lim () = 191 let q = query () |> where (Filter.in_mailbox_role "inbox") in 192 match lim with 193 | Some n -> limit n q 194 | None -> q 195 196let unread ?limit:lim () = 197 let q = query () |> where Filter.unread in 198 match lim with 199 | Some n -> limit n q 200 | None -> q 201 202let recent ?limit:lim () = 203 let yesterday = Jmap.Date.of_timestamp (Unix.time () -. 86400.) in 204 let q = query () |> where (Filter.after yesterday) in 205 match lim with 206 | Some n -> limit n q 207 | None -> q 208 209let from_sender sender ?limit:lim () = 210 let q = query () |> where (Filter.from sender) in 211 match lim with 212 | Some n -> limit n q 213 | None -> q 214 215let search text ?limit:lim () = 216 let q = query () |> where (Filter.or_ 217 [Filter.subject_contains text; 218 Filter.body_contains text]) in 219 match lim with 220 | Some n -> limit n q 221 | None -> q 222 223let flagged ?limit:lim () = 224 let q = query () |> where Filter.flagged in 225 match lim with 226 | Some n -> limit n q 227 | None -> q 228 229let with_attachments ?limit:lim () = 230 let q = query () |> where Filter.has_attachment in 231 match lim with 232 | Some n -> limit n q 233 | None -> q 234 235(* Pretty printing - generic functions that need specific email extractors *) 236let pp_email ppf _email = 237 (* This function is generic and needs to be specialized at call site *) 238 Format.fprintf ppf "Email (generic printer)" 239 240let pp_email_list ppf emails = 241 Format.fprintf ppf "@[<v>%a@]" 242 (Format.pp_print_list ~pp_sep:Format.pp_print_cut pp_email) emails