this repo has no description
at main 7.4 kB view raw
1(** JMAP email sending utility for Fastmail 2 3 This utility sends an email via JMAP to recipients specified on the command line. 4 The subject is provided as a command-line argument, and the message body is read 5 from standard input. 6 7 Usage: 8 fastmail_send --to=recipient@example.com [--to=another@example.com ...] --subject="Email subject" 9 10 Environment variables: 11 - JMAP_API_TOKEN: Required. The Fastmail API token for authentication. 12 - JMAP_FROM_EMAIL: Optional. The sender's email address. If not provided, uses the first identity. 13 14 @see <https://datatracker.ietf.org/doc/html/rfc8621#section-7> RFC8621 Section 7 15*) 16 17open Lwt.Syntax 18open Cmdliner 19 20let log_error fmt = Fmt.epr ("\u{1b}[1;31mError: \u{1b}[0m" ^^ fmt ^^ "@.") 21let log_info fmt = Fmt.pr ("\u{1b}[1;34mInfo: \u{1b}[0m" ^^ fmt ^^ "@.") 22let log_success fmt = Fmt.pr ("\u{1b}[1;32mSuccess: \u{1b}[0m" ^^ fmt ^^ "@.") 23 24(** Read the entire message body from stdin *) 25let read_message_body () = 26 let buffer = Buffer.create 1024 in 27 let rec read_lines () = 28 try 29 let line = input_line stdin in 30 Buffer.add_string buffer line; 31 Buffer.add_char buffer '\n'; 32 read_lines () 33 with 34 | End_of_file -> Buffer.contents buffer 35 in 36 read_lines () 37 38(** Main function to send an email *) 39let send_email to_addresses subject from_email = 40 (* Check for API token in environment *) 41 match Sys.getenv_opt "JMAP_API_TOKEN" with 42 | None -> 43 log_error "JMAP_API_TOKEN environment variable not set"; 44 exit 1 45 | Some token -> 46 (* Read message body from stdin *) 47 log_info "Reading message body from stdin (press Ctrl+D when finished)..."; 48 let message_body = read_message_body () in 49 if message_body = "" then 50 log_info "No message body entered, using a blank message"; 51 52 (* Initialize JMAP connection *) 53 let fastmail_uri = "https://api.fastmail.com/jmap/session" in 54 Lwt_main.run begin 55 let* conn_result = Jmap_mail.login_with_token ~uri:fastmail_uri ~api_token:token in 56 match conn_result with 57 | Error err -> 58 let msg = Jmap.Api.string_of_error err in 59 log_error "Failed to connect to Fastmail: %s" msg; 60 Lwt.return 1 61 | Ok conn -> 62 (* Get primary account ID *) 63 let account_id = 64 (* Get the primary account - first personal account in the list *) 65 let (_, _account) = List.find (fun (_, acc) -> 66 acc.Jmap.Types.is_personal) conn.session.accounts in 67 (* Use the first account id as primary *) 68 (match conn.session.primary_accounts with 69 | (_, id) :: _ -> id 70 | [] -> 71 (* Fallback if no primary accounts defined *) 72 let (id, _) = List.hd conn.session.accounts in 73 id) 74 in 75 76 (* Determine sender email address *) 77 let* from_email_result = match from_email with 78 | Some email -> Lwt.return_ok email 79 | None -> 80 (* Get first available identity *) 81 let* identities_result = Jmap_mail.get_identities conn ~account_id in 82 match identities_result with 83 | Ok [] -> 84 log_error "No identities found for account"; 85 Lwt.return_error "No identities found" 86 | Ok (identity :: _) -> Lwt.return_ok identity.email 87 | Error err -> 88 let msg = Jmap.Api.string_of_error err in 89 log_error "Failed to get identities: %s" msg; 90 Lwt.return_error msg 91 in 92 93 match from_email_result with 94 | Error _msg -> Lwt.return 1 95 | Ok from_email -> 96 (* Send the email *) 97 log_info "Sending email from %s to %s" 98 from_email 99 (String.concat ", " to_addresses); 100 101 let* submission_result = 102 Jmap_mail.create_and_submit_email 103 conn 104 ~account_id 105 ~from:from_email 106 ~to_addresses 107 ~subject 108 ~text_body:message_body 109 () 110 in 111 112 match submission_result with 113 | Error err -> 114 let msg = Jmap.Api.string_of_error err in 115 log_error "Failed to send email: %s" msg; 116 Lwt.return 1 117 | Ok submission_id -> 118 log_success "Email sent successfully (Submission ID: %s)" submission_id; 119 (* Wait briefly then check submission status *) 120 let* () = Lwt_unix.sleep 1.0 in 121 let* status_result = Jmap_mail.get_submission_status 122 conn 123 ~account_id 124 ~submission_id 125 in 126 127 (match status_result with 128 | Ok status -> 129 let status_text = match status.Jmap_mail.Types.undo_status with 130 | Some `pending -> "Pending" 131 | Some `final -> "Final (delivered)" 132 | Some `canceled -> "Canceled" 133 | None -> "Unknown" 134 in 135 log_info "Submission status: %s" status_text; 136 137 (match status.Jmap_mail.Types.delivery_status with 138 | Some statuses -> 139 List.iter (fun (email, status) -> 140 let delivery = match status.Jmap_mail.Types.delivered with 141 | Some "yes" -> "Delivered" 142 | Some "no" -> "Failed" 143 | Some "queued" -> "Queued" 144 | Some s -> s 145 | None -> "Unknown" 146 in 147 log_info "Delivery to %s: %s" email delivery 148 ) statuses 149 | None -> ()); 150 Lwt.return 0 151 | Error _ -> 152 (* We don't fail if status check fails, as the email might still be sent *) 153 Lwt.return 0) 154 end 155 156(** Command line interface *) 157let to_addresses = 158 let doc = "Email address of the recipient (can be specified multiple times)" in 159 Arg.(value & opt_all string [] & info ["to"] ~docv:"EMAIL" ~doc) 160 161let subject = 162 let doc = "Subject line for the email" in 163 Arg.(required & opt (some string) None & info ["subject"] ~docv:"SUBJECT" ~doc) 164 165let from_email = 166 let doc = "Sender's email address (optional, defaults to primary identity)" in 167 Arg.(value & opt (some string) None & info ["from"] ~docv:"EMAIL" ~doc) 168 169let cmd = 170 let doc = "Send an email via JMAP to Fastmail" in 171 let info = Cmd.info "fastmail_send" ~doc in 172 Cmd.v info Term.(const send_email $ to_addresses $ subject $ from_email) 173 174let () = match Cmd.eval_value cmd with 175 | Ok (`Ok code) -> exit code 176 | Ok (`Version | `Help) -> exit 0 177 | Error _ -> exit 1