(** JMAP email sending utility for Fastmail This utility sends an email via JMAP to recipients specified on the command line. The subject is provided as a command-line argument, and the message body is read from standard input. Usage: fastmail_send --to=recipient@example.com [--to=another@example.com ...] --subject="Email subject" Environment variables: - JMAP_API_TOKEN: Required. The Fastmail API token for authentication. - JMAP_FROM_EMAIL: Optional. The sender's email address. If not provided, uses the first identity. @see RFC8621 Section 7 *) open Lwt.Syntax open Cmdliner let log_error fmt = Fmt.epr ("\u{1b}[1;31mError: \u{1b}[0m" ^^ fmt ^^ "@.") let log_info fmt = Fmt.pr ("\u{1b}[1;34mInfo: \u{1b}[0m" ^^ fmt ^^ "@.") let log_success fmt = Fmt.pr ("\u{1b}[1;32mSuccess: \u{1b}[0m" ^^ fmt ^^ "@.") (** Read the entire message body from stdin *) let read_message_body () = let buffer = Buffer.create 1024 in let rec read_lines () = try let line = input_line stdin in Buffer.add_string buffer line; Buffer.add_char buffer '\n'; read_lines () with | End_of_file -> Buffer.contents buffer in read_lines () (** Main function to send an email *) let send_email to_addresses subject from_email = (* Check for API token in environment *) match Sys.getenv_opt "JMAP_API_TOKEN" with | None -> log_error "JMAP_API_TOKEN environment variable not set"; exit 1 | Some token -> (* Read message body from stdin *) log_info "Reading message body from stdin (press Ctrl+D when finished)..."; let message_body = read_message_body () in if message_body = "" then log_info "No message body entered, using a blank message"; (* Initialize JMAP connection *) let fastmail_uri = "https://api.fastmail.com/jmap/session" in Lwt_main.run begin let* conn_result = Jmap.Proto.login_with_token ~uri:fastmail_uri ~api_token:token in match conn_result with | Error err -> let msg = Jmap.Api.string_of_error err in log_error "Failed to connect to Fastmail: %s" msg; Lwt.return 1 | Ok conn -> (* Get primary account ID *) let account_id = (* Get the primary account - first personal account in the list *) let (_, _account) = List.find (fun (_, acc) -> acc.Jmap.Types.is_personal) conn.session.accounts in (* Use the first account id as primary *) (match conn.session.primary_accounts with | (_, id) :: _ -> id | [] -> (* Fallback if no primary accounts defined *) let (id, _) = List.hd conn.session.accounts in id) in (* Determine sender email address *) let* from_email_result = match from_email with | Some email -> Lwt.return_ok email | None -> (* Get first available identity *) let* identities_result = Jmap.Proto.get_identities conn ~account_id in match identities_result with | Ok [] -> log_error "No identities found for account"; Lwt.return_error "No identities found" | Ok (identity :: _) -> Lwt.return_ok identity.email | Error err -> let msg = Jmap.Api.string_of_error err in log_error "Failed to get identities: %s" msg; Lwt.return_error msg in match from_email_result with | Error _msg -> Lwt.return 1 | Ok from_email -> (* Send the email *) log_info "Sending email from %s to %s" from_email (String.concat ", " to_addresses); let* submission_result = Jmap.Proto.create_and_submit_email conn ~account_id ~from:from_email ~to_addresses ~subject ~text_body:message_body () in match submission_result with | Error err -> let msg = Jmap.Api.string_of_error err in log_error "Failed to send email: %s" msg; Lwt.return 1 | Ok submission_id -> log_success "Email sent successfully (Submission ID: %s)" submission_id; (* Wait briefly then check submission status *) let* () = Lwt_unix.sleep 1.0 in let* status_result = Jmap.Proto.get_submission_status conn ~account_id ~submission_id in (match status_result with | Ok status -> let status_text = match status.Jmap.Proto.Types.undo_status with | Some `pending -> "Pending" | Some `final -> "Final (delivered)" | Some `canceled -> "Canceled" | None -> "Unknown" in log_info "Submission status: %s" status_text; (match status.Jmap.Proto.Types.delivery_status with | Some statuses -> List.iter (fun (email, status) -> let delivery = match status.Jmap.Proto.Types.delivered with | Some "yes" -> "Delivered" | Some "no" -> "Failed" | Some "queued" -> "Queued" | Some s -> s | None -> "Unknown" in log_info "Delivery to %s: %s" email delivery ) statuses | None -> ()); Lwt.return 0 | Error _ -> (* We don't fail if status check fails, as the email might still be sent *) Lwt.return 0) end (** Command line interface *) let to_addresses = let doc = "Email address of the recipient (can be specified multiple times)" in Arg.(value & opt_all string [] & info ["to"] ~docv:"EMAIL" ~doc) let subject = let doc = "Subject line for the email" in Arg.(required & opt (some string) None & info ["subject"] ~docv:"SUBJECT" ~doc) let from_email = let doc = "Sender's email address (optional, defaults to primary identity)" in Arg.(value & opt (some string) None & info ["from"] ~docv:"EMAIL" ~doc) let cmd = let doc = "Send an email via JMAP to Fastmail" in let info = Cmd.info "fastmail_send" ~doc in Cmd.v info Term.(const send_email $ to_addresses $ subject $ from_email) let () = match Cmd.eval_value cmd with | Ok (`Ok code) -> exit code | Ok (`Version | `Help) -> exit 0 | Error _ -> exit 1