File Coverage

blib/lib/CPAN/Testers/WWW/Reports/Mailer.pm
Criterion Covered Total %
statement 105 567 18.5
branch 8 266 3.0
condition 1 138 0.7
subroutine 32 53 60.3
pod 4 4 100.0
total 150 1028 14.5


line stmt bran cond sub pod time code
1             package CPAN::Testers::WWW::Reports::Mailer;
2              
3 22     22   430290 use warnings;
  22         34  
  22         769  
4 22     22   79 use strict;
  22         32  
  22         476  
5              
6 22     22   75 use vars qw($VERSION);
  22         28  
  22         1594  
7             $VERSION = '0.36';
8              
9             =head1 NAME
10              
11             CPAN::Testers::WWW::Reports::Mailer - CPAN Testers Reports Mailer
12              
13             =head1 SYNOPSIS
14              
15             use CPAN::Testers::WWW::Reports::Mailer;
16              
17             my $mailer = CPAN::Testers::WWW::Reports::Mailer->new(
18             config => 'myconfig.ini'
19             );
20              
21             $mailer->check_reports();
22             $mailer->check_counts();
23              
24             =head1 DESCRIPTION
25              
26             The CPAN Testers Reports Mailer takes the preferences set within the CPANPREFS
27             database, and uses them to filter out reports that the author does or does not
28             wish to be made aware of.
29              
30             New authors are added to the system as a report for their first reported
31             distribution is submitted by a tester. Default settings are applied in the
32             first instance, with the author able to update these via the preferences
33             website.
34              
35             Initially only a Daily Summary Report is available, in time a Weekly Summary
36             Report and the individual reports will also be available.
37              
38             =head1 CONFIGURATION
39              
40             Configuration for this application can occur via the command line, the API and
41             the configuration file. Of them all, only the configuration file is required.
42              
43             The configuration file should be in the INI style, with the section CPANPREFS
44             describing the associated database access required. The general settings
45             section, SETTINGS, is optional, and can be overridden by the command line and
46             the API arguments.
47              
48             =head2 Database Configuration
49              
50             The CPANPREFS section is required, and should contain the following key/value
51             pairs to describe access to the specific database.
52              
53             =over 4
54              
55             =item * driver
56              
57             =item * database
58              
59             =item * dbhost
60              
61             =item * dbport
62              
63             =item * dbuser
64              
65             =item * dbpass
66              
67             =back
68              
69             Only 'driver' and 'database' are required for an SQLite database, while the
70             other key/values may need to be completed for other databases.
71              
72             It is now assumed that only one database connection is require, with other
73             databases held within the same database application. The primary connection
74             must be to the CPAN Preferences databases. The other databases, CPAN Statistics, Articles and Metabase
75              
76             =head2 General Configuration
77              
78             The following options are available, in the configuration file, on the command
79             line and via the API call to new() as a hash.
80              
81             =over 4
82              
83             =item * mode
84              
85             Processing mode required. This can be one of three values, 'daily', 'weekly' or
86             'reports'. 'daily' and 'weekly' create the mails for the Daily Summary and
87             Weekly Summary reports respectively. 'reports' creates individual report mails
88             for authors.
89              
90             =item * verbose
91              
92             If set to a true value, will print additional log messages.
93              
94             =item * nomail
95              
96             By default this is set to 1, to avoid accidentally running and sending lots of
97             mails :) Set to 0 to allow normal processing.
98              
99             =item * test
100              
101             If used, must be set to a single NNTPID, which will then be tested in isolation
102             for the currently set mode. Automatically sets the nomail flag to true.
103              
104             =item * lastmail
105              
106             The location of the counter file, that stores the ids of the last reports
107             processed.
108              
109             =item * mailrc
110              
111             The location of the 01mailrc.txt file stored locally. By default the location
112             is assumed to be 'data/01mailrc.txt'. If the confirguration is not set, or the
113             file cannot be found, it will be dynamically downloaded from CPAN.
114              
115             =item * logfile
116              
117             The location of the logfile. If not provided, logging is disabled.
118              
119             =item * logclean
120              
121             By default this is set to 0, append to existing log. If set to 1, will create
122             a new log or overwrite any existing log, on the first call to log a message,
123             then will automatically reset to 0, so as to append any further messages.
124              
125             =back
126              
127             =cut
128              
129             # -------------------------------------
130             # Library Modules
131              
132 22     22   12346 use Compress::Zlib;
  22         1062700  
  22         4555  
133 22     22   15202 use Config::IniFiles;
  22         381955  
  22         765  
134 22     22   11562 use CPAN::Testers::Common::DBUtils;
  22         393168  
  22         127  
135 22     22   10324 use CPAN::Testers::Common::Utils qw(guid_to_nntp);
  22         5880  
  22         1319  
136 22     22   8821 use CPAN::Testers::Fact::LegacyReport;
  22         453102  
  22         650  
137 22     22   9464 use CPAN::Testers::Fact::TestSummary;
  22         5576  
  22         485  
138 22     22   11842 use Data::Dumper;
  22         98674  
  22         1362  
139 22     22   10401 use Email::Address;
  22         410468  
  22         1262  
140 22     22   10711 use Email::Simple;
  22         75513  
  22         531  
141 22     22   126 use File::Basename;
  22         27  
  22         1766  
142 22     22   104 use File::Path;
  22         31  
  22         888  
143 22     22   8541 use File::Slurp;
  22         159474  
  22         1493  
144 22     22   13352 use Getopt::ArgvFile default=>1;
  22         94319  
  22         131  
145 22     22   90022 use Getopt::Long;
  22         192881  
  22         144  
146 22     22   4762 use IO::File;
  22         48  
  22         3417  
147 22     22   15792 use JSON;
  22         156739  
  22         115  
148 22     22   15893 use LWP::UserAgent;
  22         692475  
  22         815  
149 22     22   9560 use Math::Random::MT;
  22         18783  
  22         88  
150 22     22   788 use Metabase::Resource;
  22         26  
  22         524  
151 22     22   8649 use MIME::Base64;
  22         10838  
  22         1230  
152 22     22   8016 use MIME::QuotedPrint;
  22         3639  
  22         1009  
153 22     22   7627 use Path::Class;
  22         268009  
  22         1264  
