#!/usr/bin/perl
#============================================================= -*-perl-*-
#
# BackupPC_sendEmail: send status emails to users and admins
#
# DESCRIPTION
#
#   BackupPC_sendEmail: send status emails to users and admins.
#   BackupPC_sendEmail is run by BackupPC_nightly, so it runs
#   once every night.
#
# AUTHOR
#   Craig Barratt  <cbarratt@users.sourceforge.net>
#
# COPYRIGHT
#   Copyright (C) 2001-2020  Craig Barratt
#
#   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 3 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, see <http://www.gnu.org/licenses/>.
#
#========================================================================
#
# Version 4.4.0, released 20 Jun 2020.
#
# See http://backuppc.sourceforge.net.
#
#========================================================================

use strict;
no utf8;

use lib "/usr/share/backuppc/lib";
use BackupPC::Lib;
use BackupPC::XS;
use BackupPC::DirOps;
use Encode;

use Data::Dumper;
use Getopt::Std;
use vars qw($Lang $TopDir $BinDir $LogDir %Conf %HostConf $Hosts);

die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
$TopDir = $bpc->TopDir();
$LogDir = $bpc->LogDir();
$BinDir = $bpc->BinDir();
%Conf   = $bpc->Conf();
$Lang   = $bpc->Lang();
$Hosts  = $bpc->HostInfoRead();

$bpc->ChildInit();

use vars qw(%UserEmailInfo);
do "$LogDir/UserEmailInfo.pl";

my %opts;
if ( !getopts("ctu:", \%opts) || @ARGV != 0 ) {
    print <<EOF;
usage: $0 [-t] [-c] [-u userEmail]
options:

  -t  display the emails that would be sent, without sending them

  -c  check if BackupPC is alive and send an email if not

  -u  send a test email to userEmail
EOF
    exit(1);
}

#
# Upgrade legacy version of %UserEmailInfo
#
# Prior to 3.2.0, it was a hash with entries:
#
#    $UserEmailInfo{$user}{lastTime}
#    $UserEmailInfo{$user}{lastSubj}
#    $UserEmailInfo{$user}{lastHost}
#
# However, if a user had multiple hosts, then an email about one
# host prevents mail delivery about other hosts.  Starting in 3.2.0
# the hash is:
#
#    $UserEmailInfo{$user}{$host}{lastTime}
#    $UserEmailInfo{$user}{$host}{lastSubj}
#
my $oldFormat = 0;
foreach my $user ( keys(%UserEmailInfo) ) {
    if ( defined($UserEmailInfo{$user}{lastTime}) && ref($UserEmailInfo{$user}{lastTime}) ne 'HASH' ) {
        $oldFormat = 1;
        last;
    }
}
if ( $oldFormat ) {
    #
    # Convert to the new format
    #
    my %UserEmailInfoOld = %UserEmailInfo;
    %UserEmailInfo = ();
    foreach my $user ( keys(%UserEmailInfoOld) ) {
        next if ( $user eq "" );
        my $host = $UserEmailInfoOld{$user}{lastHost};
        next if ( !defined($host) );
        $UserEmailInfo{$user}{$host}{lastTime} = $UserEmailInfoOld{$user}{lastTime};
        $UserEmailInfo{$user}{$host}{lastSubj} = $UserEmailInfoOld{$user}{lastSubj};
    }
}

#
# Prune hosts that no longer exist
#
foreach my $user ( keys(%UserEmailInfo) ) {
    foreach my $host ( keys(%{$UserEmailInfo{$user}}) ) {
        next if ( defined($Hosts->{$host}) );
        delete($UserEmailInfo{$user}{$host});
    }
    next if ( $UserEmailInfo{$user} );
    delete($UserEmailInfo{$user});
}

my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
if ( $err ) {
    if ( $opts{c} && $Conf{EMailAdminUserName} ne "" ) {
        my $headers = $Conf{EMailHeaders};
        $headers .= "\n" if ( $headers !~ /\n$/ );
        my $subject =
            $Conf{EMailAdminSubject} ne ""
          ? $Conf{EMailAdminSubject}
          : "BackupPC: can't connect to server";
        my $mesg = <<EOF;
To: $Conf{EMailAdminUserName}
Subject: $subject
$headers
Error: cannot connect to BackupPC server.

Regards,
PC Backup Genie
EOF
        SendMail($mesg);
        exit(1);
    }
    print("Can't connect to server ($err)\n");
    exit(1);
}
exit(0) if ( $opts{c} );
my $reply = $bpc->ServerMesg("status hosts info");
$reply = $1 if ( $reply =~ /(.*)/s );
my(%Status, %Info, %Jobs, @BgQueue, @UserQueue, @CmdQueue);
eval($reply);

