#!/usr/bin/perl -w
use strict;

# $Id: mail-output,v 1.18 2013-05-08 12:55:46-04 roderick Exp $
#
# Roderick Schertler <roderick@argon.org>

# Copyright (C) 2001 Roderick Schertler
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# For a copy of the GNU General Public License write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

use sigtrap qw(die untrapped normal-signals); # process END in these cases

use Proc::SyncExec	qw(fork_retry);
use Proc::WaitStat	qw(waitstat waitstat_die);
use RS::Handy		qw($Me getopt have_prog safe_tmp rfc822_dt xdie);
use String::ShellQuote	qw(shell_quote);

my @Sendmail	= qw(sendmail /usr/sbin/sendmail /usr/lib/sendmail);

my $Debug	= 0;
my $Empty	= 0;
my $Exit	= 0;
my $Failure_only = 0;
my $Out_file	= undef;
my $Pid		= $$;
my @Recip	= ();
my $Sendmail	= undef;
my $Subject	= undef;
my $Version	= q$Revision: 1.18 $ =~ /(\d\S+)/ ? $1 : '?';

my @Option_spec = (
    'debug!'		=> \$Debug,
    'empty|e'		=> \$Empty,
    'failure-only|f'	=> \$Failure_only,
    'help!'		=> sub { usage() },
    'recip|r=s@'	=> \@Recip,
    'sendmail=s'	=> \$Sendmail,
    'subject|s=s'	=> \$Subject,
    'version'		=> sub { print "$Me version $Version\n"; exit },
);

my $Usage = <<EOF;
usage: $Me [switch]... command [arg]...
switches:
     --debug		turn debugging on
 -e, --empty		send mail even if the command doesn't output anything
 -f, --failure-only	only send the mail if the command exits non-zero
     --help		show this and then die
 -r, --recip <addr>	set recipient (default you), can be used multiple times
     --sendmail <prog>	use <prog> to send the mail
 -s, --subject <subj>	set message subject
     --version		show the version ($Version) and exit
