aboutsummaryrefslogtreecommitdiff
path: root/_tils/2021-01-12-awk-snippet-send-email-to-multiple-recipients-with-curl.md
blob: 880ddf1e2a48ca985b480e43c4dcc83b0223f2a3 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
---

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: .*<addr@server1\.org>$' "$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: .*<addr@server2\.org>$' "$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/