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