Various changes, including changes in 2.1.1 and 2.1.2 releases.
[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 2.1.0, released 20 Jun 2004.
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 %Conf);
50
51 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
52 $TopDir = $bpc->TopDir();
53 $BinDir = $bpc->BinDir();
54 %Conf   = $bpc->Conf();
55 $Lang   = $bpc->Lang();
56
57 $bpc->ChildInit();
58
59 use vars qw(%UserEmailInfo);
60 do "$TopDir/log/UserEmailInfo.pl";
61
62 my %opts;
63 if ( !getopts("t", \%opts) || @ARGV != 0 ) {
64     print("usage: $0 [-t]\n");
65     exit(1);
66 }
67
68 my $err = $bpc->ServerConnect($Conf{ServerHost}, $Conf{ServerPort});
69 if ( $err ) {
70     print("Can't connect to server ($err)\n");
71     exit(1);
72 }
73 my $reply = $bpc->ServerMesg("status hosts");
74 $reply = $1 if ( $reply =~ /(.*)/s );
75 my(%Status, %Info, %Jobs, @BgQueue, @UserQueue, @CmdQueue);
76 eval($reply);
77
78 ###########################################################################
79 # Generate sysadmin warning messages
80 ###########################################################################
81 my $mesg = "";
82
83 my @badHosts = ();
84 foreach my $host ( sort(keys(%Status)) ) {
85     next if ( ($Status{$host}{reason} ne "Reason_backup_failed"
86                && $Status{$host}{reason} ne "Reason_restore_failed")
87            || $Status{$host}{error} =~ /^lost network connection to host/ );
88     push(@badHosts, "$host ($Status{$host}{error})");
89 }
90 if ( @badHosts ) {
91     my $badHosts = join("\n  - ", sort(@badHosts));
92     $mesg .= <<EOF;
93 The following hosts had an error that is probably caused by a
94 misconfiguration.  Please fix these hosts:
95   - $badHosts
96
97 EOF
98 }
99
100 #
101 # Report if we skipped backups because the disk was too full
102 #
103 if ( $Info{DUDailySkipHostCntPrev} > 0 ) {
104     my $n = $Info{DUDailySkipHostCntPrev};
105     my $m = $Conf{DfMaxUsagePct};
106     $mesg .= <<EOF;
107 Yesterday $n hosts were skipped because the file system containing
108 $TopDir was too full.  The threshold in the
109 configuration file is $m%, while yesterday the file system was
110 up to $Info{DUDailyMaxPrev}% full.  Please find more space on the file system,
111 or reduce the number of full or incremental backups that we keep.
112
113 EOF
114 }
115
116 #
117 # Check for bogus directories (probably PCs that are no longer
118 # on the backup list)
119 #
120 my $d = DirHandle->new("$TopDir/pc") or die("Can't read $TopDir/pc: $!");
121 my @oldDirs = ();
122 my @files = $d->read;
123 $d->close;
124 foreach my $host ( @files ) {
125     next if ( $host =~ /^\./ || defined($Status{$host}) );
126     push(@oldDirs, "$TopDir/pc/$host");
127 }
128 if ( @oldDirs ) {
129     my $oldDirs = join("\n  - ", sort(@oldDirs));
130     $mesg .= <<EOF;
131 The following directories are bogus and are not being used by
132 BackupPC.  This typically happens when PCs are removed from the
133 backup list.  If you don't need any old backups from these PCs you
134 should remove these directories.  If there are machines on this
135 list that should be backed up then there is a problem with the
136 hosts file:
137   - $oldDirs
138
139 EOF
140 }
141
142 if ( $mesg ne "" && $Conf{EMailAdminUserName} ne "" ) {
143     my $headers = $Conf{EMailHeaders};
144     $headers .= "\n" if ( $headers !~ /\n$/ );
145     $mesg = <<EOF;
146 To: $Conf{EMailAdminUserName}
147 Subject: BackupPC administrative attention needed
148 $headers
149 ${mesg}Regards,
150 PC Backup Genie
151 EOF
152     if ( $opts{t} ) {
153         print("#" x 75, "\n");
154         print $mesg;
155     } else {
156         SendMail($mesg);
157     }
158 }
159
160 ###########################################################################
161 # Generate per-host warning messages sent to each user
162 ###########################################################################
163 my $Hosts = $bpc->HostInfoRead();
164
165 foreach my $host ( sort(keys(%Status)) ) {
166     next if ( $Hosts->{$host}{user} eq "" );
167     #
168     # read any per-PC config settings (allowing per-PC email settings)
169     #
170     $bpc->ConfigRead($host);
171     %Conf = $bpc->Conf();
172     my $user = $Hosts->{$host}{user};
173     next if ( time - $UserEmailInfo{$user}{lastTime}
174                         < $Conf{EMailNotifyMinDays} * 24*3600 );
175     next if ($Conf{XferMethod} eq "archive" );
176     my @Backups = $bpc->BackupInfoRead($host);
177     my $numBackups = @Backups;
178     if ( $numBackups == 0 ) {
179         my $subj = defined($Conf{EMailNoBackupEverSubj})
180                         ? $Conf{EMailNoBackupEverSubj}
181                         : $Lang->{EMailNoBackupEverSubj};
182         my $mesg = defined($Conf{EMailNoBackupEverMesg})
183                         ? $Conf{EMailNoBackupEverMesg}
184                         : $Lang->{EMailNoBackupEverMesg};
185         sendUserEmail($user, $host, $mesg, $subj, {
186                             userName => user2name($user)
187                         }) if ( !defined($Jobs{$host}) );
188         next;
189     }
190     my $last = my $lastFull = my $lastIncr = 0;
191     my $lastGoodOutlook = 0;
192     my $lastNum = -1;
193     my $numBadOutlook = 0;
194     for ( my $i = 0 ; $i < @Backups ; $i++ ) {
195         my $fh;
196         $lastNum = $Backups[$i]{num} if ( $lastNum < $Backups[$i]{num} );
197         if ( $Backups[$i]{type} eq "full" ) {
198             $lastFull = $Backups[$i]{startTime}
199                     if ( $lastFull < $Backups[$i]{startTime} );
200         } else {
201             $lastIncr = $Backups[$i]{startTime}
202                     if ( $lastIncr < $Backups[$i]{startTime} );
203         }
204         $last = $Backups[$i]{startTime}
205                     if ( $last < $Backups[$i]{startTime} );
206         my $badOutlook = 0;
207         my $file = "$TopDir/pc/$host/SmbLOG.$Backups[$i]{num}";
208         my $comp = 0;
209         if ( !-f $file ) {
210             $file = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}";
211             if ( !-f $file ) {
212                 $comp = 1;
213                 $file = "$TopDir/pc/$host/SmbLOG.$Backups[$i]{num}.z";
214                 $file = "$TopDir/pc/$host/XferLOG.$Backups[$i]{num}.z"
215                                                         if ( !-f $file );
216             }
217         }
218         next if ( !defined($fh = BackupPC::FileZIO->open($file, 0, $comp)) );
219         while ( 1 ) {
220             my $s = $fh->readLine();
221             last if ( $s eq "" );
222             if ( $s =~ /^\s*Error reading file.*\.pst : (ERRDOS - ERRlock|NT_STATUS_FILE_LOCK_CONFLICT)/
223                   || $s =~ /^\s*Error reading file.*\.pst\. Got 0 bytes/ ) {
224                 $badOutlook = 1;
225                 last;
226             }
227         }
228         $fh->close();
229         $numBadOutlook += $badOutlook;
230         if ( !$badOutlook ) {
231             $lastGoodOutlook = $Backups[$i]{startTime}
232                     if ( $lastGoodOutlook < $Backups[$i]{startTime} );
233         }
234     }
235     if ( time - $last > $Conf{EMailNotifyOldBackupDays} * 24*3600 ) {
236         my $subj = defined($Conf{EMailNoBackupRecentSubj})
237                         ? $Conf{EMailNoBackupRecentSubj}
238                         : $Lang->{EMailNoBackupRecentSubj};
239         my $mesg = defined($Conf{EMailNoBackupRecentMesg})
240                         ? $Conf{EMailNoBackupRecentMesg}
241                         : $Lang->{EMailNoBackupRecentMesg};
242         my $firstTime = sprintf("%.1f",
243                         (time - $Backups[0]{startTime}) / (24*3600));
244         my $days = sprintf("%.1f", (time - $last) / (24 * 3600));
245         sendUserEmail($user, $host, $mesg, $subj, {
246                             firstTime  => $firstTime,
247                             days       => $days,
248                             userName   => user2name($user),
249                             numBackups => $numBackups,
250                         }) if ( !defined($Jobs{$host}) );
251         next;
252     }
253     if ( $numBadOutlook > 0
254           && time - $lastGoodOutlook > $Conf{EMailNotifyOldOutlookDays}
255                                              * 24 * 3600 ) {
256         my($days, $howLong);
257         if ( $lastGoodOutlook == 0 ) {
258             $howLong = eval("qq{$Lang->{howLong_not_been_backed_up}}");
259         } else {
260             $days = sprintf("%.1f", (time - $lastGoodOutlook) / (24*3600));
261             $howLong = eval("qq{$Lang->{howLong_not_been_backed_up_for_days_days}}");
262         }
263         my $subj = defined($Conf{EMailOutlookBackupSubj})
264                         ? $Conf{EMailOutlookBackupSubj}
265                         : $Lang->{EMailOutlookBackupSubj};
266         my $mesg = defined($Conf{EMailOutlookBackupMesg})
267                         ? $Conf{EMailOutlookBackupMesg}
268                         : $Lang->{EMailOutlookBackupMesg};
269         my $firstTime = sprintf("%.1f",
270                         (time - $Backups[0]{startTime}) / (24*3600));
271         my $lastTime = sprintf("%.1f",
272                         (time - $Backups[$#Backups]{startTime}) / (24*3600));
273         sendUserEmail($user, $host, $mesg, $subj, {
274                             days       => $days,
275                             firstTime  => $firstTime,
276                             lastTime   => $lastTime,
277                             numBackups => $numBackups,
278                             userName   => user2name($user),
279                             howLong    => $howLong,
280                             serverHost => $Conf{ServerHost},
281                         }) if ( !defined($Jobs{$host}) );
282     }
283 }
284 if ( !$opts{t} ) {
285     $Data::Dumper::Indent = 1;
286     my $dumpStr = Data::Dumper->Dump(
287              [\%UserEmailInfo],
288              [qw(*UserEmailInfo)]);
289     if ( open(HOST, ">", "$TopDir/log/UserEmailInfo.pl") ) {
290         binmode(HOST);
291         print(HOST $dumpStr);
292         close(HOST);
293     }
294 }
295
296 sub user2name
297 {
298     my($user) = @_;
299     my($name) = (getpwnam($user))[6];
300     $name =~ s/\s.*//;
301     $name = $user if ( $name eq "" );
302     return $name;
303 }
304
305 sub sendUserEmail
306 {
307     my($user, $host, $mesg, $subj, $vars) = @_;
308     $vars->{user}     = $user;
309     $vars->{host}     = $host;
310     $vars->{headers}  = $Conf{EMailHeaders};
311     $vars->{headers} .= "\n" if ( $vars->{headers} !~ /\n$/ );
312     $vars->{domain}   = $Conf{EMailUserDestDomain};
313     $vars->{CgiURL}   = $Conf{CgiURL};
314     $subj =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
315     $vars->{subj}   = $subj;
316     $mesg =~ s/\$(\w+)/defined($vars->{$1}) ? $vars->{$1} : "\$$1"/eg;
317     if ( $opts{t} ) {
318         print("#" x 75, "\n");
319         print $mesg;
320     } else {
321         SendMail($mesg);
322     }
323     $UserEmailInfo{$user}{lastTime} = time;
324     $UserEmailInfo{$user}{lastSubj} = $subj;
325     $UserEmailInfo{$user}{lastHost} = $host;
326 }
327
328 sub SendMail
329 {
330     my($mesg) = @_;
331     my($from) = $Conf{EMailFromUserName};
332     local(*MAIL);
333
334     $from = "-f $from" if ( $from ne "" );
335     if ( !open(MAIL, "|$Conf{SendmailPath} -t $from") ) {
336         printf("Can't run sendmail ($Conf{SendmailPath}): $!\n");
337         return;
338     }
339     print MAIL $mesg;
340     close(MAIL);
341 }