Blob Blame History Raw
# Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
#
# 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.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94085, or: http://www.zmanda.com

package Amanda::Interactivity::email;

use strict;
use warnings;
use POSIX qw( :errno_h );
use vars qw( @ISA );
use IPC::Open3;
use File::stat;
use Time::localtime;
@ISA = qw( Amanda::Interactivity );

use Amanda::Paths;
use Amanda::Util;
use Amanda::Debug qw( debug );
use Amanda::Config qw( :getconf );
use Amanda::Changer;
use Amanda::MainLoop qw( :GIOCondition );

=head1 NAME

Amanda::Interactivity::email -- Interactivity class to send user request by email

=head1 SYNOPSIS

Amanda::Interactivity class to write user request by email

=cut

sub new {
    my $class = shift;
    my $properties = shift;

    my $self = {
	send_email_src => undef,
	check_file_src => undef,
	properties     => $properties,
    };

    if (defined $self->{'properties'}->{'check-file'}) {
	my $check_file = $self->{'properties'}->{'check-file'}->{'values'}->[0];
    }

    return bless ($self, $class);
}

sub abort {
    my $self = shift;

    if ($self->{'send_email_src'}) {
	$self->{'send_email_src'}->remove();
    }
    if ($self->{'check_file_src'}) {
	$self->{'check_file_src'}->remove();
    }
}

