(** Email changes operations using core JMAP Changes_args *) open Jmap.Methods (** Build Email/changes arguments *) let build_changes_args ~account_id ~since_state ?max_changes () = let account_id_str = Jmap.Id.to_string account_id in let max_changes_int = match max_changes with | Some uint -> Some (Jmap.UInt.to_int uint) | None -> None in Changes_args.v ~account_id:account_id_str ~since_state ?max_changes:max_changes_int () (** Convert Email/changes arguments to JSON *) let changes_args_to_json args = Changes_args.to_json args (** Track changes since a given state *) type change_tracker = { account_id : Jmap.Id.t; current_state : string; created : Jmap.Id.t list; updated : Jmap.Id.t list; destroyed : Jmap.Id.t list; } (** Create a new change tracker *) let create_tracker ~account_id ~initial_state = { account_id; current_state = initial_state; created = []; updated = []; destroyed = []; } (** Update tracker with a Changes_response *) let update_tracker tracker response = { tracker with current_state = Changes_response.new_state response; created = tracker.created @ (List.map (fun s -> match Jmap.Id.of_string s with Ok id -> id | Error e -> failwith e) (Changes_response.created response)); updated = tracker.updated @ (List.map (fun s -> match Jmap.Id.of_string s with Ok id -> id | Error e -> failwith e) (Changes_response.updated response)); destroyed = tracker.destroyed @ (List.map (fun s -> match Jmap.Id.of_string s with Ok id -> id | Error e -> failwith e) (Changes_response.destroyed response)); } (** Get all changes since tracker was created *) let get_all_changes tracker = (tracker.created, tracker.updated, tracker.destroyed) (** Get next batch of changes *) let get_next_changes ~account_id ~since_state ?(max_changes=500) () = let max_changes_uint = match Jmap.UInt.of_int max_changes with | Ok u -> u | Error _ -> failwith ("Invalid max_changes: " ^ string_of_int max_changes) in build_changes_args ~account_id ~since_state ~max_changes:max_changes_uint () (** Check if there are pending changes *) let has_pending_changes response = Changes_response.has_more_changes response (** Incremental sync helper *) module Sync = struct type sync_state = { account_id : Jmap.Id.t; last_state : string; pending_created : Jmap.Id.t list; pending_updated : Jmap.Id.t list; pending_destroyed : Jmap.Id.t list; } let init ~account_id ~initial_state = { account_id; last_state = initial_state; pending_created = []; pending_updated = []; pending_destroyed = []; } let add_response sync response = let new_state = Changes_response.new_state response in let created = Changes_response.created response in let updated = Changes_response.updated response in let destroyed = Changes_response.destroyed response in { sync with last_state = new_state; pending_created = sync.pending_created @ (List.map (fun s -> match Jmap.Id.of_string s with Ok id -> id | Error e -> failwith e) created); pending_updated = sync.pending_updated @ (List.map (fun s -> match Jmap.Id.of_string s with Ok id -> id | Error e -> failwith e) updated); pending_destroyed = sync.pending_destroyed @ (List.map (fun s -> match Jmap.Id.of_string s with Ok id -> id | Error e -> failwith e) destroyed); } let clear_pending sync = { sync with pending_created = []; pending_updated = []; pending_destroyed = []; } let get_pending sync = (sync.pending_created, sync.pending_updated, sync.pending_destroyed) let needs_sync sync response = Changes_response.has_more_changes response || sync.pending_created <> [] || sync.pending_updated <> [] || sync.pending_destroyed <> [] end (** Utility to merge multiple change responses *) let merge_changes responses = List.fold_left (fun (created, updated, destroyed) response -> let c = List.map (fun id -> match Jmap.Id.of_string id with | Ok id_t -> id_t | Error _ -> failwith ("Invalid ID: " ^ id)) (Changes_response.created response) in let u = List.map (fun id -> match Jmap.Id.of_string id with | Ok id_t -> id_t | Error _ -> failwith ("Invalid ID: " ^ id)) (Changes_response.updated response) in let d = List.map (fun id -> match Jmap.Id.of_string id with | Ok id_t -> id_t | Error _ -> failwith ("Invalid ID: " ^ id)) (Changes_response.destroyed response) in (created @ c, updated @ u, destroyed @ d) ) ([], [], []) responses (** Get updated properties if available *) let get_updated_properties response = Changes_response.updated_properties response