this repo has no description
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