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