]> arthur.barton.de Git - ax-unix.git/blob - mail/wrapper/mail-wrapper
1a7fd106f93dc880be954dd5c5503bafca348507
[ax-unix.git] / mail / wrapper / mail-wrapper
1 #!/usr/bin/env bash
2 #
3 # mail-wrapper -- Report results of a command by email
4 # Copyright (c)2017,2018,2023 Alexander Barton (alex@barton.de)
5 #
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.
10 #
11
12 NAME=$(basename "$0")
13
14 # Include "ax-common.sh":
15 ax_common_sourced=
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"
21 done
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."
25         exit 99
26 fi
27 unset dir ax_common ax_common_sourced
28
29 usage() {
30         {
31                 echo
32                 echo "Usage:"
33                 echo "  $NAME [--help|--usage]"
34                 echo "  $NAME {parameters} [<command> [<arg> [<…>]]]"
35                 echo
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 "  --to|-t <address>       Email address to send the email to."
43                 echo
44                 echo "When no <command> is given, $NAME reads from standard input. The default for"
45                 echo "<from> and <to> for the current user is \"$to\"."
46                 echo
47         } >&2
48         exit "${1:-0}"
49 }
50
51 syntax_error() {
52         ax_error -l "Syntax error!"
53         usage 2
54 }
55
56 clean_up() {
57         if [[ -z "$proc_fd_works" ]]; then
58                 ax_debug "Cleaning temporary files ..."
59                 [[ -n "$buffer_file" ]] && rm -f "$buffer_file"
60                 [[ -n "$error_file" ]] && rm -f "$error_file"
61         fi
62 }
63
64 case "$(uname)" in
65         "Darwin")
66                 unset proc_fd_works
67                 ;;
68         *)
69                 proc_fd_works=1
70 esac
71
72 # Initialize internal state.
73 buffer_file=""
74 error_file=""
75 error_level=0
76 host=$(hostname -f 2>/dev/null || hostname)
77
78 trap clean_up EXIT
79
80 # Some defaults (can be adjusted by command line parameters).
81 unset do_errors_only
82 unset dont_fail
83 unset stderr_is_warning
84 unset subject
85 from="${LOGNAME:-root} <${LOGNAME:-root}@$host>"
86 to="$from"
87
88 # Parse the command line ...
89 while [[ $# -gt 0 ]]; do
90         case "$1" in
91                 "-C")
92                         unset LANG
93                         export LC_ALL="C"
94                         ;;
95                 "--debug"|"-D")
96                         export DEBUG=1
97                         ;;
98                 "--dontfail"|"-n")
99                         dont_fail=1
100                         ;;
101                 "--errors"|"-e")
102                         do_errors_only=1
103                         ;;
104                 "--from"|"-f")
105                         shift
106                         [[ $# -gt 0 ]] || syntax_error
107                         from="$1"
108                         ;;
109                 "--help"|"--usage")
110                         usage
111                         ;;
112                 "--subject"|"-s")
113                         shift
114                         [[ $# -gt 0 ]] || syntax_error
115                         subject="$1"
116                         ;;
117                 "--stderr-is-warning"|"-W")
118                         stderr_is_warning=1
119                         ;;
120                 "--suppress-empty")
121                         # Ignore this switch for compatibility with an other
122                         # "mail-wrapper" script. This is the default anyway!
123                         ;;
124                 "--to"|"-t")
125                         shift
126                         [[ $# -gt 0 ]] || syntax_error
127                         to="$1"
128                         ;;
129                 "-"*)
130                         syntax_error
131                         ;;
132                 *)
133                         # Command to execute follows in command line.
134                         break
135                         ;;
136         esac
137         shift
138 done
139
140 # Initialize the "buffer file" on file handle #3. This file will store all
141 # output, stdout and stderr combined. The file is immediately unlinked so that
142 # we can't leak stale files. Afterwards this script accesses the "buffer file"
143 # by its file descriptor only.
144 buffer_file=$(mktemp) \
145         || ax_abort -l "Failed to create buffer file: \"$buffer_file\"!"
146 ax_debug "buffer_file=\"$buffer_file\""
147 exec 3>"$buffer_file" \
148         || ax_abort -l "Failed to redirect FD #3 to buffer file!"
149 if [[ -n "$proc_fd_works" ]]; then
150         rm "$buffer_file" \
151                 || ax_error -l "Failed to delete buffer file: \"$buffer_file\"!"
152         buffer_file="/dev/fd/3"
153 fi
154
155 if [[ $# -gt 0 ]]; then
156         # Execute command and save output in buffer file.
157         # Use a sub-shell to not pollute our name space!
158         error_file=$(mktemp) \
159                 || ax_abort -l "Failed to create error buffer file: \"$error_file\"!"
160         ax_debug "error_file=\"$error_file\""
161         exec 4>"$error_file" \
162                 || ax_abort -l "Failed to redirect FD #4 to error file!"
163         if [[ -n "$proc_fd_works" ]]; then
164                 rm "$error_file" \
165                         || ax_error -l "Failed to delete error buffer file: \"$error_file\"!"
166                 error_file="/dev/fd/4"
167         fi
168
169         job=$(basename "$1")
170
171         ax_debug "Running command \"$*\" ..."
172         exit_code=$(
173                 "$@" 2>&1 1>&3 | tee "$error_file" >&3
174                 echo "${PIPESTATUS[0]}"
175         )
176 else
177         # Read from stdin and save it to the buffer file.
178         error_file="/dev/null"
179         job="Job"
180
181         ax_debug "Reading from stdin ..."
182         while read -r line; do
183                 echo "$line" >&3 \
184                         || ax_abort -l "Failed to write to buffer file!"
185         done
186         exit_code=0
187 fi
188
189 ax_debug "exit_code=$exit_code"
190
191 declare -i count_all count_err
192 count_all=$(wc -l <"$buffer_file" || ax_abort -l "Failed to count buffer file!")
193 count_err=$(wc -l <"$error_file" || ax_abort -l "Failed to count error file!")
194
195 # Error or no error -- that's the question! An error is assumed when either the
196 # exit code of the command was non-zero or there was output to stderr.
197 # But when stderr_is_warning is set, messages on stderr result on a warning only!
198 if [[ $exit_code -ne 0 ]]; then
199         error_level=2
200 elif [[ $count_err -gt 0 ]]; then
201         [[ -n $stderr_is_warning ]] && error_level=1 || error_level=2
202 else
203         error_level=0
204 fi
205
206 # Construct email subject ...
207 [[ -z "$subject" ]] && subject="$host: $job report"
208 if [[ "$error_level" -eq 0 ]]; then
209         subject="$subject - success"
210 elif [[ "$error_level" -eq 1 ]]; then
211         subject="$subject - WARNING!"
212 else
213         subject="$subject - ERROR!"
214 fi
215
216 ax_debug "from=\"$from\""
217 ax_debug "to=\"$to\""
218 ax_debug "subject=$subject"
219
220 if [[ -n "$DEBUG" ]]; then
221         echo "--- stdout+stderr ---"
222         cat "$buffer_file"
223         echo "--- stderr ---"
224         cat "$error_file"
225         echo "---"
226 fi
227
228 ax_debug "count_all=$count_all"
229 ax_debug "count_err=$count_err"
230 ax_debug "error_level=$error_level"
231
232 # No errors detected (exit code & stderr), and email should be sent on errors
233 # only: so exit early!
234 [[ "$error_level" -lt 2 && -n "$do_errors_only" ]] && exit $exit_code
235
236 # No error detected and no output at all: skip email, exit early:
237 [[ "$error_level" -eq 0 && $count_all -eq 0 ]] && exit $exit_code
238
239 # Build the report mail.
240 # Make sure to ignore all mail(1) configuration files, system wide /etc/mailrc
241 # (by using the "-n" option) as well as ~/.mailrc (by setting the MAILRC
242 # environment variable).
243 export MAILRC=/dev/null
244 (
245         echo "$job report:"
246         echo
247         echo " - Host: $host"
248         echo " - User: $(id -un)"
249         echo " - Exit code: $exit_code"
250         echo
251         if [[ $# -gt 0 ]]; then
252                 # A command name is known (not stdin), show it!
253                 echo "Command:"
254                 echo "$@"
255                 echo
256         fi
257         if [[ $count_err -gt 0 ]]; then
258                 # Prefix mail with all error messages.
259                 echo "Error summary:"
260                 echo "-----------------------------------------------------------------------------"
261                 cat "$error_file" \
262                         || ax_abort -l "Failed to dump error file!"
263                 echo "-----------------------------------------------------------------------------"
264                 echo
265         fi
266         if [[ $count_all -ne $count_err ]]; then
267                 # Show full output when different to "error output" only.
268                 cat "$buffer_file" \
269                         || ax_abort -l "Failed to dump buffer file!"
270         fi
271 ) | mail -n -a "From: $from" -s "$subject" "$to" \
272         || ax_abort -l "Failed to send email to \"$to\"!"
273
274 [[ -n "$dont_fail" ]] && exit 0 || exit $exit_code