Use \`perldoc $Me\' to see the full documenation.
EOF

sub xwarn {
    RS::Handy::xwarn @_;
    $Exit ||= 1;
}

sub debug {
    print STDERR "debug: ", @_, "\n" if $Debug;
}

sub usage {
    xwarn @_ if @_;
    # Use exit() rather than die(), as Getopt::Long does eval().
    print STDERR $Usage;
    exit 1;
}

sub init {
    getopt -bundle, @Option_spec or usage if @ARGV;

    push @Recip, $ENV{EMAIL} || $ENV{LOGNAME} || $ENV{USER}
		    || getlogin || getpwuid($<)
		    || xdie "can't figure out your login name\n"
	unless @Recip;
    debug "recip = [", join("] [", @Recip), "]";

    if (defined $Sendmail) {
	have_prog $Sendmail
	    or xdie "--sendmail program $Sendmail doesn't exist\n";
    }
    else {
	$Sendmail = have_prog @Sendmail;
	if (!defined $Sendmail) {
	    local $" = ', ';
	    xdie "can't find sendmail program (tried @Sendmail),",
	    	    " specify it with --sendmail\n";
	}
    }
    debug "sendmail = $Sendmail";

    $SIG{HUP} = 'IGNORE';
    if (-t STDIN) {
	open STDIN, "/dev/null" or xdie "can't open /dev/null for stdin:";
    }
}

sub main {
    init;
    @ARGV or usage;
    my @cmd = @ARGV;
    my $cmd_name = $cmd[0];

    ($Out_file, my $out_fh) = safe_tmp;

    select $out_fh;	$| = 1;
    select STDOUT;	$| = 1;

    my $recip = join ", ", @Recip;
    $recip =~ s/\n(^[\040\t])/\n\t$1/g;
    $Subject = "$Me: " . shell_quote @cmd if !defined $Subject;
    $Subject =~ s/\n.*//s;
    print $out_fh
	    "To: $recip\n",
	    "Subject: $Subject\n",
    	    "X-Started-At: ", rfc822_dt, "\n",
	    "\n"
	or xdie "error writing to $Out_file:";
    my $body_pos = tell $out_fh;
    debug "body_pos = $body_pos";

    my $do_send = undef;
    my $pid = fork_retry;
    if (!$pid) {
	my $fd = fileno $out_fh;
	open STDOUT, ">&=$fd"	or xdie "child open of STDOUT:";
	open STDERR, ">&STDOUT"	or xdie "child open of STDERR:";
	$^W = 0; # I'll check my own errors, thankyouverymuch.
	exec @cmd		or xdie "can't exec $cmd_name:";
    }
    if (waitpid($pid, 0) == -1) {
	xwarn "waitpid returned -1?! ($!)\n";
    }
    elsif ($? != 0) {
	debug "sending enabled due to failure";
    	$do_send = 1;
	print $out_fh "$Me: non-zero exit (", waitstat($?),
		") from $cmd_name\n"
	    or xwarn "error writing to $Out_file:";
    }

    seek $out_fh, 0, 2 or xdie "error seeking to EOF of $Out_file:";
    my $final_pos = tell $out_fh;
    debug "final_pos = $final_pos";

    if ($final_pos == $body_pos && $Empty) {
    	debug "no output";
	print $out_fh "$Me: no output generated by $cmd_name\n"
	    or xwarn "error writing to $Out_file:";
	$final_pos = tell $out_fh;
    }

    if ($final_pos != $body_pos && !$Failure_only) {
	debug "sending enabled due to non-empty body";
    	$do_send = 1;
    }

    if ($Debug) {
	debug "message:";
	seek $out_fh, 0, 0 or xwarn "error seeking in $Out_file:";
	print while <$out_fh>;
    }

    if (!$do_send) {
    	debug "not sending message";
    }
    else {
    	debug "sending message";
    	# This has to be a sysseek() rather than a seek() since it needs
    	# to be done at the kernel level before I fork()/exec() the
    	# $Sendmail program.
	sysseek $out_fh, 0, 0 or xdie "error seeking in $Out_file:";
    	open STDIN, "<&" . fileno $out_fh
    	    or xdie "can't dup $Out_file to stdin:";
    	system $Sendmail, @Recip;
	waitstat_die $?, $Sendmail;
    }

    return 0;
}

END {
    if ($$ == $Pid && defined $Out_file && !unlink $Out_file) {
	xwarn "error unlinking $Out_file:";
	$? ||= 1;
    }
}

$Exit = eval { main } || $Exit;
die $@ if $@;
exit $Exit;

__END__

=head1 NAME

mail-output - run a command and mail back the output

=head1 SYNOPSIS

B<mail-output>
[B<--debug>]
[B<--empty>|B<-e>]
[B<--failure-only>|B<-f>]
[B<--help>]
[B<--recip>|B<-r> I<addr>]...
[B<--sendmail> I<prog>]
[B<--subject>|B<-s> I<subj>]
[B<--version>]
B<command> [I<arg>]...

=head1 DESCRIPTION

I<mail-output> runs the given I<command> and mails you any output it
generates.  (If there isn't any output no mail is sent.)  If the command
exits with a non-zero status mail is always generated.

I<mail-output> ignores hangup (C<HUP>) signals, so the program it
runs should do the same.  If stdin is a terminal, it's re-opened on
F</dev/null> instead.

I used to hold the output in an unlinked temporary file, but I switched to
using a named one so you can look at the it before the command finishes.
The file's location and name can vary, but usually it'll be called something
like F<mail-output.*> in C<$TMPDIR> or F</tmp>.

=head1 OPTIONS

=over 4

=item B<--debug>

Turn debugging on.

=item B<--empty>

=item B<-e>

Send mail even if the command doesn't output anything and returns 0.

=item B<--failure-only>

=item B<-f>

Don't send the message unless the command exits with a non-zero status.

=item B<--help>

Show the usage message and die.

=item B<--recip> I<address>

=item B<-r> I<address>

Send the mail to I<address>.  Multiple instances of this switch can be
used to send the mail to more than one person, but only put one I<address>
for each instance of B<--recip>.  If you don't use this switch the mail
will be sent back to you.  If you do use this switch then the mail will
only go to the I<address>s you specify.

=item B<--sendmail> I<prog>

Use I<prog> to send the mail.  This program should take the message on
standard input and the recipients as arguments.  If you don't give this
switch B<mail-output> searches for F<sendmail> on your $PATH and in the
usual locations.

=item B<--subject> I<subj>

=item B<-s> I<subj>

Set the subject used for the mail.  The default subject includes the
name of the command run.

=item B<--version>

Show the version number and exit.

=head1 AUTHOR

Roderick Schertler <roderick@argon.org>

=cut
