I run jobs on a regular basis on my QNAP NAS with cron
and I want to be notified if any of these jobs fails or sends
warnings.
Crontab
has a simple and super neat feature called MAILTO
.
When this variable is set, Crontab
sends any output from the cron job to the recipient.
The variable is set by default and the recipient is the current user (owner) of the crontab and the mail is a Linux "in-"mail.
To disable the feature, set the variable to an empty value: MAILTO=""
Sources
Crontab MAILTO not working on QNAP systems¶
Unfortunately, on QNAP NAS systems (at least on mine with QTS 5.X), Crontab
MAILTO
is disabled, not implemented, or
not working.
To work around this on my TS-253E, one can write a Bash script that will mimic the behavior: call a command, and if anything is
written to stdout
or stderr
, send it by email to some recipient.
A script mimicking Crontab's MAILTO¶
Requirements¶
ssmtp
available on path and configured to send out emails
For tips on installing and configuring sSMTP
, see Configure sSMTP with Gmail on Ubuntu
and Configure sSMTP with Gmail on QNAP.
Features¶
- sends an email if command has any output (either
stdout
orstderr
or both) and/or exit code is non-zero - otherwise, the script only executes the command and has no side effect
- subject is truncated to 78 chars
- subject contains the current user to identify who's crontab was executed
- subject contains the date and time of the execution of the command
- this serves both a debugging purpose and prevents Gmail from not sending/receiving identical emails
How to use¶
In a crontab
file, simply pass the command to execute as argument (mind using quotes to avoid command be executed and/or part of it missing):
[CRONEXPRESSION] /home/foo/scripts/cronmail.sh "/home/foo/scripts/bar.sh"
send email only upon error output¶
Silence stdout with 1>/dev/null
, eg.:
[CRONEXPRESSION] /home/foo/scripts/cronmail.sh "/home/foo/scripts/bar.sh 1>/dev/null"
The script¶
#!/bin/bash
# -----------------------------------------------------------------------------
# Bash unofficial strict mode [source](http://redsymbol.net/articles/unofficial-bash-strict-mode/)
# -----------------------------------------------------------------------------
set -euo pipefail
IFS=$'\n\t'
# -----------------------------------------------------------------------------
# Functions
# -----------------------------------------------------------------------------
fn_terminate_script() {
if [ "${STDOUT:-}" != "" ]; then
rm -f "$STDOUT"
fi
}
fn_echo_email_header() {
# email subject:
# * includes the date and time to work around Gmail not sending/receiving emails that are exactly the same
# (maybe this applies only over some period of time)
# * will be truncated to 78 chars (RFC 2822 recommendation [source](https://stackoverflow.com/a/1592310))
local subject="[CRON][$USER][$CMD_DATE] ${CMD}"
echo "To: $MAILTO"
echo "Subject: ${subject:0:78}"
echo ""
echo "============ executed command ============"
echo "$CMD"
echo "============== return code ==============="
echo "$CMD_RETURN_CODE"
}
fn_echo_email_no_output() {
fn_echo_email_header
fn_echo_email_footer
}
fn_echo_email_with_output() {
fn_echo_email_header
echo "================ output =================="
cat "$STDOUT"
fn_echo_email_footer
}
fn_echo_email_footer() {
echo "=========================================="
}
fn_send_email() {
ssmtp
}
# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------
# constants
MAILTO="foo.bar.acme@gmail.com"
# the command to execute from CRON
CMD="$1"
CMD_DATE=$(date +"%Y-%m-%d-%H%M%S")
# create temporary file to collect stdout and stderr to
STDOUT="$(mktemp /tmp/cronme.out.XXXXXXXXXX)" || (>&2 echo "failed to create temp file" && exit 1)
# ensure no file is left behind even if script is interrupted/killed
trap 'fn_terminate_script' SIGINT
# evaluate the command, capturing both stdout and stderr to a file
# disable bash's "e" option to ensure this script keeps on running if evaluated command is killed/exits with non-zero code
set +e
eval "$CMD" 1>"$STDOUT" 2>&1
CMD_RETURN_CODE=$?
set -e
if [ -s "$STDOUT" ]; then
fn_echo_email_with_output | fn_send_email
elif [ $CMD_RETURN_CODE -ne 0 ]; then
fn_echo_email_no_output | fn_send_email
fi
# clean up
fn_terminate_script