this repo has no description
at main 214 lines 6.8 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** JMAP CLI configuration and cmdliner terms *) 7 8open Cmdliner 9 10(** {1 Configuration Types} *) 11 12type source = Default | Env of string | Cmdline 13 14type config = { 15 session_url : string; 16 session_url_source : source; 17 api_key : string; 18 api_key_source : source; 19 account_id : string option; 20 account_id_source : source; 21 debug : bool; 22} 23 24(** {1 Pretty Printing} *) 25 26let pp_source ppf = function 27 | Default -> Fmt.(styled `Faint string) ppf "default" 28 | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")") 29 | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline" 30 31let pp_config ppf cfg = 32 let pp_field name value source = 33 Fmt.pf ppf "@,%a %a %a" 34 Fmt.(styled `Cyan string) (name ^ ":") 35 Fmt.(styled `Green string) value 36 Fmt.(styled `Faint (brackets pp_source)) source 37 in 38 let pp_opt_field name value_opt source = 39 match value_opt with 40 | None -> () 41 | Some value -> pp_field name value source 42 in 43 Fmt.pf ppf "@[<v>%a" Fmt.(styled `Bold string) "JMAP config:"; 44 pp_field "session_url" cfg.session_url cfg.session_url_source; 45 pp_field "api_key" (String.make (min 8 (String.length cfg.api_key)) '*' ^ "...") cfg.api_key_source; 46 pp_opt_field "account_id" cfg.account_id cfg.account_id_source; 47 Fmt.pf ppf "@]" 48 49(** {1 Cmdliner Terms} *) 50 51let env_var_name suffix = "JMAP_" ^ suffix 52 53let resolve_with_env ~cmdline ~env_var ~default = 54 match cmdline with 55 | Some v -> (v, Cmdline) 56 | None -> 57 match Sys.getenv_opt env_var with 58 | Some v when v <> "" -> (v, Env env_var) 59 | _ -> (default, Default) 60 61let resolve_opt_with_env ~cmdline ~env_var = 62 match cmdline with 63 | Some v -> (Some v, Cmdline) 64 | None -> 65 match Sys.getenv_opt env_var with 66 | Some v when v <> "" -> (Some v, Env env_var) 67 | _ -> (None, Default) 68 69(** Session URL term *) 70let session_url_term = 71 let doc = 72 Printf.sprintf 73 "JMAP session URL. Can also be set with %s environment variable." 74 (env_var_name "SESSION_URL") 75 in 76 Arg.(value & opt (some string) None & info ["url"; "u"] ~docv:"URL" ~doc) 77 78(** API key term *) 79let api_key_term = 80 let doc = 81 Printf.sprintf 82 "JMAP API key or Bearer token. Can also be set with %s environment variable." 83 (env_var_name "API_KEY") 84 in 85 Arg.(value & opt (some string) None & info ["api-key"; "k"] ~docv:"KEY" ~doc) 86 87(** API key file term *) 88let api_key_file_term = 89 let doc = 90 Printf.sprintf 91 "File containing JMAP API key. Can also be set with %s environment variable." 92 (env_var_name "API_KEY_FILE") 93 in 94 Arg.(value & opt (some string) None & info ["api-key-file"; "K"] ~docv:"FILE" ~doc) 95 96(** Account ID term *) 97let account_id_term = 98 let doc = 99 Printf.sprintf 100 "Account ID to use (defaults to primary mail account). Can also be set with %s." 101 (env_var_name "ACCOUNT_ID") 102 in 103 Arg.(value & opt (some string) None & info ["account"; "a"] ~docv:"ID" ~doc) 104 105(** Debug flag term *) 106let debug_term = 107 let doc = "Enable debug output" in 108 Arg.(value & flag & info ["debug"; "d"] ~doc) 109 110(** Read API key from file *) 111let read_api_key_file path = 112 try 113 let ic = open_in path in 114 let key = input_line ic in 115 close_in ic; 116 String.trim key 117 with 118 | Sys_error msg -> failwith (Printf.sprintf "Cannot read API key file: %s" msg) 119 | End_of_file -> failwith "API key file is empty" 120 121(** Combined configuration term *) 122let config_term = 123 let make session_url_opt api_key_opt api_key_file_opt account_id_opt debug = 124 (* Resolve session URL *) 125 let session_url, session_url_source = 126 resolve_with_env 127 ~cmdline:session_url_opt 128 ~env_var:(env_var_name "SESSION_URL") 129 ~default:"" 130 in 131 if session_url = "" then 132 failwith "Session URL is required. Set via --url or JMAP_SESSION_URL"; 133 134 (* Resolve API key - check key file first, then direct key *) 135 let api_key, api_key_source = 136 match api_key_file_opt with 137 | Some path -> (read_api_key_file path, Cmdline) 138 | None -> 139 match Sys.getenv_opt (env_var_name "API_KEY_FILE") with 140 | Some path when path <> "" -> (read_api_key_file path, Env (env_var_name "API_KEY_FILE")) 141 | _ -> 142 resolve_with_env 143 ~cmdline:api_key_opt 144 ~env_var:(env_var_name "API_KEY") 145 ~default:"" 146 in 147 if api_key = "" then 148 failwith "API key is required. Set via --api-key, --api-key-file, JMAP_API_KEY, or JMAP_API_KEY_FILE"; 149 150 (* Resolve account ID (optional) *) 151 let account_id, account_id_source = 152 resolve_opt_with_env 153 ~cmdline:account_id_opt 154 ~env_var:(env_var_name "ACCOUNT_ID") 155 in 156 157 { session_url; session_url_source; 158 api_key; api_key_source; 159 account_id; account_id_source; 160 debug } 161 in 162 Term.(const make $ session_url_term $ api_key_term $ api_key_file_term 163 $ account_id_term $ debug_term) 164 165(** {1 Environment Documentation} *) 166 167let env_docs = 168 {| 169Environment Variables: 170 JMAP_SESSION_URL JMAP session URL (e.g., https://api.fastmail.com/jmap/session) 171 JMAP_API_KEY API key or Bearer token for authentication 172 JMAP_API_KEY_FILE Path to file containing API key 173 JMAP_ACCOUNT_ID Account ID to use (optional, defaults to primary mail account) 174 175Configuration Precedence: 176 1. Command-line flags (e.g., --url, --api-key) 177 2. Environment variables (e.g., JMAP_SESSION_URL) 178 179Example: 180 export JMAP_SESSION_URL="https://api.fastmail.com/jmap/session" 181 export JMAP_API_KEY_FILE="$HOME/.jmap-api-key" 182 jmap emails --limit 10 183|} 184 185(** {1 Client Helpers} *) 186 187let create_client ~sw env cfg = 188 let requests = Requests.create ~sw env in 189 let auth = Requests.Auth.bearer ~token:cfg.api_key in 190 match Client.create_from_url ~auth requests cfg.session_url with 191 | Error e -> 192 Fmt.epr "@[<v>%a Failed to connect: %s@]@." 193 Fmt.(styled `Red string) "Error:" 194 (Client.error_to_string e); 195 exit 1 196 | Ok client -> client 197 198let get_account_id cfg client = 199 match cfg.account_id with 200 | Some id -> Jmap.Proto.Id.of_string_exn id 201 | None -> 202 let session = Client.session client in 203 match Jmap.Proto.Session.primary_account_for Jmap.Proto.Capability.mail session with 204 | Some id -> id 205 | None -> 206 Fmt.epr "@[<v>%a No primary mail account found. Specify --account.@]@." 207 Fmt.(styled `Red string) "Error:"; 208 exit 1 209 210let debug cfg fmt = 211 if cfg.debug then 212 Fmt.kpf (fun ppf -> Fmt.pf ppf "@.") Fmt.stderr ("@[<h>[DEBUG] " ^^ fmt ^^ "@]") 213 else 214 Format.ikfprintf ignore Format.err_formatter fmt