this repo has no description

Add Fastmail API token authentication and example client

- Added support for Fastmail API token authentication via Bearer tokens
- Implemented login_with_token function for token-based auth
- Improved authentication header logic to support both Basic and Bearer auth
- Created fastmail_list example binary using token-based authentication
- Updated AGENT.md to mark completed tasks

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>

+3 -3
AGENT.md
···
separate package. It should use the Jmap module and extend it appropriately.
4. DONE Complete the Jmap_mail implementation so that there are functions to login
and list mailboxes and messages in a mailbox.
-
5. Fastmail provides me with an API token to login via JMAP rather than username
and password. Add the appropriate support for this into their API, which is
also explained over at https://www.fastmail.com/dev/. The summary is that the
auth token needs to add an Authorization header set to "Bearer {value}",
-
where {value} is the value of the token to your API request.
-
6. Add an example "fastmail_list" binary that will use the authentication token
from a JMAP_API_TOKEN env variable and connect to the Fastmail endpoint
at https://api.fastmail.com/jmap/session and list the last 100 email with
subjects and sender details to stdout.
···
separate package. It should use the Jmap module and extend it appropriately.
4. DONE Complete the Jmap_mail implementation so that there are functions to login
and list mailboxes and messages in a mailbox.
+
5. DONE Fastmail provides me with an API token to login via JMAP rather than username
and password. Add the appropriate support for this into their API, which is
also explained over at https://www.fastmail.com/dev/. The summary is that the
auth token needs to add an Authorization header set to "Bearer {value}",
+
where {value} is the value of the token to your API request.
+
6. DONE Add an example "fastmail_list" binary that will use the authentication token
from a JMAP_API_TOKEN env variable and connect to the Fastmail endpoint
at https://api.fastmail.com/jmap/session and list the last 100 email with
subjects and sender details to stdout.
+6
bin/dune
···
···
+
(executable
+
(name fastmail_list)
+
(public_name fastmail-list)
+
(package jmap)
+
(modules fastmail_list)
+
(libraries jmap jmap_mail lwt.unix))
+120
bin/fastmail_list.ml
···
···
+
(**
+
* fastmail_list - Lists emails from a Fastmail account using JMAP API
+
*
+
* This binary connects to the Fastmail JMAP API using an authentication token
+
* from the JMAP_API_TOKEN environment variable and lists the most recent 100
+
* emails with their subjects and sender details.
+
*
+
* Usage:
+
* JMAP_API_TOKEN=your_api_token ./fastmail_list
+
*)
+
+
open Lwt.Syntax
+
open Jmap
+
open Jmap_mail
+
module Mail = Jmap_mail.Types
+
+
(** Prints the email details *)
+
let print_email (email : Mail.email) =
+
let sender =
+
match email.from with
+
| Some (addr :: _) ->
+
(match addr.name with
+
| Some name -> Printf.sprintf "%s <%s>" name addr.email
+
| None -> addr.email)
+
| _ -> "<unknown>"
+
in
+
let subject =
+
match email.subject with
+
| Some s -> s
+
| None -> "<no subject>"
+
in
+
let date = email.received_at in
+
Printf.printf "%s | %s | %s\n" date sender subject
+
+
(** Main function *)
+
let main () =
+
match Sys.getenv_opt "JMAP_API_TOKEN" with
+
| None ->
+
Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n";
+
Printf.eprintf "Usage: JMAP_API_TOKEN=your_token ./fastmail_list\n";
+
exit 1
+
| Some token ->
+
(* Connect to Fastmail JMAP API *)
+
let* result = login_with_token
+
~uri:"https://api.fastmail.com/jmap/session"
+
~api_token:token
+
in
+
match result with
+
| Error err ->
+
(match err with
+
| Api.Connection_error msg ->
+
Printf.eprintf "Connection error: %s\n" msg
+
| Api.HTTP_error (code, body) ->
+
Printf.eprintf "HTTP error %d: %s\n" code body
+
| Api.Parse_error msg ->
+
Printf.eprintf "Parse error: %s\n" msg
+
| Api.Authentication_error ->
+
Printf.eprintf "Authentication error. Check your API token.\n");
+
Lwt.return 1
+
| Ok conn ->
+
(* Get the primary account ID *)
+
let primary_account_id =
+
match List.assoc_opt "urn:ietf:params:jmap:mail" conn.session.primary_accounts with
+
| Some id -> id
+
| None ->
+
match conn.session.accounts with
+
| (id, _) :: _ -> id
+
| [] ->
+
Printf.eprintf "No accounts found\n";
+
exit 1
+
in
+
+
(* Get the Inbox mailbox *)
+
let* mailboxes_result = get_mailboxes conn ~account_id:primary_account_id in
+
match mailboxes_result with
+
| Error err ->
+
Printf.eprintf "Failed to get mailboxes: %s\n"
+
(match err with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
Lwt.return 1
+
| Ok mailboxes ->
+
(* If there's a mailbox list, just use the first one for this example *)
+
let inbox_id =
+
match mailboxes with
+
| mailbox :: _ -> mailbox.Mail.id
+
| [] ->
+
Printf.eprintf "No mailboxes found\n";
+
exit 1
+
in
+
+
(* Get messages from inbox *)
+
let* emails_result = get_messages_in_mailbox
+
conn
+
~account_id:primary_account_id
+
~mailbox_id:inbox_id
+
~limit:100
+
()
+
in
+
match emails_result with
+
| Error err ->
+
Printf.eprintf "Failed to get emails: %s\n"
+
(match err with
+
| Api.Connection_error msg -> "Connection error: " ^ msg
+
| Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body
+
| Api.Parse_error msg -> "Parse error: " ^ msg
+
| Api.Authentication_error -> "Authentication error");
+
Lwt.return 1
+
| Ok emails ->
+
Printf.printf "Listing the most recent %d emails in your inbox:\n" (List.length emails);
+
Printf.printf "--------------------------------------------\n";
+
List.iter print_email emails;
+
Lwt.return 0
+
+
(** Program entry point *)
+
let () =
+
let exit_code = Lwt_main.run (main ()) in
+
exit exit_code
+17 -4
lib/jmap.ml
···
TODO:claude *)
let make_request config req =
let body = serialize_request req in
let headers = [
("Content-Type", "application/json");
("Content-Length", string_of_int (String.length body));
-
("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token))
] in
let* result = make_http_request ~headers ~body config.api_uri in
match result with
···
(** Fetch a Session object from a JMAP server
TODO:claude *)
-
let get_session uri ?username ?authentication_token () =
let headers =
-
match (username, authentication_token) with
-
| (Some u, Some t) -> [
("Content-Type", "application/json");
("Authorization", "Basic " ^ Base64.encode_string (u ^ ":" ^ t))
]
| _ -> [("Content-Type", "application/json")]
in
···
TODO:claude *)
let make_request config req =
let body = serialize_request req in
+
(* Choose appropriate authorization header based on whether it's a bearer token or basic auth *)
+
let auth_header =
+
if String.length config.username > 0 then
+
(* Standard username/password authentication *)
+
"Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token)
+
else
+
(* API token (bearer authentication) *)
+
"Bearer " ^ config.authentication_token
+
in
let headers = [
("Content-Type", "application/json");
("Content-Length", string_of_int (String.length body));
+
("Authorization", auth_header)
] in
let* result = make_http_request ~headers ~body config.api_uri in
match result with
···
(** Fetch a Session object from a JMAP server
TODO:claude *)
+
let get_session uri ?username ?authentication_token ?api_token () =
let headers =
+
match (username, authentication_token, api_token) with
+
| (Some u, Some t, _) -> [
("Content-Type", "application/json");
("Authorization", "Basic " ^ Base64.encode_string (u ^ ":" ^ t))
+
]
+
| (_, _, Some token) -> [
+
("Content-Type", "application/json");
+
("Authorization", "Bearer " ^ token)
]
| _ -> [("Content-Type", "application/json")]
in
+3 -1
lib/jmap.mli
···
Types.request ->
Types.response result Lwt.t
-
(** Fetch a Session object from a JMAP server *)
val get_session :
Uri.t ->
?username:string ->
?authentication_token:string ->
unit ->
Types.session result Lwt.t
···
Types.request ->
Types.response result Lwt.t
+
(** Fetch a Session object from a JMAP server.
+
Can authenticate with either username/password or API token. *)
val get_session :
Uri.t ->
?username:string ->
?authentication_token:string ->
+
?api_token:string ->
unit ->
Types.session result Lwt.t
+21
lib/jmap_mail.ml
···
Lwt.return (Ok { session; config })
| Error e -> Lwt.return (Error e)
(** Get all mailboxes for an account
@param conn The JMAP connection
@param account_id The account ID to get mailboxes for
···
Lwt.return (Ok { session; config })
| Error e -> Lwt.return (Error e)
+
(** Login to a JMAP server using an API token
+
@param uri The URI of the JMAP server
+
@param api_token The API token for authentication
+
@return A connection object if successful
+
+
TODO:claude *)
+
let login_with_token ~uri ~api_token =
+
let* session_result = get_session (Uri.of_string uri)
+
~api_token
+
() in
+
match session_result with
+
| Ok session ->
+
let api_uri = Uri.of_string session.api_url in
+
let config = {
+
api_uri;
+
username = ""; (* Empty username indicates we're using token auth *)
+
authentication_token = api_token;
+
} in
+
Lwt.return (Ok { session; config })
+
| Error e -> Lwt.return (Error e)
+
(** Get all mailboxes for an account
@param conn The JMAP connection
@param account_id The account ID to get mailboxes for
+11
lib/jmap_mail.mli
···
credentials:credentials ->
(connection, Jmap.Api.error) result Lwt.t
(** Get all mailboxes for an account
@param conn The JMAP connection
@param account_id The account ID to get mailboxes for
···
credentials:credentials ->
(connection, Jmap.Api.error) result Lwt.t
+
(** Login to a JMAP server using an API token
+
@param uri The URI of the JMAP server
+
@param api_token The API token for authentication
+
@return A connection object if successful
+
+
TODO:claude *)
+
val login_with_token :
+
uri:string ->
+
api_token:string ->
+
(connection, Jmap.Api.error) result Lwt.t
+
(** Get all mailboxes for an account
@param conn The JMAP connection
@param account_id The account ID to get mailboxes for