--- title: 'Awk snippet: send email to multiple recipients with cURL' date: 2021-01-12 layout: post lang: en ref: awk-snippet-send-email-to-multiple-recipients-with-curl --- As I experiment with [Neomutt][neomutt], I wanted to keep being able to enqueue emails for sending later like my previous setup, so that I didn't rely on having an internet connection. My requirements for the `sendmail` command were: 1. store the email in a file, and send it later. 1. send from different addresses, using different SMTP servers; I couldn't find an MTA that could accomplish that, but I was able to quickly write a solution. The first part was the easiest: store the email in a file: ```shell # ~/.config/mutt/muttrc: set sendmail=~/bin/enqueue-email.sh # ~/bin/enqueue-email.sh: #!/bin/sh -eu cat - > "$HOME/mbsync/my-queued-emails/$(date -Is)" ``` Now that I had the email file store locally, I needed a program to send the email from the file, so that I could create a cronjob like: ```shell for f in ~/mbsync/my-queued-emails/*; do ~/bin/dispatch-email.sh "$f" && rm "$f" done ``` The `dispatch-email.sh` would have to look at the `From: ` header and decide which SMTP server to use. As I [found out][curl-email] that [curl][curl] supports SMTP and is able to send emails, this is what I ended up with: ```shell #!/bin/sh -eu F="$1" rcpt="$(awk ' match($0, /^(To|Cc|Bcc): (.*)$/, m) { split(m[2], tos, ",") for (i in tos) { print "--mail-rcpt " tos[i] } } ' "$F")" if grep -qE '^From: .*$' "$F"; then curl \ -s \ --url smtp://smtp.server1.org:587 \ --ssl-reqd \ --mail-from addr@server1.org \ $rcpt \ --user 'addr@server1.org:my-long-and-secure-passphrase' \ --upload-file "$F" elif grep -qE '^From: .*$' "$F"; then curl \ -s \ --url smtp://smtp.server2.org:587 \ --ssl-reqd \ --mail-from addr@server2.org \ $rcpt \ --user 'addr@server2.org:my-long-and-secure-passphrase' \ --upload-file "$F" else echo 'Bad "From: " address' exit 1 fi ``` Most of curl flags used are self-explanatory, except for `$rcpt`. curl connects to the SMTP server, but doesn't set the recipient address by looking at the message. My solution was to generate the curl flags, store them in `$rcpt` and use it unquoted to leverage shell word splitting. To me, the most interesting part was building the `$rcpt` flags. My first instinct was to try grep, but it couldn't print only matches in a regex. As I started to turn towards sed, I envisioned needing something else to loop over the sed output, and I then moved to Awk. In the short Awk snippet, 3 things were new to me: the `match(...)`, `split(...)` and `for () {}`. The only other function I have ever used was `gsub(...)`, but these new ones felt similar enough that I could almost guess their behaviour and arguments. `match(...)` stores the matches of a regex on the given array positionally, and `split(...)` stores the chunks in the given array. I even did it incrementally: ```shell $ H='To: to@example.com, to2@example.com\nCc: cc@example.com, cc2@example.com\nBcc: bcc@example.com,bcc2@example.com\n' $ printf "$H" | awk '/^To: .*$/ { print $0 }' To: to@example.com, to2@example.com $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { print m }' awk: ligne de commande:1: (FILENAME=- FNR=1) fatal : tentative d'utilisation du tableau « m » dans un contexte scalaire $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { print m[0] }' To: to@example.com, to2@example.com $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { print m[1] }' to@example.com, to2@example.com $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { split(m[1], tos, " "); print tos }' awk: ligne de commande:1: (FILENAME=- FNR=1) fatal : tentative d'utilisation du tableau « tos » dans un contexte scalaire $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { split(m[1], tos, " "); print tos[0] }' $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { split(m[1], tos, " "); print tos[1] }' to@example.com, $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { split(m[1], tos, " "); print tos[2] }' to2@example.com $ printf "$H" | awk 'match($0, /^To: (.*)$/, m) { split(m[1], tos, " "); print tos[3] }' ``` (This isn't the verbatim interactive session, but a cleaned version to make it more readable.) At this point, I realized I needed a for loop over the `tos` array, and I moved the Awk snippet into the `~/bin/dispatch-email.sh`. I liked the final thing: ```awk match($0, /^(To|Cc|Bcc): (.*)$/, m) { split(m[2], tos, ",") for (i in tos) { print "--mail-rcpt " tos[i] } } ``` As I learn more about Awk, I feel that it is too undervalued, as many people turn to Perl or other programming languages when Awk suffices. The advantage is pretty clear: writing programs that run on any POSIX system, without extra dependencies required. Coding to the standards is underrated. [neomutt]: https://neomutt.org/ [curl-email]: https://blog.edmdesigner.com/send-email-from-linux-command-line/ [curl]: https://curl.se/