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