added humanly readable unit (b k M G)
[BackupPC.git] / bin / BackupPC_sendEmail
index 2a7184c..c20c6cc 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/perl -T
+#!/usr/bin/perl
 #============================================================= -*-perl-*-
 #
 # BackupPC_sendEmail: send status emails to users and admins
@@ -13,7 +13,7 @@
 #   Craig Barratt  <cbarratt@users.sourceforge.net>
 #
 # COPYRIGHT
-#   Copyright (C) 2001  Craig Barratt
+#   Copyright (C) 2001-2009  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
 #
 #========================================================================
 #
-# Version 1.6.0_CVS, released 10 Dec 2002.
+# Version 3.2.0, released 31 Jul 2010.
 #
 # See http://backuppc.sourceforge.net.
 #
 #========================================================================
 
 use strict;
+no  utf8;
 use lib "/usr/local/BackupPC/lib";
 use BackupPC::Lib;
 use BackupPC::FileZIO;
+use Encode;
 
 use Data::Dumper;
 use Getopt::Std;
 use DirHandle ();
+use vars qw($Lang $TopDir $BinDir $LogDir %Conf $Hosts);
 
 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
-my $TopDir = $bpc->TopDir();
-my $BinDir = $bpc->BinDir();
-my %Conf   = $bpc->Conf();
+$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 "$TopDir/log/UserEmailInfo.pl";
+do "$LogDir/UserEmailInfo.pl";
 
 my %opts;
-getopts("t", \%opts);
-if ( @ARGV != 0 ) {
-    print("usage: $0 [-t]\n");
-    exit(1);
-}
+if ( !getopts("ctu:", \%opts) || @ARGV != 0 ) {
+    print <<EOF;
+usage: $0 [-t] [-c] [-u userEmail]
+options:
 
-my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
-if ( $err ) {
-    print("Can't connect to server ($err)\n");
-    exit(1);
-}
-my $reply = $bpc->ServerMesg("status hosts");
-$reply = $1 if ( $reply =~ /(.*)/s );
-my(%Status, %Info, %Jobs, @BgQueue, @UserQueue, @CmdQueue);
-eval($reply);
+  -t  display the emails that would be sent, without sending them
 
-###########################################################################
-# Generate sysadmin warning messages
-###########################################################################
-my $mesg = "";
-
-my @badHosts = ();
-foreach my $host ( sort(keys(%Status)) ) {
-    next if ( $Status{$host}{reason} ne "backup failed"
-           || $Status{$host}{error} =~ /^lost network connection to host/ );
-    push(@badHosts, "$host ($Status{$host}{error})");
-}
-if ( @badHosts ) {
-    my $badHosts = join("\n  - ", sort(@badHosts));
-    $mesg .= <<EOF;
-The following hosts had an error that is probably caused by a
-misconfiguration.  Please fix these hosts:
-  - $badHosts
+  -c  check if BackupPC is alive and send an email if not
 
+  -u  send a test email to userEmail
 EOF
+    exit(1);
 }
 
 #
-# Report if we skipped backups because the disk was too full
+# Upgrade legacy version of %UserEmailInfo
 #
-if ( $Info{DUDailySkipHostCntPrev} > 0 ) {
-    my $n = $Info{DUDailySkipHostCntPrev};
-    my $m = $Conf{DfMaxUsagePct};
-    $mesg .= <<EOF;
-Yesterday $n hosts were skipped because the file system containing
-$TopDir was too full.  The threshold in the
-configuration file is $m%, while yesterday the file system was
-up to $Info{DUDailyMaxPrev}% full.  Please find more space on the file system,
-or reduce the number of full or incremental backups that we keep.
-
-EOF
+# 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};
+    }
 }
 
 #
-# Check for bogus directories (probably PCs that are no longer
-# on the backup list)
+# Prune hosts that no longer exist
 #
-my $d = DirHandle->new("$TopDir/pc") or die("Can't read $TopDir/pc: $!");
-my @oldDirs = ();
-my @files = $d->read;
-$d->close;
-foreach my $host ( @files ) {
-    next if ( $host eq "." || $host eq ".." || defined($Status{$host}) );
-    push(@oldDirs, "$TopDir/pc/$host");
+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});
 }
-if ( @oldDirs ) {
-    my $oldDirs = join("\n  - ", sort(@oldDirs));
-    $mesg .= <<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
 
+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 $mesg = <<EOF;
+To: $Conf{EMailAdminUserName}
+Subject: BackupPC: can't connect to server
+$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);
 