sub user_request {
    my $self = shift;
    my %params = @_;
    my $buffer = "";

    my $message    = $params{'message'};
    my $label      = $params{'label'};
    my $new_volume = $params{'new_volume'};
    my $err        = $params{'err'};
    my $chg_name   = $params{'chg_name'};

    my $resend_delay;
    if (defined $self->{'properties'}->{'resend-delay'}) {
	$resend_delay = 1000 * $self->{'properties'}->{'resend-delay'}->{'values'}->[0];
    }
    my $check_file;
    if (defined $self->{'properties'}->{'check-file'}) {
	$check_file = $self->{'properties'}->{'check-file'}->{'values'}->[0];
    }

    my $check_file_delay = 10000;
    if (defined $self->{'properties'}->{'check-file-delay'}) {
	$check_file_delay = 1000 * $self->{'properties'}->{'check_file-delay'}->{'values'}->[0];
    }

    my $mailer  = getconf($CNF_MAILER);
    my $subject;
    if ($chg_name) {
	$subject = "AMANDA VOLUME REQUEST ($chg_name):";
    } else {
	$subject = "AMANDA VOLUME REQUEST:";
    }
    if ($label) {
	$subject .= " $label";
    } else {
	$subject .= " new volume";
    }

    my $mailto;
    if (defined $self->{'properties'}->{'mailto'}) {
	$mailto = $self->{'properties'}->{'mailto'}->{'values'};
    } else {
	my $a = getconf($CNF_MAILTO);
	my @mailto = split (/ /, getconf($CNF_MAILTO));
	$mailto = \@mailto;
    }
    my @cmd = ("$mailer", "-s", $subject, @{$mailto});

    my $send_email_cb;
    $send_email_cb = sub {
	$self->{'send_email_src'}->remove() if defined $self->{'send_email_src'};
	$self->{'send_email_src'} = undef;
	debug("cmd: " . join(" ", @cmd) . "\n");
	my ($pid, $fh);
	$pid = open3($fh, ">&2", ">&2", @cmd);
	print {$fh} "$err\n";
	if ($label && $new_volume) {
	    print {$fh} "Insert volume labeled '$label' or a new volume in $chg_name\n";
	} elsif ($label) {
	    print {$fh} "Insert volume labeled '$label' in $chg_name\n";
	} else {
	    print {$fh} "Insert a new volume in $chg_name\n";
	}
	if ($check_file) {
	    print {$fh} "or write the name of a new changer in '$check_file'\n";
	    print {$fh} "or write 'abort' in the file to abort the scan.\n";

	    # check_file_cb actually monitors the file's ctime and mtime
	    # timestamps separately, but it seems clearer for the email
	    # just to report the latest of the two to the user -- in
	    # practice the single date should be enough for the user to
	    # understand what Amanda is waiting for....
	    my $modtime = $self->{'check_file_mtime'};
	    if ( $self->{'check_file_ctime'} > $modtime ) {
		$modtime = $self->{'check_file_ctime'};
	    }
	    if ( $modtime > 0 ) {
		print {$fh} "\n(Waiting for the file to be modified after:\n" .
			ctime($modtime) . ".)\n";
	    } else {
		print {$fh} "\n(Waiting for the file to be created.)\n";
	    }

	    # if check_file_cb detected any warning message, include it
	    # here.
	    if ($self->{'check_file_message'}) {
		print {$fh} $self->{'check_file_message'}
	    }
	}
	close $fh;

	if ($resend_delay) {
	    $self->{'send_email_src'} = Amanda::MainLoop::call_after($resend_delay, $send_email_cb);
	}
    };

    my $check_file_cb;
    $check_file_cb = sub {
	$self->{'check_file_src'}->remove() if $self->{'check_file_src'};
	$self->{'check_file_src'} = undef;

	if (-e $check_file) {
	    my $check_file_mtime = (stat($check_file))->mtime;
	    my $check_file_ctime = (stat($check_file))->ctime;
	    if ($self->{'check_file_mtime'} < $check_file_mtime or
		$self->{'check_file_ctime'} < $check_file_ctime) {

		# The user has modified the file, so we stop
		# the email callback.  If we detect a problem with the
		# file below, the email callback is restarted; otherwise
		# this Interactivity call returns.  (The caller may
		# immediately invoke a new Interactivity call
		# afterwards, if the user hasn't aborted and the caller
		# still hasn't found the desired volume.... in which
		# case the new call will start its own email callback.)
		$self->{'send_email_src'}->remove() if $self->{'send_email_src'};
		$self->{'send_email_src'} = undef;

		# (save updated values, in case we don't return below.)
		$self->{'check_file_ctime'} = $check_file_ctime;
		$self->{'check_file_mtime'} = $check_file_mtime;
		$self->{'check_file_message'} = undef;

		if (!-f $check_file) {
		    $self->{'check_file_message'} = "\nThe check-file '$check_file' is not a flat file.\n";
		} elsif (!-r $check_file) {
		    $self->{'check_file_message'} = "\nThe check-file '$check_file' is not readable.\n";
		}
		if ($self->{'check_file_message'}) {
		    $send_email_cb->();
		} else {
		    my $fh;
		    open ($fh, '<' , $check_file);
		    my $line = <$fh>;
		    close($fh);
		    $send_email_cb = undef;
		    $check_file_cb = undef;
		    if ($line) {
			chomp $line;
			$self->abort();
			if ($line =~ /^abort$/i) {
			    return $params{'request_cb'}->(
				Amanda::Changer::Error->new('fatal',
					message => "Aborted by user"));
			} else {
			    return $params{'request_cb'}->(undef, $line);
			}
		    } else {
			return $params{'request_cb'}->(undef, '');
		    }
		}
	    }
	} else {
	    # the file doesn't currently exist
	    $self->{'check_file_mtime'} = 0;
	    $self->{'check_file_ctime'} = 0;
	    $self->{'check_file_message'} = undef
	}
	$self->{'check_file_src'} = Amanda::MainLoop::call_after($check_file_delay, $check_file_cb);
    };

    if ($check_file) {
	# save the initial timestamps of the file, so we can detect
	# when the user updates it later.
	if (-e $check_file) {
	    $self->{'check_file_mtime'} = (stat($check_file))->mtime;
	    $self->{'check_file_ctime'} = (stat($check_file))->ctime;
	    $self->{'check_file_message'} = undef
	} else {
	    $self->{'check_file_mtime'} = 0;
	    $self->{'check_file_ctime'} = 0;
	    $self->{'check_file_message'} = undef
        }
    }

    $send_email_cb->();
    if ($check_file) {
	$check_file_cb->();
    }
}

1;