3 # mail-wrapper -- Report results of a command by email
4 # Copyright (c)2017,2018,2023 Alexander Barton (alex@barton.de)
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
14 # Include "ax-common.sh":
16 for dir in "$HOME/lib" "$HOME/.ax" /usr/local /opt/ax /usr; do
17 [ -z "$ax_common_sourced" ] || break
18 ax_common="${dir}/lib/ax/ax-common.sh"
19 # shellcheck source=/usr/local/lib/ax/ax-common.sh
20 [ -r "$ax_common" ] && . "$ax_common"
22 if [ -z "$ax_common_sourced" ]; then
23 echo "Error ($NAME): \"ax-common.sh\" not found, aborting!" >&2
24 echo "Please install 'ax-unix', \"Alex' UNIX Tools & Scripts\", and try again."
27 unset dir ax_common ax_common_sourced
33 echo " $NAME [--help|--usage]"
34 echo " $NAME {parameters} [<command> [<arg> [<…>]]]"
36 echo " -C Use the \"C\" locale, no localized (error) messages."
37 echo " --dontfail|-n Don't return the error code of the command called."
38 echo " --errors|-e Generate email on errors only."
39 echo " --from|-f Email address of the sender of the email."
40 echo " --stderr-is-warning|-W Exit code indicates error; stderr is only warning."
41 echo " --subject|-s <subject> Subject for the email."
42 echo " --time|-t Include timing information."
43 echo " --to|-t <address> Email address to send the email to."
45 echo "When no <command> is given, $NAME reads from standard input. The default for"
46 echo "<from> and <to> for the current user is \"$to\"."
53 ax_error -l "Syntax error!"
58 if [[ -z "$proc_fd_works" ]]; then
59 ax_debug "Cleaning temporary files ..."
60 [[ -n "$buffer_file" ]] && rm -f "$buffer_file"
61 [[ -n "$error_file" ]] && rm -f "$error_file"
65 # Convert UNIX time stamp to date text.
69 if ! date -d @"$t" "$@" 2>/dev/null; then
70 # "date -d @<time_t>" (GNU variant) failed!
75 time_t_to_duration() {
77 if [[ "$s" -ge 60 ]]; then
80 if [[ "$m" -ge 60 ]]; then
83 if [[ "$h" -ge 24 ]]; then
86 echo "${d}d${2}${h}h${2}${m}m${2}${s}s"
88 echo "${h}h${2}${m}m${2}${s}s"
106 # Initialize internal state.
110 host=$(hostname -f 2>/dev/null || hostname)
114 # Some defaults (can be adjusted by command line parameters).
117 unset stderr_is_warning
120 from="${LOGNAME:-root} <${LOGNAME:-root}@$host>"
123 # Parse the command line ...
124 while [[ $# -gt 0 ]]; do
141 [[ $# -gt 0 ]] || syntax_error
149 [[ $# -gt 0 ]] || syntax_error
152 "--stderr-is-warning"|"-W")
159 # Ignore this switch for compatibility with an other
160 # "mail-wrapper" script. This is the default anyway!
164 [[ $# -gt 0 ]] || syntax_error
171 # Command to execute follows in command line.
178 # Initialize the "buffer file" on file handle #3. This file will store all
179 # output, stdout and stderr combined. The file is immediately unlinked so that
180 # we can't leak stale files. Afterwards this script accesses the "buffer file"
181 # by its file descriptor only.
182 buffer_file=$(mktemp) \
183 || ax_abort -l "Failed to create buffer file: \"$buffer_file\"!"
184 ax_debug "buffer_file=\"$buffer_file\""
185 exec 3>"$buffer_file" \
186 || ax_abort -l "Failed to redirect FD #3 to buffer file!"
187 if [[ -n "$proc_fd_works" ]]; then
189 || ax_error -l "Failed to delete buffer file: \"$buffer_file\"!"
190 buffer_file="/dev/fd/3"
193 if [[ $# -gt 0 ]]; then
194 # Execute command and save output in buffer file.
195 # Use a sub-shell to not pollute our name space!
196 error_file=$(mktemp) \
197 || ax_abort -l "Failed to create error buffer file: \"$error_file\"!"
198 ax_debug "error_file=\"$error_file\""
199 exec 4>"$error_file" \
200 || ax_abort -l "Failed to redirect FD #4 to error file!"
201 if [[ -n "$proc_fd_works" ]]; then
203 || ax_error -l "Failed to delete error buffer file: \"$error_file\"!"
204 error_file="/dev/fd/4"
209 ax_debug "Running command \"$*\" ..."
210 start_t=$EPOCHSECONDS
212 "$@" 2>&1 1>&3 | tee "$error_file" >&3
213 echo "${PIPESTATUS[0]}"
217 # Read from stdin and save it to the buffer file.
218 error_file="/dev/null"
221 ax_debug "Reading from stdin ..."
222 start_t=$EPOCHSECONDS
223 while read -r line; do
225 || ax_abort -l "Failed to write to buffer file!"
231 ax_debug "exit_code=$exit_code"
233 declare -i count_all count_err
234 count_all=$(wc -l <"$buffer_file" || ax_abort -l "Failed to count buffer file!")
235 count_err=$(wc -l <"$error_file" || ax_abort -l "Failed to count error file!")
237 # Error or no error -- that's the question! An error is assumed when either the
238 # exit code of the command was non-zero or there was output to stderr.
239 # But when stderr_is_warning is set, messages on stderr result on a warning only!
240 if [[ $exit_code -ne 0 ]]; then
242 elif [[ $count_err -gt 0 ]]; then
243 [[ -n $stderr_is_warning ]] && error_level=1 || error_level=2
248 # Construct email subject ...
249 [[ -z "$subject" ]] && subject="$host: $job report"
250 if [[ "$error_level" -eq 0 ]]; then
251 subject="$subject - success"
252 elif [[ "$error_level" -eq 1 ]]; then
253 subject="$subject - WARNING!"
255 subject="$subject - ERROR!"
258 ax_debug "from=\"$from\""
259 ax_debug "to=\"$to\""
260 ax_debug "subject=$subject"
262 if [[ -n "$DEBUG" ]]; then
263 echo "--- stdout+stderr ---"
265 echo "--- stderr ---"
270 ax_debug "count_all=$count_all"
271 ax_debug "count_err=$count_err"
272 ax_debug "error_level=$error_level"
274 # No errors detected (exit code & stderr), and email should be sent on errors
275 # only: so exit early!
276 [[ "$error_level" -lt 2 && -n "$do_errors_only" ]] && exit $exit_code
278 # No error detected and no output at all: skip email, exit early:
279 [[ "$error_level" -eq 0 && $count_all -eq 0 ]] && exit $exit_code
281 # Build the report mail.
282 # Make sure to ignore all mail(1) configuration files, system wide /etc/mailrc
283 # (by using the "-n" option) as well as ~/.mailrc (by setting the MAILRC
284 # environment variable).
285 export MAILRC=/dev/null
289 echo " - Host: $host"
290 echo " - User: $(id -un)"
291 echo " - Exit code: $exit_code"
292 [[ -n "$time" ]] && printf " - Duration: %s\n" "$(time_t_to_duration $((end_t - start_t)) ' ')"
294 if [[ $# -gt 0 ]]; then
295 # A command name is known (not stdin), show it!
300 [[ -n "$time" ]] && printf "%s - %s:\n\n" "$(time_t_to_date "$start_t")" "$(time_t_to_date "$end_t")"
301 if [[ $count_err -gt 0 ]]; then
302 # Prefix mail with all error messages.
303 echo "Error summary:"
304 echo "-----------------------------------------------------------------------------"
306 || ax_abort -l "Failed to dump error file!"
307 echo "-----------------------------------------------------------------------------"
310 if [[ $count_all -ne $count_err ]]; then
311 # Show full output when different to "error output" only.
313 || ax_abort -l "Failed to dump buffer file!"
315 ) | mail -n -a "From: $from" -s "$subject" "$to" \
316 || ax_abort -l "Failed to send email to \"$to\"!"
318 [[ -n "$dont_fail" ]] && exit 0 || exit $exit_code