-if ( $mesg ne "" && $Conf{EMailAdminUserName} ne "" ) {
-    $mesg = <<EOF;
-To: $Conf{EMailAdminUserName}
-Subject: BackupPC administrative attention needed
+###########################################################################
+# Generate test message if required
+###########################################################################
+if ( $opts{u} ne "" ) {
+    my $headers = $Conf{EMailHeaders};
+    $headers .= "\n" if ( $headers !~ /\n$/ );
+    my $mesg = <<EOF;
+To: $opts{u}
+Subject: BackupPC test email
+$headers
+This is a test message from $0.
 
-${mesg}Regards,
+Regards,
 PC Backup Genie
 EOF
-    if ( $opts{t} ) {
-        print("#" x 75, "\n");
-        print $mesg;
-    } else {
-       SendMail($mesg);
-    }
+    SendMail($mesg);
+    exit(0);
 }
 
 ###########################################################################
 # Generate per-host warning messages sent to each user
 ###########################################################################
-my $Hosts = $bpc->HostInfoRead();
+my @AdminBadHosts = ();
 
 foreach my $host ( sort(keys(%Status)) ) {
-    next if ( $Hosts->{$host}{user} eq "" );
     #
     # read any per-PC config settings (allowing per-PC email settings)
     #
     $bpc->ConfigRead($host);
     %Conf = $bpc->Conf();
     my $user = $Hosts->{$host}{user};
-    next if ( time - $UserEmailInfo{$user}{lastTime}
-                        < $Conf{EMailNotifyMinDays} * 24*3600 );
+
+    next if ( $user eq "" );
+
+    #
+    # 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/
+           && !$Conf{BackupsDisable}
+       ) {
+        push(@AdminBadHosts, "$host ($Status{$host}{error})");
+    }
+
+    next if ( time - $UserEmailInfo{$user}{$host}{lastTime}
+                        < $Conf{EMailNotifyMinDays} * 24*3600
+              || $Conf{XferMethod} eq "archive"
+              || $Conf{BackupsDisable}
+              || $Hosts->{$host}{user} eq ""
+          );
     my @Backups = $bpc->BackupInfoRead($host);
     my $numBackups = @Backups;
     if ( $numBackups == 0 ) {
-        my $subj = "BackupPC: no backups of $host have succeeded";
-        sendUserEmail($user, $host, $Conf{EMailNoBackupEverMesg}, $subj, {
+        my $subj = defined($Conf{EMailNoBackupEverSubj})
+                       ? $Conf{EMailNoBackupEverSubj}
+                       : $Lang->{EMailNoBackupEverSubj};
+        my $mesg = defined($Conf{EMailNoBackupEverMesg})
+                       ? $Conf{EMailNoBackupEverMesg}
+                       : $Lang->{EMailNoBackupEverMesg};
+        sendUserEmail($user, $host, $mesg, $subj, {
                             userName => user2name($user)
                         }) if ( !defined($Jobs{$host}) );
         next;
@@ -182,6 +225,11 @@ foreach my $host ( sort(keys(%Status)) ) {
     my $numBadOutlook = 0;
     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
         my $fh;
+        #
+        # ignore partials -> only fulls and incrs should be used
+        # in figuring out when the last good backup was
+        #
+        next if ( $Backups[$i]{type} eq "partial" );
         $lastNum = $Backups[$i]{num} if ( $lastNum < $Backups[$i]{num} );
         if ( $Backups[$i]{type} eq "full" ) {
             $lastFull = $Backups[$i]{startTime}
@@ -208,8 +256,8 @@ foreach my $host ( sort(keys(%Status)) ) {
         while ( 1 ) {
             my $s = $fh->readLine();
             last if ( $s eq "" );
-            if ( $s =~ /^Error reading file.*\.pst : ERRDOS - ERRlock/
-                  || $s =~ /^Error reading file.*\.pst\. Got 0 bytes/ ) {
+            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;
             }
@@ -222,11 +270,16 @@ foreach my $host ( sort(keys(%Status)) ) {
         }
     }
     if ( time - $last > $Conf{EMailNotifyOldBackupDays} * 24*3600 ) {
-        my $subj = "BackupPC: no recent backups on $host";
+        my $subj = defined($Conf{EMailNoBackupRecentSubj})
+                       ? $Conf{EMailNoBackupRecentSubj}
+                       : $Lang->{EMailNoBackupRecentSubj};
+        my $mesg = defined($Conf{EMailNoBackupRecentMesg})
+                       ? $Conf{EMailNoBackupRecentMesg}
+                       : $Lang->{EMailNoBackupRecentMesg};
         my $firstTime = sprintf("%.1f",
                         (time - $Backups[0]{startTime}) / (24*3600));
         my $days = sprintf("%.1f", (time - $last) / (24 * 3600));
-        sendUserEmail($user, $host, $Conf{EMailNoBackupRecentMesg}, $subj, {
+        sendUserEmail($user, $host, $mesg, $subj, {
                             firstTime  => $firstTime,
                             days       => $days,
                             userName   => user2name($user),
@@ -239,35 +292,118 @@ foreach my $host ( sort(keys(%Status)) ) {
                                              * 24 * 3600 ) {
         my($days, $howLong);
         if ( $lastGoodOutlook == 0 ) {
-            $howLong = "not been backed up successfully";
+            $howLong = eval("qq{$Lang->{howLong_not_been_backed_up}}");
         } else {
             $days = sprintf("%.1f", (time - $lastGoodOutlook) / (24*3600));
-            $howLong = "not been backed up for $days days";
+            $howLong = eval("qq{$Lang->{howLong_not_been_backed_up_for_days_days}}");
         }
-        my $subj = "BackupPC: Outlook files on $host need to be backed up";
+        my $subj = defined($Conf{EMailOutlookBackupSubj})
+                       ? $Conf{EMailOutlookBackupSubj}
+                       : $Lang->{EMailOutlookBackupSubj};
+        my $mesg = defined($Conf{EMailOutlookBackupMesg})
+                       ? $Conf{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, $Conf{EMailOutlookBackupMesg}, $subj, {
+        sendUserEmail($user, $host, $mesg, $subj, {
+                            days       => $days,
                             firstTime  => $firstTime,
                             lastTime   => $lastTime,
                             numBackups => $numBackups,
                             userName   => user2name($user),
                             howLong    => $howLong,
+                            serverHost => $Conf{ServerHost},
                         }) if ( !defined($Jobs{$host}) );
     }
 }
+
+###########################################################################
+# Generate sysadmin warning message
+###########################################################################
+my $adminMesg = "";
+
+if ( @AdminBadHosts ) {
+    my $badHosts = join("\n  - ", sort(@AdminBadHosts));
+    $adminMesg .= <<EOF;
+The following hosts had an error that is probably caused by a
+misconfiguration.  Please fix these hosts:
+  - $badHosts
+
+EOF
+}
+
+#
+# Report if we skipped backups because the disk was too full
+#
+if ( $Info{DUDailySkipHostCntPrev} > 0 ) {
+    my $n = $Info{DUDailySkipHostCntPrev};
+    my $m = $Conf{DfMaxUsagePct};
+    $adminMesg .= <<EOF;
+Yesterday $n hosts were skipped because the file system containing
+$TopDir was too full.  The threshold in the
+configuration file is $m%, while yesterday the file system was
+up to $Info{DUDailyMaxPrev}% full.  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 $d = DirHandle->new("$TopDir/pc") or die("Can't read $TopDir/pc: $!");
+my @oldDirs = ();
+my @files = $d->read;
+$d->close;
+foreach my $host ( @files ) {
+    next if ( $host =~ /^\./ || 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$/ );
+    $adminMesg = <<EOF;
+To: $Conf{EMailAdminUserName}
+Subject: BackupPC administrative attention needed
+$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, ">$TopDir/log/UserEmailInfo.pl") ) {
+    if ( open(HOST, ">", "$LogDir/UserEmailInfo.pl") ) {
+       binmode(HOST);
         print(HOST $dumpStr);
         close(HOST);
     }
 }
+exit(0);
 
 sub user2name
 {
@@ -281,32 +417,45 @@ sub user2name
 sub sendUserEmail
 {
     my($user, $host, $mesg, $subj, $vars) = @_;
-    $vars->{user} = $user;
-    $vars->{host} = $host;
-    $vars->{subj} = $subj;
-    $mesg =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : \$$1/eg;
-    if ( $opts{t} ) {
-        print("#" x 75, "\n");
-        print $mesg;
-    } else {
-       SendMail($mesg);
-    }
-    $UserEmailInfo{$user}{lastTime} = time;
-    $UserEmailInfo{$user}{lastSubj} = $subj;
-    $UserEmailInfo{$user}{lastHost} = $host;
+    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 $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") ) {
        printf("Can't run sendmail ($Conf{SendmailPath}): $!\n");
        return;
     }
+    binmode(MAIL, ":utf8") if ( $utf8 );
     print MAIL $mesg;
     close(MAIL);
 }