Files
web-log/container/site/content/posts/email-alerts/index.md
T
2025-07-05 15:36:34 +00:00

307 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
+++
categories = ["software"]
tags = ["automation"]
date = 2024-06-18T02:47:45Z
description = ""
draft = false
slug = "email-alerts"
title = "📧 Email Alerts with Postfix & Amazon SES"
author = "nicholas"
+++
I have many jobs on a server that run on a schedule using `cron` jobs. I would like to be notified when these jobs succeed, and especially when they fail. I will send and receive these alerts through email.Since it is impractical to run one's own email server in 2024, I am using AWS SES (Amazon Web Services - Simple Email Service) to send the emails. I do not write anything about this here.
## Prerequisites
There are a few parts to configure to get the machine working.
- Email server
- `postfix`
- `mailutils`
## Postfix
This may prompt configuration GUI (`dpkg-reconfigure postfix`) in the terminal. Select "No configuration".
### Create auth file
An auth file is needed to store credentials before hashing.
```bash
sudo apt install postfix
sudo nano /etc/postfix/aws_ses_sasl
```
`SMTP_USERNAME` (SES Access Key for IAM User) and corresponding SMTP_PASSWORD, e.g.:
```
[email-smtp.us-east-2.amazonaws.com]:587 VMHPJVEZMOQJLJMPCHX:WF62TVHpGhqObyn2RvCb92sVxTP5OiihL3IvRmivHRk4j
```
Now, I use the postmap tool to generate a hashed database file that will eventually be used by Postfix to authenticate to the remote SMTP email server. It will output a .db file of the same name as the input file in this case, `aws_ses_sasl.db`.
```shell
sudo postmap hash:/etc/postfix/aws_ses_sasl
```
### Create postfix configuration file
The auth file created above is referenced in the Postfix configuration file `main.cf` that is created below.
```shell
sudo nano /etc/postfix/main.cf
```
The last line above references a hashed database file. This file allows me to map my local email addresses to other addresses.
```sh
relayhost = [email-smtp.us-east-2.amazonaws.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/aws_ses_sasl
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_note_starttls_offer = yes
smtp_generic_maps = hash:/etc/postfix/generic
```
### Alias Configuration
Server-generated email are by default sent to email address `{user}@{hostname}.localdomain`. I need to change this by configuring an alias for users `root` and `nicholas` in `/etc/aliases` so that all emails will be forwarded to my personal email instead of just ending up unread in my syslog.
```shell
sudo nano /etc/aliases
```
```shell
root: nicholas@email.com
nicholas: nicholas@email.com
```
### Create `generic` file
This will configure the sender address. Emails sent from the system will appear to be sent from the emails named here.
```shell
sudo nano /etc/postfix/generic
```
#### `generic`
```
root@server.localdomain example@example.com
nicholas@server.localdomain example@example.com
```
#### Create hash database file
```shell
sudo postmap hash:/etc/postfix/generic
```
### Restart Postfix
```shell
sudo service postfix restart
```
### Test Email
I want to test whether I have configured the email system correctly. I will need `mailutils`.
```shell
sudo apt install mailutils
```
I can set some variables in the shell to reference in a command:
```shell
email_body="test email body"
email_subject="test email subject"
email_to="test@example.com"
email_from="alerts@uuard.com"
```
Now I attempt to send the email.
```shell
echo "${email_body}" | mail -s "${email_subject}" -r "${email_from}" "${email_to}"
```
I check the mail log to verify the email was sent.
```shell
sudo cat /var/log/mail.log | grep to=
2024-06-17T19:30:17.705051-05:00 nas postfix/smtp[3797056]: D9D331661A14: to=, relay=email-smtp.us-east-2.amazonaws.com[3.22.8.243]:587, delay=0.82, delays=0.01/0/0.56/0.25, dsn=2.0.0, status=sent (250 Ok 010f019028c06bff-df62265f-49b2-4920-b73a-8a7e4b362f2c-000000)
```
Notice the `250 Ok`. This means the email was sent successfully. This is confirmed when I check my email inbox, which shows the email has been delivered:
{{< image
src="images/email-success.png"
caption="Test Email" >}}
## Shell Scripts
As I mentioned before, the purpose of configuring the system to send mail is to alert me to the outcome of the execution of scheduled jobs. To do this, I will create shell scripts for each job, within which will contain logic to send custom emails containing information that might be interesting to me.
I have a file `test.txt` that I output using `cat` command. Both the command and the output will be captured by the script, and the script will use the `mailutils` package `mail` command to send this over email. Here is what happens when I run the command manually:
### Formatting emails
The email I sent is not very pretty, and the lack of formatting will result in a nigh unreadable jumble of text if a job produces an error. I want to be able to quickly discern what and why a job failed, so I will format my emails.
The idea is to separate the command that resulted in exix code indicating error, from the error itself. To test this, I will use `cat` as the command, and the contents of a text file as the output (since `cat` will output the contents of the text file).
```shell
cat test.txt
testing
testing...
testing.......
testing..........
it works.
```
If I configure my script correctly, this exact command and output should show up in the email in separate columns. First, I configure the `test.sh` script.
```shell
cmd="cat test.txt"
cmd_output=$($cmd)
email_subject="cron job - show file contents"
email_to="test@email.com"
email_from="alerts@uuard.com"
# check if the cat command succeeded
if [ $? -ne 0 ]; then
email_subject="error!"
exit 1
fi
#create variable email_body
read -r -d '' email_body << EOM
<html>
<head>
<style>
td, th {
font-family: helvetica;
color: #FBF7EF;
text-align:justify;
padding: 1em;
vertical-align:center;
}
table {
background: #2B303B;
}
</style>
</head>
<body>
<table>
<tr>
<th>command</th>
<td><pre>${cmd}</pre></th>
</tr>
<tr>
<th>output</th>
<td><pre>${cmd_output}</pre></td>
</table>
</body>
</html>
EOM
echo -e "${email_body}" | mail --content-type=text/html -s "${email_subject}" -r "${email_from}" "${email_to}"
```
{{< image
src="images/email-formatted.png"
caption="Email Formatted" >}}
## Improvements
### Templating
It occurs to me that it will be tedious and costly to maintain these configs if I want to change things. I decided to lift out the HTML template into a separate file so that I need only edit a single file. Now I have a few files to manage, but fewer lines of duplication between all of the configurations.
#### Template Script
```bash {filename="template-html-email.sh"}
template=$(cat <<'EOF'
<html lang="en">
<head>
<style>
td, th {
font-family: helvetica;
color: #FBF7EF;
text-align:justify;
padding: 1em;
vertical-align:center;
}
table {
background: #2B303B;
}
</style>
</head>
<body>
<table>
<tr>
<th>command</th>
<td><pre>${cmd}</pre></th>
</tr>
<tr>
<th>output</th>
<td><pre>${cmd_output}</pre></td>
</table>
</body>
</html>
EOF
)
```
#### Send Error Email Script
```bash {filename="send-error-email.sh"}
#!/bin/bash
source dry-run-config.sh
cmd="$1"
cmd_output="$2"
export cmd
export cmd_output
email_body=$(echo "$template" | envsubst)
#echo "Command: $cmd"
#echo "Command Output: $cmd_output"
#echo "Email Body: $email_body"
printf "%s" "$email_body" | mail --content-type=text/html -s "${email_subject}" -r "${email_from}" "${email_to}"
```
#### Rclone Dry Run Config
```bash {filename="dry-run-config.sh"}
#!/bin/bash
send_error_email_script="send-error-email.sh"
template_html_email_script="template-html-email.sh"
email_subject="cron alert"
email_to="nicolaspatrickward@gmail.com"
email_from="alert@uuard.com"
source "$template_html_email_script"
```
#### Dry Run
```bash
#!/bin/bash
# dry run config -- shared variables
source dry-run-config.sh
# dry-run command parameters
src_dir="/source"
dest_remote="dry-run" #this is a remote rclone dir
dest_dir="/dest"
# save both the command as executed, and the command output. Pass along to email script for formatting
cmd="docker exec -t rclone rclone copy -v $src_dir $dest_remote:$dest_dir --dry-run"
cmd_output=$($cmd)
if [ $? -ne 0 ]; then
$send_error_email_script "$cmd" "$cmd_output"
fi
```
Done.