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