From 020c1e77489b772f854bb3288b9c8d2818a6bf9d Mon Sep 17 00:00:00 2001 From: EuAndreh Date: Fri, 18 Apr 2025 02:17:12 -0300 Subject: git mv src/content/* src/content/en/ --- .../en/tils/2021/01/12/curl-awk-emails.adoc | 148 +++++++++++++++++++++ src/content/en/tils/2021/01/17/posix-shebang.adoc | 58 ++++++++ 2 files changed, 206 insertions(+) create mode 100644 src/content/en/tils/2021/01/12/curl-awk-emails.adoc create mode 100644 src/content/en/tils/2021/01/17/posix-shebang.adoc (limited to 'src/content/en/tils/2021/01') diff --git a/src/content/en/tils/2021/01/12/curl-awk-emails.adoc b/src/content/en/tils/2021/01/12/curl-awk-emails.adoc new file mode 100644 index 0000000..d432da2 --- /dev/null +++ b/src/content/en/tils/2021/01/12/curl-awk-emails.adoc @@ -0,0 +1,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: .*$' "$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: + +[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. diff --git a/src/content/en/tils/2021/01/17/posix-shebang.adoc b/src/content/en/tils/2021/01/17/posix-shebang.adoc new file mode 100644 index 0000000..5cf0695 --- /dev/null +++ b/src/content/en/tils/2021/01/17/posix-shebang.adoc @@ -0,0 +1,58 @@ += POSIX sh and shebangs + +:awk-1: link:../../../2020/12/15/shellcheck-repo.html +:awk-2: link:../12/curl-awk-emails.html + +As I {awk-1}[keep moving] {awk-2}[towards POSIX], I'm on the process of +migrating all my Bash scripts to POSIX sh. + +As I dropped `[[`, arrays and other Bashisms, I was left staring at the first +line of every script, wondering what to do: what is the POSIX sh equivalent of +`#!/usr/bin/env bash`? I already knew that POSIX says nothing about shebangs, +and that the portable way to call a POSIX sh script is `sh script.sh`, but +I didn't know what to do with that first line. + +What I had previously was: + +[source,sh] +---- +#!/usr/bin/env bash +set -Eeuo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" +---- + +Obviously, the `$BASH_SOURCE` would be gone, and I would have to adapt some of +my scripts to not rely on the script location. The `-E` and `-o pipefail` +options were also gone, and would be replaced by nothing. + +I converted all of them to: + +[source,sh] +---- +#!/bin/sh -eu +---- + +I moved the `-eu` options to the shebang line itself, striving for conciseness. +But as I changed callers from `./script.sh` to `sh script.sh`, things started to +fail. Some tests that should fail reported errors, but didn't return 1. + +My first reaction was to revert back to `./script.sh`, but the POSIX bug I +caught is a strong strain, and when I went back to it, I figured that the +callers were missing some flags. Specifically, `sh -eu script.sh`. + +Then it clicked: when running with `sh script.sh`, the shebang line with the sh +options is ignored, as it is a comment! + +Which means that the shebang most friendly with POSIX is: + +[source,sh] +---- +#!/bin/sh +set -eu +---- + +. when running via `./script.sh`, if the system has an executable at `/bin/sh`, + it will be used to run the script; +. when running via `sh script.sh`, the sh options aren't ignored as previously. + +TIL. -- cgit v1.2.3