create view backups_on_dvds needed for archive
[BackupPC.git] / bin / BackupPC_sendEmail
1 #!/usr/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-2009  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.2.0, released 31 Jul 2010.
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 use Encode;
46
47 use Data::Dumper;
48 use Getopt::Std;
49 use DirHandle ();
50 use vars qw($Lang $TopDir $BinDir $LogDir %Conf $Hosts);
51
52 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
53 $TopDir = $bpc->TopDir();
54 $LogDir = $bpc->LogDir();
55 $BinDir = $bpc->BinDir();
56 %Conf   = $bpc->Conf();
57 $Lang   = $bpc->Lang();
58 $Hosts  = $bpc->HostInfoRead();
59
60 $bpc->ChildInit();
61
62 use vars qw(%UserEmailInfo);
63 do "$LogDir/UserEmailInfo.pl";
64
65 my %opts;
66 if ( !getopts("ctu:", \%opts) || @ARGV != 0 ) {
67     print <<EOF;
68 usage: $0 [-t] [-c] [-u userEmail]
69 options:
70
71   -t  display the emails that would be sent, without sending them
72
73   -c  check if BackupPC is alive and send an email if not
74
75   -u  send a test email to userEmail
76 EOF
77     exit(1);
78 }
79
80 #
81 # Upgrade legacy version of %UserEmailInfo
82 #
83 # Prior to 3.2.0, it was a hash with entries:
84 #
85 #    $UserEmailInfo{$user}{lastTime}
86 #    $UserEmailInfo{$user}{lastSubj}
87 #    $UserEmailInfo{$user}{lastHost}
88 #
89 # However, if a user had multiple hosts, then an email about one
90 # host prevents mail delivery about other hosts.  Starting in 3.2.0
91 # the hash is:
92 #
93 #    $UserEmailInfo{$user}{$host}{lastTime}
94 #    $UserEmailInfo{$user}{$host}{lastSubj}
95 #
96 my $oldFormat = 0;
97 foreach my $user ( keys(%UserEmailInfo) ) {
98     if ( defined($UserEmailInfo{$user}{lastTime})
99             && ref($UserEmailInfo{$user}{lastTime}) ne 'HASH' ) {
100         $oldFormat = 1;
101         last;
102     }
103 }
104 if ( $oldFormat ) {
105     #
106     # Convert to the new format
107     #
108     my %UserEmailInfoOld = %UserEmailInfo;
109     %UserEmailInfo = ();
110     foreach my $user ( keys(%UserEmailInfoOld) ) {
111         next if ( $user eq "" );
112         my $host = $UserEmailInfoOld{$user}{lastHost};
113         next if ( !defined($host) );
114         $UserEmailInfo{$user}{$host}{lastTime} = $UserEmailInfoOld{$user}{lastTime};
115         $UserEmailInfo{$user}{$host}{lastSubj} = $UserEmailInfoOld{$user}{lastSubj};
116     }
117 }
118
119 #
120 # Prune hosts that no longer exist
121 #
122 foreach my $user ( keys(%UserEmailInfo) ) {
123     foreach my $host ( keys(%{$UserEmailInfo{$user}}) ) {
124         next if ( defined($Hosts->{$host}) );
125         delete($UserEmailInfo{$user}{$host});
126     }
127     next if ( $UserEmailInfo{$user} );
128     delete($UserEmailInfo{$user});
129 }
130
131 my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
132 if ( $err ) {
133     if ( $opts{c} && $Conf{EMailAdminUserName} ne "" ) {
134         my $headers = $Conf{EMailHeaders};
135         $headers .= "\n" if ( $headers !~ /\n$/ );
136         my $mesg = <<EOF;
137 To: $Conf{EMailAdminUserName}
138 Subject: BackupPC: can't connect to server
139 $headers
140 Error: cannot connect to BackupPC server.
141
142 Regards,
143 PC Backup Genie
144 EOF
145         SendMail($mesg);
146         exit(1);
147     }
148     print("Can't connect to server ($err)\n");
149     exit(1);
150 }
151 exit(0) if ( $opts{c} );
152 my $reply = $bpc->ServerMesg("status hosts info");
153 $reply = $1 if ( $reply =~ /(.*)/s );
154 my(%Status, %Info, %Jobs, @BgQueue, @UserQueue, @CmdQueue);
155 eval($reply);
156
157 ###########################################################################
158 # Generate test message if required
159 ###########################################################################
160 if ( $opts{u} ne "" ) {
161     my $headers = $Conf{EMailHeaders};
162     $headers .= "\n" if ( $headers !~ /\n$/ );
163     my $mesg = <<EOF;
164 To: $opts{u}
165 Subject: BackupPC test email
166 $headers
167 This is a test message from $0.
168
169 Regards,
170 PC Backup Genie
171 EOF
172     SendMail($mesg);
173     exit(0);
174 }
175
176 ###########################################################################
177 # Generate per-host warning messages sent to each user
178 ###########################################################################
179 my @AdminBadHosts = ();
180
181 foreach my $host ( sort(keys(%Status)) ) {
182     #
183     # read any per-PC config settings (allowing per-PC email settings)
184     #
185     $bpc->ConfigRead($host);
186     %Conf = $bpc->Conf();
187     my $user = $Hosts->{$host}{user};
188
189     next if ( $user eq "" );
190
191     #
192     # Accumulate host errors for the admin email below
193     #
194     if ( ($Status{$host}{reason} eq "Reason_backup_failed"
195                || $Status{$host}{reason} eq "Reason_restore_failed")
196            && $Status{$host}{error} !~ /^lost network connection to host/
197            && !$Conf{BackupsDisable}
198        ) {
199         push(@AdminBadHosts, "$host ($Status{$host}{error})");
200     }
201
202     next if ( time - $UserEmailInfo{$user}{$host}{lastTime}
203                         < $Conf{EMailNotifyMinDays} * 24*3600
204               || $Conf{XferMethod} eq "archive"
205               || $Conf{BackupsDisable}
206               || $Hosts->{$host}{user} eq ""
207           );
208     my @Backups = $bpc->BackupInfoRead($host);
209     my $numBackups = @Backups;
210     if ( $numBackups == 0 ) {
211         my $subj = defined($Conf{EMailNoBackupEverSubj})
212                         ? $Conf{EMailNoBackupEverSubj}
213                         : $Lang->{EMailNoBackupEverSubj};
214         my $mesg = defined($Conf{EMailNoBackupEverMesg})
215                         ? $Conf{EMailNoBackupEverMesg}
216                         : $Lang->{EMailNoBackupEverMesg};
217         sendUserEmail($user, $host, $mesg, $subj, {
218                             userName => user2name($user)
219                         }) if ( !defined($Jobs{$host}) );
220         next;
221     }
222     my $last = my $lastFull = my $lastIncr = 0;
223     my $lastGoodOutlook = 0;
224     my $lastNum = -1;
225     my $numBadOutlook = 0;
226     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
227         my $fh;
228         #
229         # ignore partials -> only fulls and incrs should be used
230         # in figuring out when the last good backup was
231         #
232         next if ( $Backups[$i]{type} eq "partial" );
233         $lastNum = $Backups[$i]{num} if ( $lastNum < $Backups[$i]{num} );
234         if ( $Backups[$i]{type} eq "full" ) {
235             $lastFull = $Backups[$i]{startTime}
236                     if ( $lastFull < $Backups[$i]{startTime} );
237         } else {
238             $lastIncr = $Backups[$i]{startTime}
239                     if ( $lastIncr < $Backups[$i]{startTime} );
240         }
241         $last = $Backups[$i]{startTime}
242                     if ( $last < $Backups[$i]{startTime} );
243         my $badOutlook = 0;
244         my $file = "$TopDir/pc/$host/SmbLOG.$Backups[$i]{num}";
245         my $comp = 0;
246         if ( !-f $file ) {
247             $file = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}";
248             if ( !-f $file ) {
249                 $comp = 1;
250                 $file = "$TopDir/pc/$host/SmbLOG.$Backups[$i]{num}.z";
251                 $file = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}.z"
252                                                         if ( !-f $file );
253             }
254         }
255         next if ( !defined($fh = BackupPC::FileZIO->open($file, 0, $comp)) );
256         while ( 1 ) {
257             my $s = $fh->readLine();
258             last if ( $s eq "" );
259             if ( $s =~ /^\s*Error reading file.*\.pst : (ERRDOS - ERRlock|NT_STATUS_FILE_LOCK_CONFLICT)/
260                   || $s =~ /^\s*Error reading file.*\.pst\. Got 0 bytes/ ) {
261                 $badOutlook = 1;
262                 last;
263             }
264         }
265         $fh->close();
266         $numBadOutlook += $badOutlook;
267         if ( !$badOutlook ) {
268             $lastGoodOutlook = $Backups[$i]{startTime}
269                     if ( $lastGoodOutlook < $Backups[$i]{startTime} );
270         }
271     }
272     if ( time - $last > $Conf{EMailNotifyOldBackupDays} * 24*3600 ) {
273         my $subj = defined($Conf{EMailNoBackupRecentSubj})
274                         ? $Conf{EMailNoBackupRecentSubj}
275                         : $Lang->{EMailNoBackupRecentSubj};
276         my $mesg = defined($Conf{EMailNoBackupRecentMesg})
277                         ? $Conf{EMailNoBackupRecentMesg}
278                         : $Lang->{EMailNoBackupRecentMesg};
279         my $firstTime = sprintf("%.1f",
280                         (time - $Backups[0]{startTime}) / (24*3600));
281         my $days = sprintf("%.1f", (time - $last) / (24 * 3600));
282         sendUserEmail($user, $host, $mesg, $subj, {
283                             firstTime  => $firstTime,
284                             days       => $days,
285                             userName   => user2name($user),
286                             numBackups => $numBackups,
287                         }) if ( !defined($Jobs{$host}) );
288         next;
289     }
290     if ( $numBadOutlook > 0
291           && time - $lastGoodOutlook > $Conf{EMailNotifyOldOutlookDays}
292                                              * 24 * 3600 ) {
293         my($days, $howLong);
294         if ( $lastGoodOutlook == 0 ) {
295             $howLong = eval("qq{$Lang->{howLong_not_been_backed_up}}");
296         } else {
297             $days = sprintf("%.1f", (time - $lastGoodOutlook) / (24*3600));
298             $howLong = eval("qq{$Lang->{howLong_not_been_backed_up_for_days_days}}");
299         }
300         my $subj = defined($Conf{EMailOutlookBackupSubj})
301                         ? $Conf{EMailOutlookBackupSubj}
302                         : $Lang->{EMailOutlookBackupSubj};
303         my $mesg = defined($Conf{EMailOutlookBackupMesg})
304                         ? $Conf{EMailOutlookBackupMesg}
305                         : $Lang->{EMailOutlookBackupMesg};
306         my $firstTime = sprintf("%.1f",
307                         (time - $Backups[0]{startTime}) / (24*3600));
308         my $lastTime = sprintf("%.1f",
309                         (time - $Backups[$#Backups]{startTime}) / (24*3600));
310         sendUserEmail($user, $host, $mesg, $subj, {
311                             days       => $days,
312                             firstTime  => $firstTime,
313                             lastTime   => $lastTime,
314                             numBackups => $numBackups,
315                             userName   => user2name($user),
316                             howLong    => $howLong,
317                             serverHost => $Conf{ServerHost},
318                         }) if ( !defined($Jobs{$host}) );
319     }
320 }
321
322 ###########################################################################
323 # Generate sysadmin warning message
324 ###########################################################################
325 my $adminMesg = "";
326
327 if ( @AdminBadHosts ) {
328     my $badHosts = join("\n  - ", sort(@AdminBadHosts));
329     $adminMesg .= <<EOF;
330 The following hosts had an error that is probably caused by a
331 misconfiguration.  Please fix these hosts:
332   - $badHosts
333
334 EOF
335 }
336
337 #
338 # Report if we skipped backups because the disk was too full
339 #
340 if ( $Info{DUDailySkipHostCntPrev} > 0 ) {
341     my $n = $Info{DUDailySkipHostCntPrev};
342     my $m = $Conf{DfMaxUsagePct};
343     $adminMesg .= <<EOF;
344 Yesterday $n hosts were skipped because the file system containing
345 $TopDir was too full.  The threshold in the
346 configuration file is $m%, while yesterday the file system was
347 up to $Info{DUDailyMaxPrev}% full.  Please find more space on the file system,
348 or reduce the number of full or incremental backups that we keep.
349
350 EOF
351 }
352
353 #
354 # Check for bogus directories (probably PCs that are no longer
355 # on the backup list)
356 #
357 my $d = DirHandle->new("$TopDir/pc") or die("Can't read $TopDir/pc: $!");
358 my @oldDirs = ();
359 my @files = $d->read;
360 $d->close;
361 foreach my $host ( @files ) {
362     next if ( $host =~ /^\./ || defined($Status{$host}) );
363     push(@oldDirs, "$TopDir/pc/$host");
364 }
365 if ( @oldDirs ) {
366     my $oldDirs = join("\n  - ", sort(@oldDirs));
367     $adminMesg .= <<EOF;
368 The following directories are bogus and are not being used by
369 BackupPC.  This typically happens when PCs are removed from the
370 backup list.  If you don't need any old backups from these PCs you
371 should remove these directories.  If there are machines on this
372 list that should be backed up then there is a problem with the
373 hosts file:
374   - $oldDirs
375
376 EOF
377 }
378
379 if ( $adminMesg ne "" && $Conf{EMailAdminUserName} ne "" ) {
380     my $headers = $Conf{EMailHeaders};
381     $headers .= "\n" if ( $headers !~ /\n$/ );
382     $adminMesg = <<EOF;
383 To: $Conf{EMailAdminUserName}
384 Subject: BackupPC administrative attention needed
385 $headers
386 ${adminMesg}Regards,
387 PC Backup Genie
388 EOF
389     SendMail($adminMesg);
390 }
391
392 ###########################################################################
393 # Save email state and exit
394 ###########################################################################
395 if ( !$opts{t} ) {
396     $Data::Dumper::Indent = 1;
397     my $dumpStr = Data::Dumper->Dump(
398              [\%UserEmailInfo],
399              [qw(*UserEmailInfo)]);
400     if ( open(HOST, ">", "$LogDir/UserEmailInfo.pl") ) {
401         binmode(HOST);
402         print(HOST $dumpStr);
403         close(HOST);
404     }
405 }
406 exit(0);
407
408 sub user2name
409 {
410     my($user) = @_;
411     my($name) = (getpwnam($user))[6];
412     $name =~ s/\s.*//;
413     $name = $user if ( $name eq "" );
414     return $name;
415 }
416
417 sub sendUserEmail
418 {
419     my($user, $host, $mesg, $subj, $vars) = @_;
420     return if ( $Conf{BackupsDisable} );
421
422     $vars->{user}     = $user;
423     $vars->{host}     = $host;
424     $vars->{headers}  = $Conf{EMailHeaders};
425     $vars->{headers} .= "\n" if ( $vars->{headers} !~ /\n$/ );
426     $vars->{domain}   = $Conf{EMailUserDestDomain};
427     $vars->{CgiURL}   = $Conf{CgiURL};
428     $subj =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
429     $vars->{subj}     = encode('MIME-Header', $subj);
430     $mesg =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
431     SendMail($mesg);
432     $UserEmailInfo{$user}{$host}{lastTime} = time;
433     $UserEmailInfo{$user}{$host}{lastSubj} = $subj;
434 }
435
436 sub SendMail
437 {
438     my($mesg) = @_;
439     my $from = $Conf{EMailFromUserName};
440     my $utf8 = 1
441         if ( $Conf{EMailHeaders} =~ /Content-Type:.*charset="utf-?8"/i );
442     local(*MAIL);
443
444     if ( $opts{t} ) {
445         binmode(STDOUT, ":utf8") if ( $utf8 );
446         
447         print("#" x 75, "\n");
448         print $mesg;
449         return;
450     }
451     $from = "-f $from" if ( $from ne "" );
452     print("Sending test email using $Conf{SendmailPath} -t $from\n")
453                 if ( $opts{u} ne "" );
454     if ( !open(MAIL, "|$Conf{SendmailPath} -t $from") ) {
455         printf("Can't run sendmail ($Conf{SendmailPath}): $!\n");
456         return;
457     }
458     binmode(MAIL, ":utf8") if ( $utf8 );
459     print MAIL $mesg;
460     close(MAIL);
461 }