154 22     22   10068 use Parse::CPAN::Authors;
  22         115806  
  22         115  
155 22     22   10403 use Template;
  22         308280  
  22         628  
156 22     22   10262 use Time::Piece;
  22         128674  
  22         94  
157 22     22   8810 use version;
  22         27357  
  22         122  
158 22     22   15155 use WWW::Mechanize;
  22         590917  
  22         940  
159              
160 22     22   179 use base qw(Class::Accessor::Fast);
  22         545  
  22         114905  
161              
162             # -------------------------------------
163             # Variables
164              
165             # default configuration settings
166             my %default = (
167             lastmail => '_lastmail',
168             verbose => 0,
169             nomail => 1,
170             logclean => 0,
171             mode => 'daily',
172             mailrc => 'data/01mailrc.txt'
173             );
174              
175             my $sponsorfile = 'sponsors.json';
176              
177             my (%AUTHORS,%PREFS,@SPONSORS,$MT,$IHEART);
178              
179             my %MODES = (
180             daily => { type => 1, period => '24 hours', report => 'Daily Summary' },
181             weekly => { type => 2, period => '7 days', report => 'Weekly Summary' }, # typically a Saturday
182             reports => { type => 3, period => '', report => 'Test' },
183             monthly => { type => 4, period => 'month', report => 'Monthly Summary' },
184             sun => { type => 5, period => '7 days', report => 'Weekly Summary' },
185             mon => { type => 6, period => '7 days', report => 'Weekly Summary' },
186             tue => { type => 7, period => '7 days', report => 'Weekly Summary' },
187             wed => { type => 8, period => '7 days', report => 'Weekly Summary' },
188             thu => { type => 9, period => '7 days', report => 'Weekly Summary' },
189             fri => { type => 10, period => '7 days', report => 'Weekly Summary' },
190             sat => { type => 11, period => '7 days', report => 'Weekly Summary' },
191             );
192              
193             my $FROM = 'CPAN Tester Report Server ';
194             my $HOW = '/usr/sbin/sendmail -bm';
195             my $HEAD = 'To: "NAME"
196             From: FROM
197             Date: DATE
198             Subject: SUBJECT
199              
200             ';
201              
202             my @dotw = ( "Sunday", "Monday", "Tuesday", "Wednesday",
203             "Thursday", "Friday", "Saturday" );
204              
205             my @months = (
206             { 'id' => 1, 'value' => "January", },
207             { 'id' => 2, 'value' => "February", },
208             { 'id' => 3, 'value' => "March", },
209             { 'id' => 4, 'value' => "April", },
210             { 'id' => 5, 'value' => "May", },
211             { 'id' => 6, 'value' => "June", },
212             { 'id' => 7, 'value' => "July", },
213             { 'id' => 8, 'value' => "August", },
214             { 'id' => 9, 'value' => "September", },
215             { 'id' => 10, 'value' => "October", },
216             { 'id' => 11, 'value' => "November", },
217             { 'id' => 12, 'value' => "December" },
218             );
219              
220             our %phrasebook = (
221             'LastReport' => "SELECT MAX(id) FROM cpanstats.cpanstats",
222             'GetEarliest' => "SELECT id FROM cpanstats.cpanstats WHERE fulldate > ? ORDER BY id LIMIT 1",
223              
224             'FindAuthorType' => "SELECT pauseid FROM prefs_distributions WHERE report = ?",
225              
226             'GetReports' => "SELECT id,guid,dist,version,platform,perl,state FROM cpanstats.cpanstats WHERE id > ? AND state IN ('pass','fail','na','unknown') ORDER BY id",
227             'GetReports2' => "SELECT c.id,c.guid,c.dist,c.version,c.platform,c.perl,c.state FROM cpanstats.cpanstats AS c INNER JOIN cpanstats.ixlatest AS x ON x.dist=c.dist WHERE c.id > ? AND c.state IN ('pass','fail','na','unknown') AND author IN (%s) ORDER BY c.id",
228             'GetReportCount' => "SELECT id FROM cpanstats.cpanstats WHERE platform=? AND perl=? AND state=? AND id < ? AND dist=? AND version=? LIMIT 2",
229             'GetLatestDistVers' => "SELECT version FROM cpanstats.uploads WHERE dist=? ORDER BY released DESC LIMIT 1",
230             'GetAuthor' => "SELECT author FROM cpanstats.uploads WHERE dist=? AND version=? LIMIT 1",
231             'GetAuthors' => "SELECT author,dist,version FROM cpanstats.uploads",
232              
233             'GetAuthorPrefs' => "SELECT * FROM prefs_authors WHERE pauseid=?",
234             'GetDefaultPrefs' => "SELECT * FROM prefs_authors AS a INNER JOIN prefs_distributions AS d ON d.pauseid=a.pauseid AND d.distribution='-' WHERE a.pauseid=?",
235             'GetDistPrefs' => "SELECT * FROM prefs_distributions WHERE pauseid=? AND distribution=?",
236             'InsertAuthorLogin' => 'INSERT INTO prefs_authors (active,lastlogin,pauseid) VALUES (1,?,?)',
237             'InsertDistPrefs' => "INSERT INTO prefs_distributions (pauseid,distribution,ignored,report,grade,tuple,version,patches,perl,platform) VALUES (?,?,0,1,'FAIL','FIRST','LATEST',0,'ALL','ALL')",
238              
239             'GetArticle' => "SELECT * FROM articles.articles WHERE id=?",
240              
241             'GetReportTest' => "SELECT id,guid,dist,version,platform,perl,state FROM cpanstats.cpanstats WHERE id = ? AND state IN ('pass','fail','na','unknown') ORDER BY id",
242              
243             'GetMetabaseByGUID' => 'SELECT * FROM metabase.metabase WHERE guid=?',
244             'GetTestersEmail' => 'SELECT * FROM metabase.testers_email',
245             'GetTesters' => 'SELECT * FROM metabase.testers_email ORDER BY id'
246             );
247              
248             #----------------------------------------------------------------------------
249             # The Application Programming Interface
250              
251             __PACKAGE__->mk_accessors(
252             qw( lastmail verbose nomail test logfile logclean mode mailrc tt pause ));
253              
254             # -------------------------------------
255             # The Public Interface Functions
256              
257             sub new {
258 2     2 1 2843 my $class = shift;
259 2         6 my %hash = @_;
260              
261 2         2 my $self = {};
262 2         3 bless $self, $class;
263              
264 2         1 my %options;
265 2 50       9 GetOptions( \%options,
266             'config=s',
267             'lastmail=s',
268             'mailrc=s',
269             'test=i',
270             'logfile=s',
271             'logclean',
272             'verbose',
273             'nomail',
274             'mode=s',
275             'help|h',
276             'version|v'
277             ) or help(1);
278              
279             # default to API settings if no command line option
280 2         788 for(qw(config help version)) {
281 6 100 33     18 $options{$_} ||= $hash{$_} if(defined $hash{$_});
282             }
283              
284 2 50       5 $self->help(1) if($options{help});
285 2 50       5 $self->help(0) if($options{version});
286              
287             # ensure we have a configuration file
288 2 100       21 die "Must specify a configuration file\n" unless( $options{config});
289 1 50       23 die "Configuration file [$options{config}] not found\n" unless(-f $options{config});
290              
291             # load configuration
292 0           my $cfg = Config::IniFiles->new( -file => $options{config} );
293              
294             # configure databases
295 0           for my $db (qw(CPANPREFS)) {
296 0 0         die "No configuration for $db database\n" unless($cfg->SectionExists($db));
297 0           my %opts;
298 0           for my $key (qw(driver database dbfile dbhost dbport dbuser dbpass)) {
299 0           my $val = $cfg->val($db,$key);
300 0 0         $opts{$key} = $val if(defined $val);
301             }
302 0           $self->{$db} = CPAN::Testers::Common::DBUtils->new(%opts);
303 0 0         die "Cannot configure $db database\n" unless($self->{$db});
304 0 0         $self->{db}->{mysql_auto_reconnect} = 1 if($opts{driver} =~ /mysql/i);
305             }
306              
307 0           $self->test( $self->_defined_or( $options{test}, $hash{test}, $cfg->val('SETTINGS','test' ), 0 ) );
308 0 0         $options{nomail} = 1 if($self->test);
309              
310 0           $self->verbose( $self->_defined_or( $options{verbose}, $hash{verbose}, $cfg->val('SETTINGS','verbose' ), $default{verbose}) );
311 0           $self->nomail( $self->_defined_or( $options{nomail}, $hash{nomail}, $cfg->val('SETTINGS','nomail' ), $default{nomail}) );
312 0           $self->lastmail($self->_defined_or( $options{lastmail}, $hash{lastmail}, $cfg->val('SETTINGS','lastmail' ), $default{lastmail}) );
313 0           $self->mailrc( $self->_defined_or( $options{mailrc}, $hash{mailrc}, $cfg->val('SETTINGS','mailrc' ), $default{mailrc} ) );
314 0           $self->logfile( $self->_defined_or( $options{logfile}, $hash{logfile}, $cfg->val('SETTINGS','logfile' ) ) );
315 0           $self->logclean($self->_defined_or( $options{logclean}, $hash{logclean}, $cfg->val('SETTINGS','logclean' ), $default{logclean} ) );
316 0           $self->mode(lc $self->_defined_or( $options{mode}, $hash{mode}, $cfg->val('SETTINGS','mode' ), $default{mode} ) );
317              
318 0           $IHEART = $cfg->val('SETTINGS','iheart_random' );
319              
320 0           my $mode = $self->mode;
321 0 0         if($mode =~ /day/) {
322 0           $mode = substr($mode,0,3);
323 0           $self->mode($mode);
324             }
325              
326 0 0         unless($mode =~ /^(daily|weekly|reports|monthly|sun|mon|tue|wed|thu|fri|sat)$/) {
327 0           die "mode can MUST be 'daily', 'weekly', 'monthly', 'reports', or a day of the week.\n";
328             }
329              
330 0           $self->pause($self->_download_mailrc());
331              
332             # set up API to Template Toolkit
333 0           $self->tt( Template->new(
334             {
335             EVAL_PERL => 1,
336             INCLUDE_PATH => [ 'templates' ],
337             }
338             ));
339              
340 0           my @testers = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetTestersEmail'});
341 0           for my $tester (@testers) {
342 0   0       $self->{testers}{$tester->{creator}}{name} ||= $tester->{fullname};
343 0   0       $self->{testers}{$tester->{creator}}{email} ||= $tester->{email};
344             }
345              
346 0           $self->_load_authors();
347 0           $self->_load_testers();
348 0           $self->_load_sponsors();
349              
350 0           return $self;
351             }
352              
353             sub check_reports {
354 0     0 1   my $self = shift;
355 0           my $mode = $self->mode;
356 0           my $report_type = $MODES{$mode}->{type};
357 0           my $last_id = int( $self->_get_lastid() );
358 0           my (%reports,%tvars);
359              
360 0           $self->_log( "INFO: START checking reports in '$mode' mode\n" );
361 0           $self->_log( "INFO: last_id=$last_id\n" );
362              
363 0           my $next;
364 0 0         if($self->test) {
    0          
365 0           $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetReportTest'},$self->test);
366             } elsif($mode ne 'daily') {
367 0           my @authors = $self->{CPANPREFS}->get_query('hash',$phrasebook{'FindAuthorType'}, $report_type);
368 0 0         return $self->_set_lastid() unless(@authors);
369 0           my $sql = sprintf $phrasebook{'GetReports2'}, join(',',map {"'$_->{pauseid}'"} @authors);
  0            
370 0           $next = $self->{CPANPREFS}->iterator('hash',$sql,$last_id);
371             } else {
372             # find all reports since last update
373 0           $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetReports'},$last_id);
374 0 0         unless($next) {
375 0           $self->_log( "INFO: STOP checking reports\n" );
376 0           return;
377             }
378             }
379              
380 0           my $rows = 0;
381 0           while( my $row = $next->()) {
382 0           $rows++;
383 0 0         $self->_log( "DEBUG: processing report: $row->{id}\n" ) if($self->verbose);
384              
385 0           $self->{counts}{REPORTS}++;
386 0           $last_id = $row->{id};
387 0           $row->{state} = uc $row->{state};
388 0           $self->{counts}{$row->{state}}++;
389              
390 0 0         $self->_log( "DEBUG: dist: $row->{dist} $row->{version} $row->{state}\n" ) if($self->verbose);
391              
392 0           my $author = $self->_get_author($row->{dist}, $row->{version});
393 0 0 0       $self->_log( "DEBUG: author: ".($author||'')."\n" ) if($self->verbose);
394 0 0         next unless($author);
395              
396 0 0         unless($author) {
397 0           $self->_log( "WARN: author not found for distribution [$row->{dist}], [$row->{version}]\n" );
398 0           next;
399             }
400              
401 0   0       $row->{version} ||= '';
402 0   0       $row->{platform} ||= '';
403 0   0       $row->{perl} ||= '';
404              
405             # get author preferences
406 0   0       my $prefs = $self->_get_prefs($author) || next;
407              
408             # do we need to worry about this author?
409 0 0         if($prefs->{active} == 2) {
410 0           $self->{counts}{NOMAIL}++;
411 0 0         $self->_log( "DEBUG: author: $author - not active\n" ) if($self->verbose);
412 0           next;
413             }
414              
415             # get distribution preferences
416 0           $prefs = $self->_get_prefs($author, $row->{dist});
417 0 0         $self->_log( "DEBUG: dist prefs: " .($prefs ? 'Found' : 'Not Found')."\n" ) if($self->verbose);
    0          
418 0 0         next unless($prefs);
419 0 0 0       $self->_log( "DEBUG: dist prefs: ignored=" .($prefs->{ignored} || 0)."\n" ) if($self->verbose);
420 0 0         next if($prefs->{ignored});
421 0 0         $self->_log( "DEBUG: dist prefs: report=$prefs->{report}, report type=$report_type\n" ) if($self->verbose);
422 0 0         next if($prefs->{report} != $report_type);
423 0 0 0       $self->_log( "DEBUG: dist prefs: $row->{state}=" .($prefs->{grades}{$row->{state}}||'undef')."\n" ) if($self->verbose);
424 0 0 0       $self->_log( "DEBUG: dist prefs: ALL=" .($prefs->{grades}{ALL}||'undef')."\n" ) if($self->verbose);
425 0 0 0       next unless($prefs->{grades}{$row->{state}} || $prefs->{grades}{'ALL'});
426 0 0         $self->_log( "DEBUG: dist prefs: CONTINUE\n" ) if($self->verbose);
427              
428             # Check whether distribution version is required.
429             # If version set to 'LATEST' check this is the current version, if set
430             # to 'ALL' then we should allow EVERYTHING through, otherwise filter
431             # on the requested versions.
432              
433 0 0 0       if($row->{version} && $prefs->{version} && $prefs->{version} ne 'ALL') {
      0        
434 0 0         if($prefs->{version} eq 'LATEST') {
435 0           my @vers = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetLatestDistVers'},$row->{dist});
436 0 0         $self->_log( "DEBUG: dist prefs: vers=".(scalar(@vers))."\n" ) if($self->verbose);
437 0 0         $self->_log( "DEBUG: dist prefs: version=$vers[0]->[0], $row->{version}\n" ) if($self->verbose);
438 0 0 0       next if(@vers && $vers[0]->[0] ne $row->{version});
439             } else {
440 0           $prefs->{version} =~ s/\s*//g;
441 0           my %m = map {$_ => 1} split(',',$prefs->{version});
  0            
442 0 0         $self->_log( "DEBUG: dist prefs: $row->{version}\n" ) if($self->verbose);
443 0 0         next unless($m{$row->{version}});
444             }
445             }
446              
447             # Check whether this platform is required.
448 0 0 0       if($row->{platform} && $prefs->{platform} && $prefs->{platform} ne 'ALL') {
      0        
449 0           $prefs->{platform} =~ s/\s*//g;
450 0           $prefs->{platform} =~ s/,/|/g;
451 0           $prefs->{platform} =~ s/\./\\./g;
452 0           $prefs->{platform} =~ s/^(\w+)\|//;
453 0 0 0       if($1 && $1 eq 'NOT') {
454 0 0         $self->_log( "DEBUG: dist prefs: $row->{platform}, =~ $prefs->{platform}\n" ) if($self->verbose);
455 0 0         next if($row->{platform} =~ /$prefs->{platform}/);
456             } else {
457 0 0         $self->_log( "DEBUG: dist prefs: $row->{platform}, !~ $prefs->{platform}\n" ) if($self->verbose);
458 0 0         next if($row->{platform} !~ /$prefs->{platform}/);
459             }
460             }
461              
462             # Check whether this perl version is required.
463 0 0 0       if($row->{perl} && $prefs->{perl} && $prefs->{perl} ne 'ALL') {
      0        
464 0           my $perlv = $row->{perl};
465 0           $perlv = $row->{perl};
466 0           $perlv =~ s/\s.*//;
467              
468 0           $prefs->{perl} =~ s/\s*//g;
469 0           $prefs->{perl} =~ s/,/|/g;
470 0           $prefs->{perl} =~ s/\./\\./g;
471 0           my $v = version->new("$perlv")->numify;
472 0           $prefs->{platform} =~ s/^(\w+)\|//;
473 0 0 0       if($1 && $1 eq 'NOT') {
474 0 0         $self->_log( "DEBUG: dist prefs: $perlv || $v =~ $prefs->{perl}\n" ) if($self->verbose);
475 0 0 0       next if($perlv =~ /$prefs->{perl}/ && $v =~ /$prefs->{perl}/);
476             } else {
477 0 0         $self->_log( "DEBUG: dist prefs: $perlv || $v !~ $prefs->{perl}\n" ) if($self->verbose);
478 0 0 0       next if($perlv !~ /$prefs->{perl}/ && $v !~ /$prefs->{perl}/);
479             }
480             }
481              
482             # Check whether patches are required.
483 0 0         $self->_log( "DEBUG: dist prefs: patches=$prefs->{patches}, row perl $row->{perl}\n" ) if($self->verbose);
484 0 0 0       next if(!$prefs->{patches} && $row->{perl} =~ /(RC\d+|patch)/);
485              
486             # check whether only first instance required
487 0 0         if($prefs->{tuple} eq 'FIRST') {
488             my @count = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetReportCount'},
489 0           $row->{platform}, $row->{perl}, $row->{state}, $row->{id}, $row->{dist}, $row->{version});
490 0 0         $self->_log( "DEBUG: dist prefs: tuple=FIRST, count=".(scalar(@count))."\n" ) if($self->verbose);
491 0 0         next if(@count > 0);
492             }
493              
494 0 0         $self->_log( "DEBUG: report is being added to mailshot\n" ) if($self->verbose);
495              
496 0 0         if($mode eq 'reports') {
497 0           $self->_send_report($author,$row);
498             }
499              
500 0   0       push @{$reports{$author}->{dists}{$row->{dist}}->{versions}{$row->{version}}->{platforms}{$row->{platform}}->{perls}{$row->{perl}}->{states}{uc $row->{state}}->{value}}, ($row->{guid} || $row->{id});
  0            
501             }
502              
503 0           $self->_log( "INFO: STOP checking reports in '$mode' mode\n" );
504              
505 0 0         return $self->_set_lastid() unless($rows);
506              
507 0 0         if($mode ne 'reports') {
508 0           $self->_log( "INFO: START parsing data in '$mode' mode\n" );
509 0 0         $self->_log( "DEBUG: processing authors: ".(scalar(keys %reports))."\n" ) if($self->verbose);
510              
511 0           for my $author (sort keys %reports) {
512 0 0         $self->_log( "DEBUG: $author\n" ) if($self->verbose);
513              
514 0           my $pause = $self->pause->author($author);
515 0 0         $tvars{name} = $pause ? $pause->name : $author;
516 0           $tvars{author} = $author;
517 0           $tvars{dists} = ();
518              
519             # get author preferences
520 0           my $prefs = $self->_get_prefs($author);
521              
522             # active:
523             # 0 - new author, no correspondance
524             # 1 - new author, notification mailed
525             # 2 - author requested no mail
526             # 3 - author requested summary report
527              
528 0 0 0       if(!$prefs->{active} || $prefs->{active} == 0) {
529 0           $tvars{subject} = 'Welcome to CPAN Testers';
530 0           $self->_write_mail('notification.eml',\%tvars);
531 0           $self->{counts}{NEWAUTH}++;
532              
533             # insert author defaults, however check that they don't already
534             # exists in the system first, in case entries are out of sync.
535 0           my @auth = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetAuthorPrefs'}, $author);
536 0 0         $self->{CPANPREFS}->do_query($phrasebook{'InsertAuthorLogin'}, time(), $author) unless(@auth);
537 0           my @dist = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetDistPrefs'}, $author,'-');
538 0 0         $self->{CPANPREFS}->do_query($phrasebook{'InsertDistPrefs'}, $author, '-') unless(@dist);
539             }
540              
541 0 0         $self->_log( "DEBUG: $author - distributions = ".(scalar(keys %{$reports{$author}->{dists}}))."\n" ) if($self->verbose);
  0            
542              
543 0           my ($reports,@e);
544 0           for my $dist (sort keys %{$reports{$author}->{dists}}) {
  0            
545 0           my $v = $reports{$author}->{dists}{$dist};
546 0           my @d;
547 0           for my $version (sort keys %{$v->{versions}}) {
  0            
548 0           my $w = $v->{versions}{$version};
549 0           my @c;
550 0           for my $platform (sort keys %{$w->{platforms}}) {
  0            
551 0           my $x = $w->{platforms}{$platform};
552 0           my @b;
553 0           for my $perl (sort keys %{$x->{perls}}) {
  0            
554 0           my $y = $x->{perls}{$perl};
555 0           my @a;
556 0           for my $state (sort keys %{$y->{states}}) {
  0            
557 0           my $z = $y->{states}{$state};
558 0           push @a, {state => $state, ids => $z->{value}};
559 0           $reports++;
560             }
561 0           push @b, {perl => $perl, states => \@a};
562             }
563 0           push @c, {platform => $platform, perls => \@b};
564             }
565 0           push @d, {version => $version, platforms => \@c};
566             }
567 0           push @e, {dist => $dist, versions => \@d};
568             }
569              
570 0 0         next unless($reports);
571 0 0         if($self->verbose) { $self->_log( "DEBUG: $author - reports = $reports\n" ) }
  0            
572 0           else { $self->_log( "INFO: $author - dists=".(scalar(keys %{$reports{$author}->{dists}})).", reports=$reports\n" ) }
  0            
573              
574 0           $tvars{dists} = \@e;
575 0           $tvars{period} = $MODES{$mode}->{period};
576 0           $tvars{report} = $MODES{$mode}->{report};
577 0           $tvars{subject} = "CPAN Testers $tvars{report} Report";
578              
579 0           $self->_write_mail('mailer.eml',\%tvars);
580             }
581              
582 0           $self->_log( "INFO: STOP parsing data in '$mode' mode\n" );
583             }
584              
585 0           $self->_set_lastid($last_id);
586             }
587              
588             sub check_counts {
589 0     0 1   my $self = shift;
590 0           my $mode = $self->mode;
591              
592 0           $self->_log( "INFO: COUNTS for '$mode' mode:\n" );
593 0           my @counts = qw(REPORTS PASS FAIL UNKNOWN NA NOMAIL MAILS NEWAUTH GOOD BAD);
594 0 0         push @counts, 'TEST' if($self->nomail);
595              
596 0           for(@counts) {
597 0   0       $self->{counts}{$_} ||= 0;
598 0           $self->_log( sprintf "INFO: %7s = %6d\n", $_, $self->{counts}{$_} );
599             }
600             }
601              
602             sub help {
603 0     0 1   my ($self,$full) = @_;
604              
605 0 0         if($full) {
606 0           print <
607              
608             Usage: $0 --config= \\
609             [--logfile= [--logclean]] [--verbose] [--nomail] \\
610             [--test=] [--lastmail=] \\
611             [--mode=(daily|weekly|report|monthly|sun|mon|tue|wed|thu|fri|sat)] \\
612             [-h] [-v]
613              
614             --config= database configuration file
615             --logfile= log file (*)
616             --logclean 0 = append, 1 = overwrite (*)
617             --verbose print additional log messages
618             --nomail nomail flag, no mail sent if true (*)
619             --test= test an id in debug mode, no mail sent (*)
620             --lastmail= lastmail counter file (*)
621             --mode run mode (*)
622             -h this help screen
623             -v program version
624              
625             NOTES:
626             * - these will override any settings within the configuration file.
627             HERE
628              
629             }
630              
631 0           print "$0 v$VERSION\n";
632 0           exit(0);
633             }
634              
635             #----------------------------------------------------------------------------
636             # Internal Methods
637              
638             sub _get_lastid {
639 0     0     my ($self,$id) = @_;
640 0           my $mode = $self->mode;
641              
642 0 0         unless( -f $self->lastmail ) {
643 0           mkpath(dirname($self->lastmail));
644 0           overwrite_file( $self->lastmail, 'daily=0,weekly=0,reports=0' );
645             }
646              
647 0 0         if (defined $id) {
648 0           my $text = read_file($self->lastmail);
649 0 0         if($text =~ m!$mode=\d+!) {
650 0           $text =~ s!($mode=)\d+!$1$id!;
651             } else {
652 0           $text .= ",$mode=$id"; # auto add mode
653             }
654 0           $text =~ s/\s+//g;
655 0           overwrite_file( $self->lastmail, $text );
656 0           return $id;
657             }
658              
659 0           my $text = read_file($self->lastmail);
660 0 0         return $id if(($id) = $text =~ m!$mode=(\d+)!);
661 0           return $self->_get_earliest(); # mode not found, find earliest id based on mode
662             }
663              
664             sub _set_lastid {
665 0     0     my ($self,$id) = @_;
666              
667 0 0         if(!defined $id) {
668 0           my @lastid = $self->{CPANPREFS}->get_query('array',$phrasebook{'LastReport'});
669 0 0         $id = @lastid ? $lastid[0]->[0] : 0;
670             }
671              
672 0           $self->_log( "INFO: new last_id=$id\n" );
673 0           $self->_log( "INFO: STOP checking reports\n" );
674              
675 0 0         return $id if($self->nomail);
676              
677 0           $self->_get_lastid($id);
678             }
679              
680             sub _get_earliest {
681 0     0     my $self = shift;
682 0           my $mode = $self->mode;
683              
684 0           my @date = localtime(time);
685 0           $date[5] += 1900;
686 0           $date[4] += 1;
687 0 0 0       if($mode eq 'monthly') {
    0          
688 0           $date[4] -= 1;
689 0           $date[3] = 1;
690             } elsif($mode eq 'daily' || $mode eq 'reports') {
691 0           $date[3] -= 1;
692             } else {
693 0           $date[3] -=7;
694             }
695              
696 0 0         if($date[3] < 1) {
697 0           $date[4] -= 1;
698 0 0 0       if($date[4] == 2 && $date[5] % 4) {
    0 0        
    0 0        
      0        
699 0           $date[3] = 28 - $date[3];
700             } elsif($date[3] == 2) {
701 0           $date[3] = 29 - $date[3];
702             } elsif($date[3] == 4 || $date[3] == 6 || $date[3] == 9 || $date[3] == 11) {
703 0           $date[3] = 30 - $date[3];
704             } else {
705 0           $date[3] = 31 - $date[3];
706             }
707 0 0         if($date[4] < 1) {
708 0           $date[4] = 12;
709 0           $date[5] -= 1;
710             }
711             }
712              
713 0           my $fulldate = sprintf "%04d%02d%02d000000", $date[5], $date[4], $date[3];
714 0           my @report = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetEarliest'}, $fulldate);
715 0 0         return 0 unless(@report);
716 0   0       return $report[0]->[0] || 0;
717             }
718              
719             sub _get_prefs {
720 0     0     my $self = shift;
721 0           my ($author,$dist) = @_;
722 0           my $active = 0;
723              
724 0 0         return unless($author);
725              
726             # get distribution defaults
727 0 0 0       if($author && $dist) {
728 0 0         if(defined $PREFS{$author}{dists}{$dist}) {
729 0           return $PREFS{$author}{dists}{$dist};
730             }
731              
732 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetDistPrefs'}, $author,$dist);
733 0 0         if(@rows) {
734 0           $PREFS{$author}{dists}{$dist} = $self->_parse_prefs($rows[0]);
735 0           return $PREFS{$author}{dists}{$dist};
736             }
737              
738             # fall through and assume author defaults
739             }
740              
741             # get author defaults
742 0 0         if($author) {
743 0 0         if(defined $PREFS{$author}{default}) {
744 0           return $PREFS{$author}{default};
745             }
746              
747 0           my @auth = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetAuthorPrefs'}, $author);
748 0 0         if(@auth) {
749 0   0       $PREFS{$author}{default}{active} = $auth[0]->{active} || 0;
750              
751 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetDefaultPrefs'}, $author);
752 0 0         if(@rows) {
753 0           $PREFS{$author}{default} = $self->_parse_prefs($rows[0]);
754 0   0       $PREFS{$author}{default}{active} = $rows[0]->{active} || 0;
755 0           return $PREFS{$author}{default};
756             } else {
757 0           $self->{CPANPREFS}->do_query($phrasebook{'InsertDistPrefs'}, $author, '-');
758 0           $active = $PREFS{$author}{default}{active};
759             }
760             }
761              
762             # fall through and assume new author
763             }
764              
765 0   0       $dist ||= '-';
766              
767             # use global defaults
768 0           my %prefs = (
769             active => $active,
770             ignored => 0,
771             report => 1,
772             grades => {'FAIL' => 1},
773             tuple => 'FIRST',
774             version => 'LATEST',
775             patches => 0,
776             perl => 'ALL',
777             platform => 'ALL',
778             );
779 0           $PREFS{$author}{dists}{$dist} = \%prefs;
780 0           return \%prefs;
781             }
782              
783             sub _parse_prefs {
784 0     0     my ($self,$row) = @_;
785 0           my %hash;
786              
787 0   0       $row->{grade} ||= 'FAIL';
788 0           my %grades = map {$_ => 1} split(',',$row->{grade});
  0            
789              
790 0           $hash{grades} = \%grades;
791 0           $hash{ignored} = $self->_defined_or($row->{ignored}, 0);
792 0           $hash{report} = $self->_defined_or($row->{report}, 1);
793 0           $hash{tuple} = $self->_defined_or($row->{tuple}, 'FIRST');
794 0           $hash{version} = $self->_defined_or($row->{version}, 'LATEST');
795 0           $hash{patches} = $self->_defined_or($row->{patches}, 0);
796 0           $hash{perl} = $self->_defined_or($row->{perl}, 'ALL');
797 0           $hash{platform} = $self->_defined_or($row->{platform}, 'ALL');
798              
799 0           return \%hash;
800             }
801              
802             sub _send_report {
803 0     0     my ($self,$author,$row) = @_;
804 0           my %tvars;
805              
806 0           my $nntpid = guid_to_nntp($row->{guid});
807              
808             # old NNTP article lookup
809 0 0         if($nntpid) {
810             # get article
811 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetArticle'}, $nntpid);
812              
813             #$self->_log( "ARTICLE: $nntpid: $rows[0]->{article}\n" );
814              
815             # disassemble article
816 0 0         $rows[0]->{article} = decode_qp($rows[0]->{article}) if($rows[0]->{article} =~ /=3D/);
817 0           my $mail = Email::Simple->new($rows[0]->{article});
818 0 0         return unless $mail;
819              
820             # get from & subject line
821 0           my $from = $mail->header("From");
822 0           my $subject = $mail->header("Subject");
823 0 0         return unless $subject;
824              
825 0           my ($address) = Email::Address->parse($from);
826 0           my $reply = sprintf "%s\@%s", $address->user, $address->host;
827              
828             # extract the body
829 0           my $encoding = $mail->header('Content-Transfer-Encoding');
830 0           my $body = $mail->body;
831 0 0 0       $body = decode_base64($body) if($encoding && $encoding eq 'base64');
832              
833             # set up new mail headers
834 0           my $pause = $self->pause->author($author);
835 0 0         %tvars = (
836             author => $author,
837             name => ($pause ? $pause->name : $author),
838             subject => $subject,
839             from => $reply,
840             body => $body,
841             reply => $reply
842             );
843              
844             # new Metabase lookup
845             } else {
846 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetMetabaseByGUID'},$row->{guid});
847 0 0         return unless(@rows);
848              
849 0           my $data = eval { decode_json($rows[0]->{report}) };
  0            
850 0 0         if ( $@ ) {
851 0           $self->_log( "WARN: Bad JSON in metabase report $row->{guid}\n" );
852 0           return;
853             }
854            
855 0           my $fact = CPAN::Testers::Fact::LegacyReport->from_struct( $data->{'CPAN::Testers::Fact::LegacyReport'} );
856 0           my $body = $fact->{content}{textreport};
857              
858 0           my $report = CPAN::Testers::Fact::TestSummary->from_struct( $data->{'CPAN::Testers::Fact::TestSummary'} );
859 0           my $state = uc $report->{content}{grade};
860 0           my $osname = $report->{content}{osname};
861 0           my $perl = $report->{content}{perl_version};
862              
863 0           my $distro = Metabase::Resource->new( $report->{metadata}{core}{resource} );
864 0           my $dist = $distro->metadata->{dist_name};
865 0           my $version = $distro->metadata->{dist_version};
866 0           my $author2 = $distro->metadata->{author};
867              
868 0           my ($tester_name,$tester_email) = $self->_get_tester( $report->creator );
869              
870 0           my $subject = sprintf "%s %s-%s %s %s", $state, $dist, $version, $perl, $osname;
871              
872             # set up new mail headers
873 0 0         my $pause = $author2 ? $self->pause->author($author2) : $self->pause->author($author);
874 0 0         %tvars = (
875             author => $author,
876             name => ($pause ? $pause->name : $author),
877             subject => $subject,
878             from => $tester_email,
879             body => $body,
880             reply => $tester_email
881             );
882             }
883              
884             # send data
885 0           $self->_write_mail('report.eml',\%tvars);
886             }
887              
888             sub _write_mail {
889 0     0     my ($self,$template,$parms) = @_;
890              
891 0 0         unless($parms->{author}) {
892 0           $self->_log( "INFO: BAD: $parms->{author} [$parms->{name}]\n" );
893 0           $self->{counts}{BAD}++;
894 0           return;
895             }
896              
897 0   0       my $from = $parms->{from} || $FROM;
898 0   0       my $subject = $parms->{subject} || 'CPAN Testers Daily Reports';
899 0           my $cmd = qq!| $HOW $parms->{author}\@cpan.org!;
900              
901 0           $self->{counts}{MAILS}++;
902              
903 0           my $DATE = $self->_emaildate();
904 0           $DATE =~ s/\s+$//;
905              
906 0           my $sponsor = $self->_get_sponsor();
907 0           $self->_log( "INFO: Get Sponsor: ".Dumper($sponsor)."\n" );
908 0           $parms->{SPONSOR_CATEGORY} = $sponsor->{category};
909 0           $parms->{SPONSOR_NAME} = $sponsor->{title};
910 0           $parms->{SPONSOR_BODY} = $sponsor->{body};
911 0           $parms->{SPONSOR_HREF} = $sponsor->{href};
912 0           $parms->{SPONSOR_URL} = $sponsor->{url};
913              
914 0           my $text;
915 0 0         $self->tt->process( $template, $parms, \$text ) || die $self->tt->error;
916              
917 0   0       $parms->{name} ||= $parms->{author};
918              
919 0           my $body;
920 0 0         $body = "Reply-To: $parms->{reply}\n" if($parms->{reply});
921 0           $body .= $HEAD . $text;
922 0           $body =~ s/FROM/$from/g;
923 0           $body =~ s/NAME/$parms->{name}/g;
924 0           $body =~ s/EMAIL/$parms->{author}\@cpan.org/g;
925 0           $body =~ s/DATE/$DATE/g;
926 0           $body =~ s/SUBJECT/$subject/g;
927              
928 0 0         if($self->nomail) {
    0          
929 0           $self->_log( "INFO: TEST: $parms->{author}\n" );
930 0           $self->{counts}{TEST}++;
931 0 0         my $fh = IO::File->new('mailer-debug.log','a+') or die "Cannot write to debug file [mailer-debug.log]: $!\n";
932 0           print $fh $body;
933 0           $fh->close;
934            
935             } elsif(my $fh = IO::File->new($cmd)) {
936 0           print $fh $body;
937 0           $fh->close;
938 0           $self->_log( "INFO: GOOD: $parms->{author}\n" );
939 0           $self->{counts}{GOOD}++;
940              
941             } else {
942 0           $self->_log( "INFO: BAD: $parms->{author}\n" );
943 0           $self->{counts}{BAD}++;
944             }
945             }
946              
947             sub _emaildate {
948 0     0     my $self = shift;
949 0           my $t = localtime;
950 0           return $t->strftime("%a, %d %b %Y %H:%M:%S +0000");
951             }
952              
953             sub _download_mailrc {
954 0     0     my $self = shift;
955 0           my $file = $self->mailrc;
956 0           my $data;
957              
958 0 0 0       if($file && -f $file) {
959 0           $data = read_file($file);
960              
961             } else {
962 0           my $url = 'http://www.cpan.org/authors/01mailrc.txt.gz';
963 0           my $ua = LWP::UserAgent->new;
964 0           $ua->timeout(180);
965 0           my $response = $ua->get($url);
966              
967 0 0         if ($response->is_success) {
968 0           my $gzipped = $response->content;
969 0           $data = Compress::Zlib::memGunzip($gzipped);
970 0 0         die "Error uncompressing data from $url" unless $data;
971             } else {
972 0           die "Error fetching $url";
973             }
974             }
975              
976 0           my $p = Parse::CPAN::Authors->new($data);
977 0 0         die "Cannot parse data from 01mailrc.txt" unless($p);
978 0           return $p;
979             }
980              
981             sub _load_testers {
982 0     0     my $self = shift;
983 0           my $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetTesters'});
984 0           while(my $row = $next->()) {
985 0   0       $self->{testers}{$row->{resource}}{name} ||= $row->{fullname};
986 0   0       $self->{testers}{$row->{resource}}{email} ||= $row->{email};
987             }
988             }
989              
990             sub _get_tester {
991 0     0     my ($self,$creator) = @_;
992              
993 0 0 0       return unless($creator && $self->{testers}{$creator});
994 0           return $self->{testers}{$creator}{name},$self->{testers}{$creator}{email};
995             }
996              
997             sub _load_authors {
998 0     0     my $self = shift;
999 0           my $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetAuthors'});
1000 0           while(my $row = $next->()) {
1001 0           $AUTHORS{$row->{dist}}{$row->{version}} = $row->{author};
1002             }
1003             }
1004              
1005             sub _get_author {
1006 0     0     my ($self,$dist,$vers) = @_;
1007 0 0 0       return unless($dist && $vers);
1008 0           return $AUTHORS{$dist}{$vers};
1009             }
1010              
1011             sub _get_authorX {
1012 0     0     my $self = shift;
1013 0           my ($dist,$vers) = @_;
1014 0 0 0       return unless($dist && $vers);
1015              
1016 0 0 0       unless($AUTHORS{$dist} && $AUTHORS{$dist}{$vers}) {
1017 0           my @author = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetAuthor'}, $dist, $vers);
1018 0 0         $AUTHORS{$dist}{$vers} = @author ? $author[0]->[0] : undef;
1019             }
1020 0           return $AUTHORS{$dist}{$vers};
1021             }
1022              
1023             sub _load_sponsors {
1024 0     0     my $self = shift;
1025 0           my $json;
1026              
1027 0           my $mech = WWW::Mechanize->new();
1028 0           $mech->agent_alias( 'Linux Mozilla' );
1029 0           eval { $mech->get( $IHEART ) };
  0            
1030              
1031             # if the network connection failed...
1032 0 0 0       if($@ || !$mech->success() || !$mech->content()) {
      0        
1033 0 0         if(-f $sponsorfile) {
1034 0           $json = read_file($sponsorfile);
1035             } else {
1036 0           return;
1037             }
1038             } else {
1039 0           $json = $mech->content();
1040             }
1041              
1042 0           my $data = decode_json($json);
1043              
1044 0 0         return unless($data);
1045              
1046 0           for my $item (@$data) {
1047 0           for my $link (@{$item->{links}}) {
  0            
1048             push @SPONSORS, {
1049             category => $item->{category},
1050             title => $link->{title},
1051             body => $link->{body},
1052             href => $link->{href},
1053             url => $link->{href}
1054 0           };
1055              
1056 0 0         $SPONSORS[-1]{url} =~ s!^https?://(?:www\.)?([^/]+).*!$1! if($SPONSORS[-1]{url});
1057 0 0         $SPONSORS[-1]{body} =~ s!

\s*]*>!!g if($SPONSORS[-1]{body});
1058 0 0         $SPONSORS[-1]{body} =~ s!<[^>]+>!!g if($SPONSORS[-1]{body});
1059             }
1060             }
1061              
1062             # save file in case the network connection fails
1063 0           overwrite_file($sponsorfile, $json);
1064              
1065             #$self->_log( "INFO: " . scalar(@SPONSORS) . " Sponsors loaded\n" );
1066             #$self->_log( "INFO: Sponsors: " . Dumper(\@SPONSORS) );
1067              
1068 0           $MT = Math::Random::MT->new(time);
1069             }
1070              
1071             sub _get_sponsor {
1072 0     0     my $self = shift;
1073 0           my $rand = $MT->rand(scalar(@SPONSORS));
1074 0           $self->_log( "INFO: Sponsors: rand=$rand: " . Dumper($SPONSORS[$rand]) );
1075 0           return $SPONSORS[$rand];
1076             }
1077              
1078             sub _log {
1079 0     0     my $self = shift;
1080 0 0         my $log = $self->logfile or return;
1081 0 0         mkpath(dirname($log)) unless(-f $log);
1082              
1083 0           my $t = localtime;
1084 0           my $s = $t->strftime("%Y/%m/%d %H:%M:%S");
1085              
1086 0 0         my $mode = $self->logclean ? 'w+' : 'a+';
1087 0           $self->logclean(0);
1088              
1089 0 0         my $fh = IO::File->new($log,$mode) or die "Cannot write to log file [$log]: $!\n";
1090 0           print $fh "$s: " . join(' ', @_);
1091 0           $fh->close;
1092             }
1093              
1094             sub _defined_or {
1095 0     0     my $self = shift;
1096 0           while(@_) {
1097 0           my $value = shift;
1098 0 0         return $value if(defined $value);
1099             }
1100              
1101 0           return;
1102             }
1103              
1104             1;
1105              
1106             __END__