This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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>

+181 -8
+3 -3
AGENT.md
··· 26 26 separate package. It should use the Jmap module and extend it appropriately. 27 27 4. DONE Complete the Jmap_mail implementation so that there are functions to login 28 28 and list mailboxes and messages in a mailbox. 29 - 5. Fastmail provides me with an API token to login via JMAP rather than username 29 + 5. DONE Fastmail provides me with an API token to login via JMAP rather than username 30 30 and password. Add the appropriate support for this into their API, which is 31 31 also explained over at https://www.fastmail.com/dev/. The summary is that the 32 32 auth token needs to add an Authorization header set to "Bearer {value}", 33 - where {value} is the value of the token to your API request. 34 - 6. Add an example "fastmail_list" binary that will use the authentication token 33 + where {value} is the value of the token to your API request. 34 + 6. DONE Add an example "fastmail_list" binary that will use the authentication token 35 35 from a JMAP_API_TOKEN env variable and connect to the Fastmail endpoint 36 36 at https://api.fastmail.com/jmap/session and list the last 100 email with 37 37 subjects and sender details to stdout.
+6
bin/dune
··· 1 + (executable 2 + (name fastmail_list) 3 + (public_name fastmail-list) 4 + (package jmap) 5 + (modules fastmail_list) 6 + (libraries jmap jmap_mail lwt.unix))
+120
bin/fastmail_list.ml
··· 1 + (** 2 + * fastmail_list - Lists emails from a Fastmail account using JMAP API 3 + * 4 + * This binary connects to the Fastmail JMAP API using an authentication token 5 + * from the JMAP_API_TOKEN environment variable and lists the most recent 100 6 + * emails with their subjects and sender details. 7 + * 8 + * Usage: 9 + * JMAP_API_TOKEN=your_api_token ./fastmail_list 10 + *) 11 + 12 + open Lwt.Syntax 13 + open Jmap 14 + open Jmap_mail 15 + module Mail = Jmap_mail.Types 16 + 17 + (** Prints the email details *) 18 + let print_email (email : Mail.email) = 19 + let sender = 20 + match email.from with 21 + | Some (addr :: _) -> 22 + (match addr.name with 23 + | Some name -> Printf.sprintf "%s <%s>" name addr.email 24 + | None -> addr.email) 25 + | _ -> "<unknown>" 26 + in 27 + let subject = 28 + match email.subject with 29 + | Some s -> s 30 + | None -> "<no subject>" 31 + in 32 + let date = email.received_at in 33 + Printf.printf "%s | %s | %s\n" date sender subject 34 + 35 + (** Main function *) 36 + let main () = 37 + match Sys.getenv_opt "JMAP_API_TOKEN" with 38 + | None -> 39 + Printf.eprintf "Error: JMAP_API_TOKEN environment variable not set\n"; 40 + Printf.eprintf "Usage: JMAP_API_TOKEN=your_token ./fastmail_list\n"; 41 + exit 1 42 + | Some token -> 43 + (* Connect to Fastmail JMAP API *) 44 + let* result = login_with_token 45 + ~uri:"https://api.fastmail.com/jmap/session" 46 + ~api_token:token 47 + in 48 + match result with 49 + | Error err -> 50 + (match err with 51 + | Api.Connection_error msg -> 52 + Printf.eprintf "Connection error: %s\n" msg 53 + | Api.HTTP_error (code, body) -> 54 + Printf.eprintf "HTTP error %d: %s\n" code body 55 + | Api.Parse_error msg -> 56 + Printf.eprintf "Parse error: %s\n" msg 57 + | Api.Authentication_error -> 58 + Printf.eprintf "Authentication error. Check your API token.\n"); 59 + Lwt.return 1 60 + | Ok conn -> 61 + (* Get the primary account ID *) 62 + let primary_account_id = 63 + match List.assoc_opt "urn:ietf:params:jmap:mail" conn.session.primary_accounts with 64 + | Some id -> id 65 + | None -> 66 + match conn.session.accounts with 67 + | (id, _) :: _ -> id 68 + | [] -> 69 + Printf.eprintf "No accounts found\n"; 70 + exit 1 71 + in 72 + 73 + (* Get the Inbox mailbox *) 74 + let* mailboxes_result = get_mailboxes conn ~account_id:primary_account_id in 75 + match mailboxes_result with 76 + | Error err -> 77 + Printf.eprintf "Failed to get mailboxes: %s\n" 78 + (match err with 79 + | Api.Connection_error msg -> "Connection error: " ^ msg 80 + | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body 81 + | Api.Parse_error msg -> "Parse error: " ^ msg 82 + | Api.Authentication_error -> "Authentication error"); 83 + Lwt.return 1 84 + | Ok mailboxes -> 85 + (* If there's a mailbox list, just use the first one for this example *) 86 + let inbox_id = 87 + match mailboxes with 88 + | mailbox :: _ -> mailbox.Mail.id 89 + | [] -> 90 + Printf.eprintf "No mailboxes found\n"; 91 + exit 1 92 + in 93 + 94 + (* Get messages from inbox *) 95 + let* emails_result = get_messages_in_mailbox 96 + conn 97 + ~account_id:primary_account_id 98 + ~mailbox_id:inbox_id 99 + ~limit:100 100 + () 101 + in 102 + match emails_result with 103 + | Error err -> 104 + Printf.eprintf "Failed to get emails: %s\n" 105 + (match err with 106 + | Api.Connection_error msg -> "Connection error: " ^ msg 107 + | Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body 108 + | Api.Parse_error msg -> "Parse error: " ^ msg 109 + | Api.Authentication_error -> "Authentication error"); 110 + Lwt.return 1 111 + | Ok emails -> 112 + Printf.printf "Listing the most recent %d emails in your inbox:\n" (List.length emails); 113 + Printf.printf "--------------------------------------------\n"; 114 + List.iter print_email emails; 115 + Lwt.return 0 116 + 117 + (** Program entry point *) 118 + let () = 119 + let exit_code = Lwt_main.run (main ()) in 120 + exit exit_code
+17 -4
lib/jmap.ml
··· 414 414 TODO:claude *) 415 415 let make_request config req = 416 416 let body = serialize_request req in 417 + (* Choose appropriate authorization header based on whether it's a bearer token or basic auth *) 418 + let auth_header = 419 + if String.length config.username > 0 then 420 + (* Standard username/password authentication *) 421 + "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token) 422 + else 423 + (* API token (bearer authentication) *) 424 + "Bearer " ^ config.authentication_token 425 + in 417 426 let headers = [ 418 427 ("Content-Type", "application/json"); 419 428 ("Content-Length", string_of_int (String.length body)); 420 - ("Authorization", "Basic " ^ Base64.encode_string (config.username ^ ":" ^ config.authentication_token)) 429 + ("Authorization", auth_header) 421 430 ] in 422 431 let* result = make_http_request ~headers ~body config.api_uri in 423 432 match result with ··· 486 495 (** Fetch a Session object from a JMAP server 487 496 488 497 TODO:claude *) 489 - let get_session uri ?username ?authentication_token () = 498 + let get_session uri ?username ?authentication_token ?api_token () = 490 499 let headers = 491 - match (username, authentication_token) with 492 - | (Some u, Some t) -> [ 500 + match (username, authentication_token, api_token) with 501 + | (Some u, Some t, _) -> [ 493 502 ("Content-Type", "application/json"); 494 503 ("Authorization", "Basic " ^ Base64.encode_string (u ^ ":" ^ t)) 504 + ] 505 + | (_, _, Some token) -> [ 506 + ("Content-Type", "application/json"); 507 + ("Authorization", "Bearer " ^ token) 495 508 ] 496 509 | _ -> [("Content-Type", "application/json")] 497 510 in
+3 -1
lib/jmap.mli
··· 336 336 Types.request -> 337 337 Types.response result Lwt.t 338 338 339 - (** Fetch a Session object from a JMAP server *) 339 + (** Fetch a Session object from a JMAP server. 340 + Can authenticate with either username/password or API token. *) 340 341 val get_session : 341 342 Uri.t -> 342 343 ?username:string -> 343 344 ?authentication_token:string -> 345 + ?api_token:string -> 344 346 unit -> 345 347 Types.session result Lwt.t 346 348
+21
lib/jmap_mail.ml
··· 1073 1073 Lwt.return (Ok { session; config }) 1074 1074 | Error e -> Lwt.return (Error e) 1075 1075 1076 + (** Login to a JMAP server using an API token 1077 + @param uri The URI of the JMAP server 1078 + @param api_token The API token for authentication 1079 + @return A connection object if successful 1080 + 1081 + TODO:claude *) 1082 + let login_with_token ~uri ~api_token = 1083 + let* session_result = get_session (Uri.of_string uri) 1084 + ~api_token 1085 + () in 1086 + match session_result with 1087 + | Ok session -> 1088 + let api_uri = Uri.of_string session.api_url in 1089 + let config = { 1090 + api_uri; 1091 + username = ""; (* Empty username indicates we're using token auth *) 1092 + authentication_token = api_token; 1093 + } in 1094 + Lwt.return (Ok { session; config }) 1095 + | Error e -> Lwt.return (Error e) 1096 + 1076 1097 (** Get all mailboxes for an account 1077 1098 @param conn The JMAP connection 1078 1099 @param account_id The account ID to get mailboxes for
+11
lib/jmap_mail.mli
··· 866 866 credentials:credentials -> 867 867 (connection, Jmap.Api.error) result Lwt.t 868 868 869 + (** Login to a JMAP server using an API token 870 + @param uri The URI of the JMAP server 871 + @param api_token The API token for authentication 872 + @return A connection object if successful 873 + 874 + TODO:claude *) 875 + val login_with_token : 876 + uri:string -> 877 + api_token:string -> 878 + (connection, Jmap.Api.error) result Lwt.t 879 + 869 880 (** Get all mailboxes for an account 870 881 @param conn The JMAP connection 871 882 @param account_id The account ID to get mailboxes for