3f82313411526ce10a3ba1b1e66c82f76b887620
[BackupPC.git] / bin / BackupPC_sendEmail
1 #!/bin/perl
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_sendEmail: send status emails to users and admins
5 #
6 # DESCRIPTION
7 #
8 #   BackupPC_sendEmail: send status emails to users and admins.
9 #   BackupPC_sendEmail is run by BackupPC_nightly, so it runs
10 #   once every night.
11 #
12 # AUTHOR
13 #   Craig Barratt  <cbarratt@users.sourceforge.net>
14 #
15 # COPYRIGHT
16 #   Copyright (C) 2001-2003  Craig Barratt
17 #
18 #   This program is free software; you can redistribute it and/or modify
19 #   it under the terms of the GNU General Public License as published by
20 #   the Free Software Foundation; either version 2 of the License, or
21 #   (at your option) any later version.
22 #
23 #   This program is distributed in the hope that it will be useful,
24 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
25 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26 #   GNU General Public License for more details.
27 #
28 #   You should have received a copy of the GNU General Public License
29 #   along with this program; if not, write to the Free Software
30 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
31 #
32 #========================================================================
33 #
34 # Version 3.0.0alpha, released 23 Jan 2006.
35 #
36 # See http://backuppc.sourceforge.net.
37 #
38 #========================================================================
39
40 use strict;
41 no  utf8;
42 use lib "/usr/local/BackupPC/lib";
43 use BackupPC::Lib;
44 use BackupPC::FileZIO;
45
46 use Data::Dumper;
47 use Getopt::Std;
48 use DirHandle ();
49 use vars qw($Lang $TopDir $BinDir $LogDir %Conf);
50
51 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
52 $TopDir = $bpc->TopDir();
53 $LogDir = $bpc->LogDir();
54 $BinDir = $bpc->BinDir();
55 %Conf   = $bpc->Conf();
56 $Lang   = $bpc->Lang();
57
58 $bpc->ChildInit();
59
60 use vars qw(%UserEmailInfo);
61 do "$LogDir/UserEmailInfo.pl";
62
63 my %opts;
64 if ( !getopts("t", \%opts) || @ARGV != 0 ) {
65     print("usage: $0 [-t]\n");
66     exit(1);
67 }
68
69 my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
70 if ( $err ) {
71     print("Can't connect to server ($err)\n");
72     exit(1);
73 }
74 my $reply = $bpc->ServerMesg("status hosts");
75 $reply = $1 if ( $reply =~ /(.*)/s );
76 my(%Status, %Info, %Jobs, @BgQueue, @UserQueue, @CmdQueue);
77 eval($reply);
78
79 ###########################################################################
80 # Generate sysadmin warning messages
81 ###########################################################################
82 my $mesg = "";
83
84 my @badHosts = ();
85 foreach my $host ( sort(keys(%Status)) ) {
86     next if ( ($Status{$host}{reason} ne "Reason_backup_failed"
87                && $Status{$host}{reason} ne "Reason_restore_failed")
88            || $Status{$host}{error} =~ /^lost network connection to host/ );
89     push(@badHosts, "$host ($Status{$host}{error})");
90 }
91 if ( @badHosts ) {
92     my $badHosts = join("\n  - ", sort(@badHosts));
93     $mesg .= <<EOF;
94 The following hosts had an error that is probably caused by a
95 misconfiguration.  Please fix these hosts:
96   - $badHosts
97
98 EOF
99 }
100
101 #
102 # Report if we skipped backups because the disk was too full
103 #
104 if ( $Info{DUDailySkipHostCntPrev} > 0 ) {
105     my $n = $Info{DUDailySkipHostCntPrev};
106     my $m = $Conf{DfMaxUsagePct};
107     $mesg .= <<EOF;
108 Yesterday $n hosts were skipped because the file system containing
109 $TopDir was too full.  The threshold in the
110 configuration file is $m%, while yesterday the file system was
111 up to $Info{DUDailyMaxPrev}% full.  Please find more space on the file system,
112 or reduce the number of full or incremental backups that we keep.
113
114 EOF
115 }
116
117 #
118 # Check for bogus directories (probably PCs that are no longer
119 # on the backup list)
120 #
121 my $d = DirHandle->new("$TopDir/pc") or die("Can't read $TopDir/pc: $!");
122 my @oldDirs = ();
123 my @files = $d->read;
124 $d->close;
125 foreach my $host ( @files ) {
126     next if ( $host =~ /^\./ || defined($Status{$host}) );
127     push(@oldDirs, "$TopDir/pc/$host");
128 }
129 if ( @oldDirs ) {
130     my $oldDirs = join("\n  - ", sort(@oldDirs));
131     $mesg .= <<EOF;
132 The following directories are bogus and are not being used by
133 BackupPC.  This typically happens when PCs are removed from the
134 backup list.  If you don't need any old backups from these PCs you
135 should remove these directories.  If there are machines on this
136 list that should be backed up then there is a problem with the
137 hosts file:
138   - $oldDirs
139
140 EOF
141 }
142
143 if ( $mesg ne "" && $Conf{EMailAdminUserName} ne "" ) {
144     my $headers = $Conf{EMailHeaders};
145     $headers .= "\n" if ( $headers !~ /\n$/ );
146     $mesg = <<EOF;
147 To: $Conf{EMailAdminUserName}
148 Subject: BackupPC administrative attention needed
149 $headers
150 ${mesg}Regards,
151 PC Backup Genie
152 EOF
153     if ( $opts{t} ) {
154         print("#" x 75, "\n");
155         print $mesg;
156     } else {
157         SendMail($mesg);
158     }
159 }
160
161 ###########################################################################
162 # Generate per-host warning messages sent to each user
163 ###########################################################################
164 my $Hosts = $bpc->HostInfoRead();
165
166 foreach my $host ( sort(keys(%Status)) ) {
167     next if ( $Hosts->{$host}{user} eq "" );
168     #
169     # read any per-PC config settings (allowing per-PC email settings)
170     #
171     $bpc->ConfigRead($host);
172     %Conf = $bpc->Conf();
173     my $user = $Hosts->{$host}{user};
174     next if ( time - $UserEmailInfo{$user}{lastTime}
175                         < $Conf{EMailNotifyMinDays} * 24*3600 );
176     next if ($Conf{XferMethod} eq "archive" );
177     my @Backups = $bpc->BackupInfoRead($host);
178     my $numBackups = @Backups;
179     if ( $numBackups == 0 ) {
180         my $subj = defined($Conf{EMailNoBackupEverSubj})
181                         ? $Conf{EMailNoBackupEverSubj}
182                         : $Lang->{EMailNoBackupEverSubj};
183         my $mesg = defined($Conf{EMailNoBackupEverMesg})
184                         ? $Conf{EMailNoBackupEverMesg}
185                         : $Lang->{EMailNoBackupEverMesg};
186         sendUserEmail($user, $host, $mesg, $subj, {
187                             userName => user2name($user)
188                         }) if ( !defined($Jobs{$host}) );
189         next;
190     }
191     my $last = my $lastFull = my $lastIncr = 0;
192     my $lastGoodOutlook = 0;
193     my $lastNum = -1;
194     my $numBadOutlook = 0;
195     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
196         my $fh;
197         #
198         # ignore partials -> only fulls and incrs should be used
199         # in figuring out when the last good backup was
200         #
201         next if ( $Backups[$i]{type} eq "partial" );
202         $lastNum = $Backups[$i]{num} if ( $lastNum < $Backups[$i]{num} );
203         if ( $Backups[$i]{type} eq "full" ) {
204             $lastFull = $Backups[$i]{startTime}
205                     if ( $lastFull < $Backups[$i]{startTime} );
206         } else {
207             $lastIncr = $Backups[$i]{startTime}
208                     if ( $lastIncr < $Backups[$i]{startTime} );
209         }
210         $last = $Backups[$i]{startTime}
211                     if ( $last < $Backups[$i]{startTime} );
212         my $badOutlook = 0;
213         my $file = "$TopDir/pc/$host/SmbLOG.$Backups[$i]{num}";
214         my $comp = 0;
215         if ( !-f $file ) {
216             $file = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}";
217             if ( !-f $file ) {
218                 $comp = 1;
219                 $file = "$TopDir/pc/$host/SmbLOG.$Backups[$i]{num}.z";
220                 $file = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}.z"
221                                                         if ( !-f $file );
222             }
223         }
224         next if ( !defined($fh = BackupPC::FileZIO->open($file, 0, $comp)) );
225         while ( 1 ) {
226             my $s = $fh->readLine();
227             last if ( $s eq "" );
228             if ( $s =~ /^\s*Error reading file.*\.pst : (ERRDOS - ERRlock|NT_STATUS_FILE_LOCK_CONFLICT)/
229                   || $s =~ /^\s*Error reading file.*\.pst\. Got 0 bytes/ ) {
230                 $badOutlook = 1;
231                 last;
232             }
233         }
234         $fh->close();
235         $numBadOutlook += $badOutlook;
236         if ( !$badOutlook ) {
237             $lastGoodOutlook = $Backups[$i]{startTime}
238                     if ( $lastGoodOutlook < $Backups[$i]{startTime} );
239         }
240     }
241     if ( time - $last > $Conf{EMailNotifyOldBackupDays} * 24*3600 ) {
242         my $subj = defined($Conf{EMailNoBackupRecentSubj})
243                         ? $Conf{EMailNoBackupRecentSubj}
244                         : $Lang->{EMailNoBackupRecentSubj};
245         my $mesg = defined($Conf{EMailNoBackupRecentMesg})
246                         ? $Conf{EMailNoBackupRecentMesg}
247                         : $Lang->{EMailNoBackupRecentMesg};
248         my $firstTime = sprintf("%.1f",
249                         (time - $Backups[0]{startTime}) / (24*3600));
250         my $days = sprintf("%.1f", (time - $last) / (24 * 3600));
251         sendUserEmail($user, $host, $mesg, $subj, {
252                             firstTime  => $firstTime,
253                             days       => $days,
254                             userName   => user2name($user),
255                             numBackups => $numBackups,
256                         }) if ( !defined($Jobs{$host}) );
257         next;
258     }
259     if ( $numBadOutlook > 0
260           && time - $lastGoodOutlook > $Conf{EMailNotifyOldOutlookDays}
261                                              * 24 * 3600 ) {
262         my($days, $howLong);
263         if ( $lastGoodOutlook == 0 ) {
264             $howLong = eval("qq{$Lang->{howLong_not_been_backed_up}}");
265         } else {
266             $days = sprintf("%.1f", (time - $lastGoodOutlook) / (24*3600));
267             $howLong = eval("qq{$Lang->{howLong_not_been_backed_up_for_days_days}}");
268         }
269         my $subj = defined($Conf{EMailOutlookBackupSubj})
270                         ? $Conf{EMailOutlookBackupSubj}
271                         : $Lang->{EMailOutlookBackupSubj};
272         my $mesg = defined($Conf{EMailOutlookBackupMesg})
273                         ? $Conf{EMailOutlookBackupMesg}
274                         : $Lang->{EMailOutlookBackupMesg};
275         my $firstTime = sprintf("%.1f",
276                         (time - $Backups[0]{startTime}) / (24*3600));
277         my $lastTime = sprintf("%.1f",
278                         (time - $Backups[$#Backups]{startTime}) / (24*3600));
279         sendUserEmail($user, $host, $mesg, $subj, {
280                             days       => $days,
281                             firstTime  => $firstTime,
282                             lastTime   => $lastTime,
283                             numBackups => $numBackups,
284                             userName   => user2name($user),
285                             howLong    => $howLong,
286                             serverHost => $Conf{ServerHost},
287                         }) if ( !defined($Jobs{$host}) );
288     }
289 }
290 if ( !$opts{t} ) {
291     $Data::Dumper::Indent = 1;
292     my $dumpStr = Data::Dumper->Dump(
293              [\%UserEmailInfo],
294              [qw(*UserEmailInfo)]);
295     if ( open(HOST, ">", "$LogDir/UserEmailInfo.pl") ) {
296         binmode(HOST);
297         print(HOST $dumpStr);
298         close(HOST);
299     }
300 }
301
302 sub user2name
303 {
304     my($user) = @_;
305     my($name) = (getpwnam($user))[6];
306     $name =~ s/\s.*//;
307     $name = $user if ( $name eq "" );
308     return $name;
309 }
310
311 sub sendUserEmail
312 {
313     my($user, $host, $mesg, $subj, $vars) = @_;
314     $vars->{user}     = $user;
315     $vars->{host}     = $host;
316     $vars->{headers}  = $Conf{EMailHeaders};
317     $vars->{headers} .= "\n" if ( $vars->{headers} !~ /\n$/ );
318     $vars->{domain}   = $Conf{EMailUserDestDomain};
319     $vars->{CgiURL}   = $Conf{CgiURL};
320     $subj =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
321     $vars->{subj}   = $subj;
322     $mesg =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
323     if ( $opts{t} ) {
324         print("#" x 75, "\n");
325         print $mesg;
326     } else {
327         SendMail($mesg);
328     }
329     $UserEmailInfo{$user}{lastTime} = time;
330     $UserEmailInfo{$user}{lastSubj} = $subj;
331     $UserEmailInfo{$user}{lastHost} = $host;
332 }
333
334 sub SendMail
335 {
336     my($mesg) = @_;
337     my($from) = $Conf{EMailFromUserName};
338     local(*MAIL);
339
340     $from = "-f $from" if ( $from ne "" );
341     if ( !open(MAIL, "|$Conf{SendmailPath} -t $from") ) {
342         printf("Can't run sendmail ($Conf{SendmailPath}): $!\n");
343         return;
344     }
345     print MAIL $mesg;
346     close(MAIL);
347 }