When I stand up containers and virtual machines on my home network, I typically set them up with postfix using Gmail’s SMTP servers and my Gmail account so they can send emails like notifications and alerts. But as part of an ongoing effort to diversify my tech stack, I decided to make two leaps at once for new containers and VMs going forward: using sendmail as the MTA instead of postfix, and using Fastmail’s SMTP server.

With Fastmail’s connection details in hand, it was pretty straightforward to complete setup using sendmailconfig. Now that the easy way was figured out, I set my sights on writing my own bash script to do the job.

The complete script is on GitHub. Below I’ll put some discussion and explanation of some of my choices in the script.

For scripts that require root, I’ve experimented over the years with a few different ways of detecting whether the script is being run by root/sudo. Here’s the current incarnation of my check for this:

# check for root/sudo
EUID_copy="${EUID:-$(id -u)}"
if [[ -z "${EUID_copy}" ]]; then
    printf "This script must be run as root, and the current user's EUID could not be determined.\n" >&2 && exit 1
elif ((EUID_copy != 0)); then
    printf "This script must be run as root.\n" >&2 && exit 1
fi

Often I’ll get any necessary arguments from the user using getopts and an elaborate case block. This time I used environment variable passing. So a typical invocation of this script (with made-up details) looks like this:

FASTMAIL_EMAIL_ADDRESS="xxxxxxxx@fastmail.com" FASTMAIL_APP_SPECIFIC_PASSWORD="xxxxxxxxxxxxxxxx" ./setup-mta-sendmail.sh

Getting arguments this way substantially simplified the process of checking for required arguments and setting up any needed defaults, as shown:

# check for required arguments
[[ -z "${FASTMAIL_EMAIL_ADDRESS}" && -z "${FASTMAIL_APP_SPECIFIC_PASSWORD}" ]] && printf "env vars FASTMAIL_EMAIL_ADDRESS and FASTMAIL_APP_SPECIFIC_PASSWORD must be set.\n" >&2 && exit 1
[[ -z "${FASTMAIL_EMAIL_ADDRESS}" ]] && printf "env var FASTMAIL_EMAIL_ADDRESS must be set.\n" >&2 && exit 1
[[ -z "${FASTMAIL_APP_SPECIFIC_PASSWORD}" ]] && printf "env var FASTMAIL_APP_SPECIFIC_PASSWORD must be set.\n" >&2 && exit 1

# set server and port defaults
[[ -z "${SMTP_SERVER}" ]] && SMTP_SERVER="smtp.fastmail.com"
[[ -z "${SMTP_PORT}" ]] && SMTP_PORT="587"

You never know who might run a script you send out into the world, so I try to write my scripts to work as courteously as possible from the get-go. For example, making backups of files (if they exist) instead of blindly overwriting them:

# backup existing SSL/TLS files, if present
if [[ -f "${SENDMAIL_KEY_FILE}" ]]; then
    cp "${SENDMAIL_KEY_FILE}" "${SENDMAIL_KEY_FILE}".bak
fi
if [[ -f "${SENDMAIL_CERT_FILE}" ]]; then
    cp "${SENDMAIL_CERT_FILE}" "${SENDMAIL_CERT_FILE}".bak
fi

The sendmail config file, usually /etc/mail/sendmail.mc, presented a small problem. I needed to add some lines to it, but I couldn’t just append them to the file because some of the directives I needed to add were supposed to be located before other directives already present in the file. To put it another way, there were lines already in the config files that needed to come after lines I was going to add. There were only two directives that needed to be relocated, so in this situation I made the choice to temporarily remove the 2 directives from the config file, append my new lines, and then re-add the original 2 directives so they came after my new lines.

I used a bash array and a loop to process the necessary deletions:

# update sendmail.mc, so first make a backup
cp "${SENDMAIL_CONFIG_FILE_MC}" "${SENDMAIL_CONFIG_FILE_MC}.bak"
# delete some lines and re-add them after we append our new lines
sendmail_mc_lines_to_delete=(
    "MAILER_DEFINITIONS"
    "MAILER(\`local')dnl"
    "MAILER(\`smtp')dnl"
)

for pattern in "${sendmail_mc_lines_to_delete[@]}"; do
    sed -i "/${pattern}/d" "${SENDMAIL_CONFIG_FILE_MC}"
done

And when I appended my new lines, I re-added the directives I had just recently deleted.

Suggestions or corrections for this script are welcome; just submit an issue or PR on GitHub.