summaryrefslogtreecommitdiff
path: root/src/content/tils/2021/01/12/curl-awk-emails.adoc
blob: d432da28fc6b1f87a590e13fe0f1a8407687addd (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
143
144
145
146
147
148
= Awk snippet: send email to multiple recipients with cURL

:neomutt: https://neomutt.org/
:found-out-article: https://blog.edmdesigner.com/send-email-from-linux-command-line/
:curl: https://curl.se/

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:

. store the email in a file, and send it later;
. 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:

[source,sh]
----
# ~/.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:

[source,sh]
----
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-article}[found out] that {curl}[curl]
supports SMTP and is able to send emails, this is what I ended up with:

[source,sh]
----
#!/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:

[source,sh]
----
$ 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:

[source,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.