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