###########################################################################
# Generate test message if required
###########################################################################
if ( $opts{u} ne "" ) {
    my $headers = $Conf{EMailHeaders};
    $headers .= "\n" if ( $headers !~ /\n$/ );
    my $subject =
        $Conf{EMailAdminSubject} ne ""
      ? $Conf{EMailAdminSubject}
      : "BackupPC test email";
    my $mesg = <<EOF;
To: $opts{u}
Subject: $subject
$headers
This is a test message from $0.

Regards,
PC Backup Genie
EOF
    SendMail($mesg);
    exit(0);
}

###########################################################################
# Generate per-host warning messages sent to each user
###########################################################################
my @AdminBadHostsEmail = ();
my @AdminBadHosts      = ();

foreach my $host ( sort(keys(%Status)) ) {
    #
    # read any per-PC config settings (allowing per-PC email settings)
    #
    $bpc->ConfigRead($host);
    %HostConf = $bpc->Conf();
    my $user = $Hosts->{$host}{user};

    #
    # Accumulate host errors for the admin email below
    #
    if (   ($Status{$host}{reason} eq "Reason_backup_failed" || $Status{$host}{reason} eq "Reason_restore_failed")
        && $Status{$host}{error} !~ /^lost network connection to host/
        && !$HostConf{BackupsDisable} ) {
        push(@AdminBadHostsEmail, $HostConf{EMailAdminUserName});
        push(@AdminBadHosts,      "$host ($Status{$host}{error})");
    }

    next
      if ( time - $UserEmailInfo{$user}{$host}{lastTime} < $HostConf{EMailNotifyMinDays} * 24 * 3600
        || $HostConf{XferMethod} eq "archive"
        || $HostConf{BackupsDisable}
        || $Hosts->{$host}{user} eq ""
        || $user eq "" );

    my @Backups         = $bpc->BackupInfoRead($host);
    my $last            = 0;
    my $lastGoodOutlook = 0;
    my $lastNum         = -1;
    my $numBadOutlook   = 0;
    my $numBackups      = 0;

    for ( my $i = 0 ; $i < @Backups ; $i++ ) {
        my $fh;
        #
        # ignore partials and active backups -> only fulls and incrs
        # should be used in figuring out when the last good backup was
        #
        next if ( $Backups[$i]{type} eq "partial" || $Backups[$i]{type} eq "active" );
        $lastNum = $Backups[$i]{num}       if ( $lastNum < $Backups[$i]{num} );
        $last    = $Backups[$i]{startTime} if ( $last < $Backups[$i]{startTime} );
        $numBackups++;
        next if ( $Backups[$i]{xferMethod} ne "smb" );

        my $badOutlook = 0;
        my $comp       = 0;
        my $file       = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}";
        if ( !-f $file ) {
            $comp = 1;
            $file = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}.z";
        }
        next if ( !-f $file || !defined($fh = BackupPC::XS::FileZIO::open($file, 0, $comp)) );
        while ( 1 ) {
            my $s = $fh->readLine();
            last if ( $s eq "" );
            if (   $s =~ /^\s*Error reading file.*\.pst : (ERRDOS - ERRlock|NT_STATUS_FILE_LOCK_CONFLICT)/
                || $s =~ /^\s*Error reading file.*\.pst\. Got 0 bytes/ ) {
                $badOutlook = 1;
                last;
            }
        }
        $fh->close();
        $numBadOutlook += $badOutlook;
        if ( !$badOutlook ) {
            $lastGoodOutlook = $Backups[$i]{startTime}
              if ( $lastGoodOutlook < $Backups[$i]{startTime} );
        }
    }
    if ( $numBackups == 0 ) {
        my $subj =
          defined($HostConf{EMailNoBackupEverSubj})
          ? $HostConf{EMailNoBackupEverSubj}
          : $Lang->{EMailNoBackupEverSubj};
        my $mesg =
          defined($HostConf{EMailNoBackupEverMesg})
          ? $HostConf{EMailNoBackupEverMesg}
          : $Lang->{EMailNoBackupEverMesg};
        sendUserEmail(
            $user, $host, $mesg, $subj,
            {
                userName => user2name($user)
            }
        ) if ( !defined($Jobs{$host}) );
        next;
    }
    if ( time - $last > $HostConf{EMailNotifyOldBackupDays} * 24 * 3600 ) {
        my $subj =
          defined($HostConf{EMailNoBackupRecentSubj})
          ? $HostConf{EMailNoBackupRecentSubj}
          : $Lang->{EMailNoBackupRecentSubj};
        my $mesg =
          defined($HostConf{EMailNoBackupRecentMesg})
          ? $HostConf{EMailNoBackupRecentMesg}
          : $Lang->{EMailNoBackupRecentMesg};
        my $firstTime = sprintf("%.1f", (time - $Backups[0]{startTime}) / (24 * 3600));
        my $days      = sprintf("%.1f", (time - $last) / (24 * 3600));
        sendUserEmail(
            $user, $host, $mesg, $subj,
            {
                firstTime  => $firstTime,
                days       => $days,
                userName   => user2name($user),
                numBackups => $numBackups,
            }
        ) if ( !defined($Jobs{$host}) );
        next;
    }
    if ( $numBadOutlook > 0 && time - $lastGoodOutlook > $HostConf{EMailNotifyOldOutlookDays} * 24 * 3600 ) {
        my($days, $howLong);
        if ( $lastGoodOutlook == 0 ) {
            $howLong = eval("qq{$Lang->{howLong_not_been_backed_up}}");
        } else {
            $days    = sprintf("%.1f", (time - $lastGoodOutlook) / (24 * 3600));
            $howLong = eval("qq{$Lang->{howLong_not_been_backed_up_for_days_days}}");
        }
        my $subj =
          defined($HostConf{EMailOutlookBackupSubj})
          ? $HostConf{EMailOutlookBackupSubj}
          : $Lang->{EMailOutlookBackupSubj};
        my $mesg =
          defined($HostConf{EMailOutlookBackupMesg})
          ? $HostConf{EMailOutlookBackupMesg}
          : $Lang->{EMailOutlookBackupMesg};
        my $firstTime = sprintf("%.1f", (time - $Backups[0]{startTime}) / (24 * 3600));
        my $lastTime  = sprintf("%.1f", (time - $Backups[$#Backups]{startTime}) / (24 * 3600));
        sendUserEmail(
            $user, $host, $mesg, $subj,
            {
                days       => $days,
                firstTime  => $firstTime,
                lastTime   => $lastTime,
                numBackups => $numBackups,
                userName   => user2name($user),
                howLong    => $howLong,
                serverHost => $HostConf{ServerHost},
            }
        ) if ( !defined($Jobs{$host}) );
    }
}

#
# Send per-host errors to per-host admin email
#
if ( @AdminBadHosts ) {
    for my $i ( 0 .. $#AdminBadHosts ) {
        my $badHost = $AdminBadHosts[$i];
        if ( $AdminBadHostsEmail[$i] ne "" ) {
            my $badHostMesg .= <<EOF;
The following host had an error that is probably caused by a
misconfiguration.  Please fix this host:
  - $badHost

EOF
            my $headers = $Conf{EMailHeaders};
            $headers .= "\n" if ( $headers !~ /\n$/ );
            my $subject =
                $Conf{EMailAdminSubject} ne ""
              ? $Conf{EMailAdminSubject}
              : "BackupPC administrative attention needed";
            $badHostMesg = <<EOF;
To: $AdminBadHostsEmail[$i]
Subject: $subject
$headers
${badHostMesg}Regards,
PC Backup Genie
EOF
            SendMail($badHostMesg);
        }
    }
}

###########################################################################
# Generate sysadmin warning message
###########################################################################
my $adminMesg = "";

#
# Report if we skipped backups because the disk was too full
#
if ( $Info{DUDailySkipHostCntPrev} > 0 ) {
    my $n      = $Info{DUDailySkipHostCntPrev};
    my $m      = $Conf{DfMaxUsagePct};
    my $mInode = $Conf{DfMaxInodeUsagePct};
    $adminMesg .= <<EOF;
Yesterday $n hosts were skipped because the file system containing
$TopDir was too full.  The threshold in the configuration
file is currently $m%, while yesterday the file system was up
to $Info{DUDailyMaxPrev}% full.  The maximum inode usage yesterday
was $Info{DUInodeDailyMaxPrev}% and the threshold is currently $mInode%.

Please find more space on the file system, or reduce the number of full
or incremental backups that we keep.

EOF
}

#
# Check for bogus directories (probably PCs that are no longer
# on the backup list)
#
my @oldDirs = ();
foreach my $e ( @{BackupPC::DirOps::dirRead($bpc, "$TopDir/pc")} ) {
    next if ( $e->{name} eq "." || $e->{name} eq ".." );
    my $host = $e->{name};
    next if ( defined($Status{$host}) );
    push(@oldDirs, "$TopDir/pc/$host");
}
if ( @oldDirs ) {
    my $oldDirs = join("\n  - ", sort(@oldDirs));
    $adminMesg .= <<EOF;
The following directories are bogus and are not being used by
BackupPC.  This typically happens when PCs are removed from the
backup list.  If you don't need any old backups from these PCs you
should remove these directories.  If there are machines on this
list that should be backed up then there is a problem with the
hosts file:
  - $oldDirs

EOF
}

if ( $adminMesg ne "" && $Conf{EMailAdminUserName} ne "" ) {
    my $headers = $Conf{EMailHeaders};
    $headers .= "\n" if ( $headers !~ /\n$/ );
    my $subject =
        $Conf{EMailAdminSubject} ne ""
      ? $Conf{EMailAdminSubject}
      : "BackupPC administrative attention needed";
    $adminMesg = <<EOF;
To: $Conf{EMailAdminUserName}
Subject: $subject
$headers
${adminMesg}Regards,
PC Backup Genie
EOF
    SendMail($adminMesg);
}

###########################################################################
# Save email state and exit
###########################################################################
if ( !$opts{t} ) {
    $Data::Dumper::Indent = 1;
    my $dumpStr = Data::Dumper->Dump([\%UserEmailInfo], [qw(*UserEmailInfo)]);
    if ( open(HOST, ">", "$LogDir/UserEmailInfo.pl") ) {
        binmode(HOST);
        print(HOST $dumpStr);
        close(HOST);
    }
}
exit(0);

sub user2name
{
    my($user) = @_;
    my($name) = (getpwnam($user))[6];
    $name =~ s/\s.*//;
    $name = $user if ( $name eq "" );
    return $name;
}

sub sendUserEmail
{
    my($user, $host, $mesg, $subj, $vars) = @_;
    return if ( $Conf{BackupsDisable} );

    $vars->{user}    = $user;
    $vars->{host}    = $host;
    $vars->{headers} = $Conf{EMailHeaders};
    $vars->{headers} .= "\n" if ( $vars->{headers} !~ /\n$/ );
    $vars->{domain} = $Conf{EMailUserDestDomain};
    $vars->{CgiURL} = $Conf{CgiURL};
    $subj =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
    $vars->{subj} = encode('MIME-Header', $subj);
    $mesg =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
    SendMail($mesg);
    $UserEmailInfo{$user}{$host}{lastTime} = time;
    $UserEmailInfo{$user}{$host}{lastSubj} = $subj;
}

sub SendMail
{
    my($mesg) = @_;
    my $from  = $Conf{EMailFromUserName};
    my $utf8  = 1
      if ( $Conf{EMailHeaders} =~ /Content-Type:.*charset="utf-?8"/i );
    local(*MAIL);

    if ( $opts{t} ) {
        binmode(STDOUT, ":utf8") if ( $utf8 );

        print("#" x 75, "\n");
        print $mesg;
        return;
    }
    $from = "-f $from" if ( $from ne "" );
    print("Sending test email using $Conf{SendmailPath} -t $from\n")
      if ( $opts{u} ne "" );
    if ( !open(MAIL, "|$Conf{SendmailPath} -t $from") ) {
        print("Can't run sendmail ($Conf{SendmailPath}): $!\n");
        return;
    }
    binmode(MAIL, ":utf8") if ( $utf8 );
    print MAIL $mesg;
    close(MAIL);
}
