File Coverage

blib/lib/CPAN/Testers/WWW/Reports/Mailer.pm
Criterion Covered Total %
statement 108 589 18.3
branch 8 272 2.9
condition 1 138 0.7
subroutine 33 55 60.0
pod 4 4 100.0
total 154 1058 14.5


line stmt bran cond sub pod time code
1             package CPAN::Testers::WWW::Reports::Mailer;
2              
3 22     22   410592 use warnings;
  22         619  
  22         638  
4 22     22   69 use strict;
  22         33  
  22         404  
5              
6 22     22   58 use vars qw($VERSION);
  22         27  
  22         1385  
7             $VERSION = '0.37';
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   11636 use Compress::Zlib;
  22         985435  
  22         4323  
133 22     22   14141 use Config::IniFiles;
  22         367620  
  22         655  
134 22     22   10220 use CPAN::Testers::Common::DBUtils;
  22         387951  
  22         146  
135 22     22   9332 use CPAN::Testers::Common::Utils qw(guid_to_nntp);
  22         5203  
  22         1066  
136 22     22   8169 use CPAN::Testers::Fact::LegacyReport;
  22         425775  
  22         577  
137 22     22   8603 use CPAN::Testers::Fact::TestSummary;
  22         5158  
  22         442  
138 22     22   11410 use Data::Dumper;
  22         95066  
  22         1160  
139 22     22   9703 use Data::FlexSerializer;
  22         24626311  
  22         166  
140 22     22   28182 use Email::Address;
  22         98240  
  22         1018  
141 22     22   8566 use Email::Simple;
  22         73396  
  22         501  
142 22     22   127 use File::Basename;
  22         26  
  22         1771  
143 22     22   104 use File::Path;
  22         32  
  22         993  
144 22     22   7966 use File::Slurp;
  22         52015  
  22         1291  
145 22     22   11060 use Getopt::ArgvFile default=>1;
  22         77962  
  22         119  
146 22     22   107392 use Getopt::Long;
  22         166410  
  22         112  
147 22     22   3768 use IO::File;
  22         40  
  22         3277  
148 22     22   12228 use JSON;
  22         80546  
  22         121  
149 22     22   14221 use LWP::UserAgent;
  22         671017  
  22         651  
150 22     22   9000 use Math::Random::MT;
  22         17948  
  22         79  
151 22     22   768 use Metabase::Resource;
  22         36  
  22         420  
152 22     22   8448 use MIME::Base64;
  22         10365  
  22         1206  
153 22     22   9333 use MIME::QuotedPrint;
  22         3579  
  22         893  
154 22     22   8079 use Path::Class;
  22         257588  
  22         1086  
155 22     22   8576 use Parse::CPAN::Authors;
  22         105838  
  22         100  
156 22     22   9510 use Template;
  22         290785  
  22         573  
157 22     22   10027 use Time::Piece;
  22         125838  
  22         84  
158 22     22   8915 use version;
  22         26758  
  22         110  
159 22     22   14736 use WWW::Mechanize;
  22         560069  
  22         699  
160              
161 22     22   139 use base qw(Class::Accessor::Fast);
  22         52  
  22         111384  
162              
163             # -------------------------------------
164             # Variables
165              
166             # default configuration settings
167             my %default = (
168             lastmail => '_lastmail',
169             verbose => 0,
170             nomail => 1,
171             logclean => 0,
172             mode => 'daily',
173             mailrc => 'data/01mailrc.txt'
174             );
175              
176             my $sponsorfile = 'sponsors.json';
177              
178             my (%AUTHORS,%PREFS,@SPONSORS,$MT,$IHEART,$serializer);
179              
180             my %MODES = (
181             daily => { type => 1, period => '24 hours', report => 'Daily Summary' },
182             weekly => { type => 2, period => '7 days', report => 'Weekly Summary' }, # typically a Saturday
183             reports => { type => 3, period => '', report => 'Test' },
184             monthly => { type => 4, period => 'month', report => 'Monthly Summary' },
185             sun => { type => 5, period => '7 days', report => 'Weekly Summary' },
186             mon => { type => 6, period => '7 days', report => 'Weekly Summary' },
187             tue => { type => 7, period => '7 days', report => 'Weekly Summary' },
188             wed => { type => 8, period => '7 days', report => 'Weekly Summary' },
189             thu => { type => 9, period => '7 days', report => 'Weekly Summary' },
190             fri => { type => 10, period => '7 days', report => 'Weekly Summary' },
191             sat => { type => 11, period => '7 days', report => 'Weekly Summary' },
192             );
193              
194             my $FROM = 'CPAN Tester Report Server <do_not_reply@cpantesters.org>';
195             my $HOW = '/usr/sbin/sendmail -bm';
196             my $HEAD = 'To: "NAME" <EMAIL>
197             From: FROM
198             Date: DATE
199             Subject: SUBJECT
200              
201             ';
202              
203             my @dotw = ( "Sunday", "Monday", "Tuesday", "Wednesday",
204             "Thursday", "Friday", "Saturday" );
205              
206             my @months = (
207             { 'id' => 1, 'value' => "January", },
208             { 'id' => 2, 'value' => "February", },
209             { 'id' => 3, 'value' => "March", },
210             { 'id' => 4, 'value' => "April", },
211             { 'id' => 5, 'value' => "May", },
212             { 'id' => 6, 'value' => "June", },
213             { 'id' => 7, 'value' => "July", },
214             { 'id' => 8, 'value' => "August", },
215             { 'id' => 9, 'value' => "September", },
216             { 'id' => 10, 'value' => "October", },
217             { 'id' => 11, 'value' => "November", },
218             { 'id' => 12, 'value' => "December" },
219             );
220              
221             our %phrasebook = (
222             'LastReport' => "SELECT MAX(id) FROM cpanstats.cpanstats",
223             'GetEarliest' => "SELECT id FROM cpanstats.cpanstats WHERE fulldate > ? ORDER BY id LIMIT 1",
224              
225             'FindAuthorType' => "SELECT pauseid FROM prefs_distributions WHERE report = ?",
226              
227             'GetReports' => "SELECT id,guid,dist,version,platform,perl,state FROM cpanstats.cpanstats WHERE id > ? AND state IN ('pass','fail','na','unknown') ORDER BY id",
228             '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",
229             'GetReportCount' => "SELECT id FROM cpanstats.cpanstats WHERE platform=? AND perl=? AND state=? AND id < ? AND dist=? AND version=? LIMIT 2",
230             'GetLatestDistVers' => "SELECT version FROM cpanstats.uploads WHERE dist=? ORDER BY released DESC LIMIT 1",
231             'GetAuthor' => "SELECT author FROM cpanstats.uploads WHERE dist=? AND version=? LIMIT 1",
232             'GetAuthors' => "SELECT author,dist,version FROM cpanstats.uploads",
233              
234             'GetAuthorPrefs' => "SELECT * FROM prefs_authors WHERE pauseid=?",
235             'GetDefaultPrefs' => "SELECT * FROM prefs_authors AS a INNER JOIN prefs_distributions AS d ON d.pauseid=a.pauseid AND d.distribution='-' WHERE a.pauseid=?",
236             'GetDistPrefs' => "SELECT * FROM prefs_distributions WHERE pauseid=? AND distribution=?",
237             'InsertAuthorLogin' => 'INSERT INTO prefs_authors (active,lastlogin,pauseid) VALUES (1,?,?)',
238             'InsertDistPrefs' => "INSERT INTO prefs_distributions (pauseid,distribution,ignored,report,grade,tuple,version,patches,perl,platform) VALUES (?,?,0,1,'FAIL','FIRST','LATEST',0,'ALL','ALL')",
239              
240             'GetArticle' => "SELECT * FROM articles.articles WHERE id=?",
241              
242             'GetReportTest' => "SELECT id,guid,dist,version,platform,perl,state FROM cpanstats.cpanstats WHERE id = ? AND state IN ('pass','fail','na','unknown') ORDER BY id",
243              
244             'GetMetabaseByGUID' => 'SELECT * FROM metabase.metabase WHERE guid=?',
245             'GetTestersEmail' => 'SELECT * FROM metabase.testers_email',
246             'GetTesters' => 'SELECT * FROM metabase.testers_email ORDER BY id'
247             );
248              
249             #----------------------------------------------------------------------------
250             # The Application Programming Interface
251              
252             __PACKAGE__->mk_accessors(
253             qw( lastmail verbose nomail test logfile logclean mode mailrc tt pause ));
254              
255             # -------------------------------------
256             # The Public Interface Functions
257              
258             sub new {
259 2     2 1 3188 my $class = shift;
260 2         4 my %hash = @_;
261              
262 2         3 my $self = {};
263 2         2 bless $self, $class;
264              
265 2         3 my %options;
266 2 50       8 GetOptions( \%options,
267             'config=s',
268             'lastmail=s',
269             'mailrc=s',
270             'test=i',
271             'logfile=s',
272             'logclean',
273             'verbose',
274             'nomail',
275             'mode=s',
276             'help|h',
277             'version|v'
278             ) or help(1);
279              
280             # default to API settings if no command line option
281 2         781 for(qw(config help version)) {
282 6 100 33     18 $options{$_} ||= $hash{$_} if(defined $hash{$_});
283             }
284              
285 2 50       6 $self->help(1) if($options{help});
286 2 50       3 $self->help(0) if($options{version});
287              
288             # ensure we have a configuration file
289 2 100       21 die "Must specify a configuration file\n" unless( $options{config});
290 1 50       23 die "Configuration file [$options{config}] not found\n" unless(-f $options{config});
291              
292             # load configuration
293 0           my $cfg = Config::IniFiles->new( -file => $options{config} );
294              
295             # configure databases
296 0           for my $db (qw(CPANPREFS)) {
297 0 0         die "No configuration for $db database\n" unless($cfg->SectionExists($db));
298 0           my %opts;
299 0           for my $key (qw(driver database dbfile dbhost dbport dbuser dbpass)) {
300 0           my $val = $cfg->val($db,$key);
301 0 0         $opts{$key} = $val if(defined $val);
302             }
303 0           $self->{$db} = CPAN::Testers::Common::DBUtils->new(%opts);
304 0 0         die "Cannot configure $db database\n" unless($self->{$db});
305 0 0         $self->{db}->{mysql_auto_reconnect} = 1 if($opts{driver} =~ /mysql/i);
306             }
307              
308 0           $self->test( $self->_defined_or( $options{test}, $hash{test}, $cfg->val('SETTINGS','test' ), 0 ) );
309 0 0         $options{nomail} = 1 if($self->test);
310              
311 0           $self->verbose( $self->_defined_or( $options{verbose}, $hash{verbose}, $cfg->val('SETTINGS','verbose' ), $default{verbose}) );
312 0           $self->nomail( $self->_defined_or( $options{nomail}, $hash{nomail}, $cfg->val('SETTINGS','nomail' ), $default{nomail}) );
313 0           $self->lastmail($self->_defined_or( $options{lastmail}, $hash{lastmail}, $cfg->val('SETTINGS','lastmail' ), $default{lastmail}) );
314 0           $self->mailrc( $self->_defined_or( $options{mailrc}, $hash{mailrc}, $cfg->val('SETTINGS','mailrc' ), $default{mailrc} ) );
315 0           $self->logfile( $self->_defined_or( $options{logfile}, $hash{logfile}, $cfg->val('SETTINGS','logfile' ) ) );
316 0           $self->logclean($self->_defined_or( $options{logclean}, $hash{logclean}, $cfg->val('SETTINGS','logclean' ), $default{logclean} ) );
317 0           $self->mode(lc $self->_defined_or( $options{mode}, $hash{mode}, $cfg->val('SETTINGS','mode' ), $default{mode} ) );
318              
319 0           $IHEART = $cfg->val('SETTINGS','iheart_random' );
320              
321 0           my $mode = $self->mode;
322 0 0         if($mode =~ /day/) {
323 0           $mode = substr($mode,0,3);
324 0           $self->mode($mode);
325             }
326              
327 0 0         unless($mode =~ /^(daily|weekly|reports|monthly|sun|mon|tue|wed|thu|fri|sat)$/) {
328 0           die "mode can MUST be 'daily', 'weekly', 'monthly', 'reports', or a day of the week.\n";
329             }
330              
331 0           $self->pause($self->_download_mailrc());
332              
333             # set up API to Template Toolkit
334 0           $self->tt( Template->new(
335             {
336             EVAL_PERL => 1,
337             INCLUDE_PATH => [ 'templates' ],
338             }
339             ));
340              
341 0           my @testers = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetTestersEmail'});
342 0           for my $tester (@testers) {
343 0   0       $self->{testers}{$tester->{creator}}{name} ||= $tester->{fullname};
344 0   0       $self->{testers}{$tester->{creator}}{email} ||= $tester->{email};
345             }
346              
347 0           $self->_load_authors();
348 0           $self->_load_testers();
349 0           $self->_load_sponsors();
350              
351 0           $serializer = Data::FlexSerializer->new(
352             detect_compression => 1,
353             detect_sereal => 1,
354             detect_json => 1,
355             );
356              
357 0           return $self;
358             }
359              
360             sub check_reports {
361 0     0 1   my $self = shift;
362 0           my $mode = $self->mode;
363 0           my $report_type = $MODES{$mode}->{type};
364 0           my $last_id = int( $self->_get_lastid() );
365 0           my (%reports,%tvars);
366              
367 0           $self->_log( "INFO: START checking reports in '$mode' mode\n" );
368 0           $self->_log( "INFO: last_id=$last_id\n" );
369              
370 0           my $next;
371 0 0         if($self->test) {
    0          
372 0           $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetReportTest'},$self->test);
373             } elsif($mode ne 'daily') {
374 0           my @authors = $self->{CPANPREFS}->get_query('hash',$phrasebook{'FindAuthorType'}, $report_type);
375 0 0         return $self->_set_lastid() unless(@authors);
376 0           my $sql = sprintf $phrasebook{'GetReports2'}, join(',',map {"'$_->{pauseid}'"} @authors);
  0            
377 0           $next = $self->{CPANPREFS}->iterator('hash',$sql,$last_id);
378             } else {
379             # find all reports since last update
380 0           $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetReports'},$last_id);
381 0 0         unless($next) {
382 0           $self->_log( "INFO: STOP checking reports\n" );
383 0           return;
384             }
385             }
386              
387 0           my $rows = 0;
388 0           while( my $row = $next->()) {
389 0           $rows++;
390 0 0         $self->_log( "DEBUG: processing report: $row->{id}\n" ) if($self->verbose);
391              
392 0           $self->{counts}{REPORTS}++;
393 0           $last_id = $row->{id};
394 0           $row->{state} = uc $row->{state};
395 0           $self->{counts}{$row->{state}}++;
396              
397 0 0         $self->_log( "DEBUG: dist: $row->{dist} $row->{version} $row->{state}\n" ) if($self->verbose);
398              
399 0           my $author = $self->_get_author($row->{dist}, $row->{version});
400 0 0 0       $self->_log( "DEBUG: author: ".($author||'')."\n" ) if($self->verbose);
401 0 0         next unless($author);
402              
403 0 0         unless($author) {
404 0           $self->_log( "WARN: author not found for distribution [$row->{dist}], [$row->{version}]\n" );
405 0           next;
406             }
407              
408 0   0       $row->{version} ||= '';
409 0   0       $row->{platform} ||= '';
410 0   0       $row->{perl} ||= '';
411              
412             # get author preferences
413 0   0       my $prefs = $self->_get_prefs($author) || next;
414              
415             # do we need to worry about this author?
416 0 0         if($prefs->{active} == 2) {
417 0           $self->{counts}{NOMAIL}++;
418 0 0         $self->_log( "DEBUG: author: $author - not active\n" ) if($self->verbose);
419 0           next;
420             }
421              
422             # get distribution preferences
423 0           $prefs = $self->_get_prefs($author, $row->{dist});
424 0 0         $self->_log( "DEBUG: dist prefs: " .($prefs ? 'Found' : 'Not Found')."\n" ) if($self->verbose);
    0          
425 0 0         next unless($prefs);
426 0 0 0       $self->_log( "DEBUG: dist prefs: ignored=" .($prefs->{ignored} || 0)."\n" ) if($self->verbose);
427 0 0         next if($prefs->{ignored});
428 0 0         $self->_log( "DEBUG: dist prefs: report=$prefs->{report}, report type=$report_type\n" ) if($self->verbose);
429 0 0         next if($prefs->{report} != $report_type);
430 0 0 0       $self->_log( "DEBUG: dist prefs: $row->{state}=" .($prefs->{grades}{$row->{state}}||'undef')."\n" ) if($self->verbose);
431 0 0 0       $self->_log( "DEBUG: dist prefs: ALL=" .($prefs->{grades}{ALL}||'undef')."\n" ) if($self->verbose);
432 0 0 0       next unless($prefs->{grades}{$row->{state}} || $prefs->{grades}{'ALL'});
433 0 0         $self->_log( "DEBUG: dist prefs: CONTINUE\n" ) if($self->verbose);
434              
435             # Check whether distribution version is required.
436             # If version set to 'LATEST' check this is the current version, if set
437             # to 'ALL' then we should allow EVERYTHING through, otherwise filter
438             # on the requested versions.
439              
440 0 0 0       if($row->{version} && $prefs->{version} && $prefs->{version} ne 'ALL') {
      0        
441 0 0         if($prefs->{version} eq 'LATEST') {
442 0           my @vers = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetLatestDistVers'},$row->{dist});
443 0 0         $self->_log( "DEBUG: dist prefs: vers=".(scalar(@vers))."\n" ) if($self->verbose);
444 0 0         $self->_log( "DEBUG: dist prefs: version=$vers[0]->[0], $row->{version}\n" ) if($self->verbose);
445 0 0 0       next if(@vers && $vers[0]->[0] ne $row->{version});
446             } else {
447 0           $prefs->{version} =~ s/\s*//g;
448 0           my %m = map {$_ => 1} split(',',$prefs->{version});
  0            
449 0 0         $self->_log( "DEBUG: dist prefs: $row->{version}\n" ) if($self->verbose);
450 0 0         next unless($m{$row->{version}});
451             }
452             }
453              
454             # Check whether this platform is required.
455 0 0 0       if($row->{platform} && $prefs->{platform} && $prefs->{platform} ne 'ALL') {
      0        
456 0           $prefs->{platform} =~ s/\s*//g;
457 0           $prefs->{platform} =~ s/,/|/g;
458 0           $prefs->{platform} =~ s/\./\\./g;
459 0           $prefs->{platform} =~ s/^(\w+)\|//;
460 0 0 0       if($1 && $1 eq 'NOT') {
461 0 0         $self->_log( "DEBUG: dist prefs: $row->{platform}, =~ $prefs->{platform}\n" ) if($self->verbose);
462 0 0         next if($row->{platform} =~ /$prefs->{platform}/);
463             } else {
464 0 0         $self->_log( "DEBUG: dist prefs: $row->{platform}, !~ $prefs->{platform}\n" ) if($self->verbose);
465 0 0         next if($row->{platform} !~ /$prefs->{platform}/);
466             }
467             }
468              
469             # Check whether this perl version is required.
470 0 0 0       if($row->{perl} && $prefs->{perl} && $prefs->{perl} ne 'ALL') {
      0        
471 0           my $perlv = $row->{perl};
472 0           $perlv = $row->{perl};
473 0           $perlv =~ s/\s.*//;
474              
475 0           $prefs->{perl} =~ s/\s*//g;
476 0           $prefs->{perl} =~ s/,/|/g;
477 0           $prefs->{perl} =~ s/\./\\./g;
478 0           my $v = version->new("$perlv")->numify;
479 0           $prefs->{platform} =~ s/^(\w+)\|//;
480 0 0 0       if($1 && $1 eq 'NOT') {
481 0 0         $self->_log( "DEBUG: dist prefs: $perlv || $v =~ $prefs->{perl}\n" ) if($self->verbose);
482 0 0 0       next if($perlv =~ /$prefs->{perl}/ && $v =~ /$prefs->{perl}/);
483             } else {
484 0 0         $self->_log( "DEBUG: dist prefs: $perlv || $v !~ $prefs->{perl}\n" ) if($self->verbose);
485 0 0 0       next if($perlv !~ /$prefs->{perl}/ && $v !~ /$prefs->{perl}/);
486             }
487             }
488              
489             # Check whether patches are required.
490 0 0         $self->_log( "DEBUG: dist prefs: patches=$prefs->{patches}, row perl $row->{perl}\n" ) if($self->verbose);
491 0 0 0       next if(!$prefs->{patches} && $row->{perl} =~ /(RC\d+|patch)/);
492              
493             # check whether only first instance required
494 0 0         if($prefs->{tuple} eq 'FIRST') {
495             my @count = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetReportCount'},
496 0           $row->{platform}, $row->{perl}, $row->{state}, $row->{id}, $row->{dist}, $row->{version});
497 0 0         $self->_log( "DEBUG: dist prefs: tuple=FIRST, count=".(scalar(@count))."\n" ) if($self->verbose);
498 0 0         next if(@count > 0);
499             }
500              
501 0 0         $self->_log( "DEBUG: report is being added to mailshot\n" ) if($self->verbose);
502              
503 0 0         if($mode eq 'reports') {
504 0           $self->_send_report($author,$row);
505             }
506              
507 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            
508             }
509              
510 0           $self->_log( "INFO: STOP checking reports in '$mode' mode\n" );
511              
512 0 0         return $self->_set_lastid() unless($rows);
513              
514 0 0         if($mode ne 'reports') {
515 0           $self->_log( "INFO: START parsing data in '$mode' mode\n" );
516 0 0         $self->_log( "DEBUG: processing authors: ".(scalar(keys %reports))."\n" ) if($self->verbose);
517              
518 0           for my $author (sort keys %reports) {
519 0 0         $self->_log( "DEBUG: $author\n" ) if($self->verbose);
520              
521 0           my $pause = $self->pause->author($author);
522 0 0         $tvars{name} = $pause ? $pause->name : $author;
523 0           $tvars{author} = $author;
524 0           $tvars{dists} = ();
525              
526             # get author preferences
527 0           my $prefs = $self->_get_prefs($author);
528              
529             # active:
530             # 0 - new author, no correspondance
531             # 1 - new author, notification mailed
532             # 2 - author requested no mail
533             # 3 - author requested summary report
534              
535 0 0 0       if(!$prefs->{active} || $prefs->{active} == 0) {
536 0           $tvars{subject} = 'Welcome to CPAN Testers';
537 0           $self->_write_mail('notification.eml',\%tvars);
538 0           $self->{counts}{NEWAUTH}++;
539              
540             # insert author defaults, however check that they don't already
541             # exists in the system first, in case entries are out of sync.
542 0           my @auth = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetAuthorPrefs'}, $author);
543 0 0         $self->{CPANPREFS}->do_query($phrasebook{'InsertAuthorLogin'}, time(), $author) unless(@auth);
544 0           my @dist = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetDistPrefs'}, $author,'-');
545 0 0         $self->{CPANPREFS}->do_query($phrasebook{'InsertDistPrefs'}, $author, '-') unless(@dist);
546             }
547              
548 0 0         $self->_log( "DEBUG: $author - distributions = ".(scalar(keys %{$reports{$author}->{dists}}))."\n" ) if($self->verbose);
  0            
549              
550 0           my ($reports,@e);
551 0           for my $dist (sort keys %{$reports{$author}->{dists}}) {
  0            
552 0           my $v = $reports{$author}->{dists}{$dist};
553 0           my @d;
554 0           for my $version (sort keys %{$v->{versions}}) {
  0            
555 0           my $w = $v->{versions}{$version};
556 0           my @c;
557 0           for my $platform (sort keys %{$w->{platforms}}) {
  0            
558 0           my $x = $w->{platforms}{$platform};
559 0           my @b;
560 0           for my $perl (sort keys %{$x->{perls}}) {
  0            
561 0           my $y = $x->{perls}{$perl};
562 0           my @a;
563 0           for my $state (sort keys %{$y->{states}}) {
  0            
564 0           my $z = $y->{states}{$state};
565 0           push @a, {state => $state, ids => $z->{value}};
566 0           $reports++;
567             }
568 0           push @b, {perl => $perl, states => \@a};
569             }
570 0           push @c, {platform => $platform, perls => \@b};
571             }
572 0           push @d, {version => $version, platforms => \@c};
573             }
574 0           push @e, {dist => $dist, versions => \@d};
575             }
576              
577 0 0         next unless($reports);
578 0 0         if($self->verbose) { $self->_log( "DEBUG: $author - reports = $reports\n" ) }
  0            
579 0           else { $self->_log( "INFO: $author - dists=".(scalar(keys %{$reports{$author}->{dists}})).", reports=$reports\n" ) }
  0            
580              
581 0           $tvars{dists} = \@e;
582 0           $tvars{period} = $MODES{$mode}->{period};
583 0           $tvars{report} = $MODES{$mode}->{report};
584 0           $tvars{subject} = "CPAN Testers $tvars{report} Report";
585              
586 0           $self->_write_mail('mailer.eml',\%tvars);
587             }
588              
589 0           $self->_log( "INFO: STOP parsing data in '$mode' mode\n" );
590             }
591              
592 0           $self->_set_lastid($last_id);
593             }
594              
595             sub check_counts {
596 0     0 1   my $self = shift;
597 0           my $mode = $self->mode;
598              
599 0           $self->_log( "INFO: COUNTS for '$mode' mode:\n" );
600 0           my @counts = qw(REPORTS PASS FAIL UNKNOWN NA NOMAIL MAILS NEWAUTH GOOD BAD);
601 0 0         push @counts, 'TEST' if($self->nomail);
602              
603 0           for(@counts) {
604 0   0       $self->{counts}{$_} ||= 0;
605 0           $self->_log( sprintf "INFO: %7s = %6d\n", $_, $self->{counts}{$_} );
606             }
607             }
608              
609             sub help {
610 0     0 1   my ($self,$full) = @_;
611              
612 0 0         if($full) {
613 0           print <<HERE;
614              
615             Usage: $0 --config=<file> \\
616             [--logfile=<file> [--logclean]] [--verbose] [--nomail] \\
617             [--test=<id>] [--lastmail=<file>] \\
618             [--mode=(daily|weekly|report|monthly|sun|mon|tue|wed|thu|fri|sat)] \\
619             [-h] [-v]
620              
621             --config=<file> database configuration file
622             --logfile=<file> log file (*)
623             --logclean 0 = append, 1 = overwrite (*)
624             --verbose print additional log messages
625             --nomail nomail flag, no mail sent if true (*)
626             --test=<id> test an id in debug mode, no mail sent (*)
627             --lastmail=<file> lastmail counter file (*)
628             --mode run mode (*)
629             -h this help screen
630             -v program version
631              
632             NOTES:
633             * - these will override any settings within the configuration file.
634             HERE
635              
636             }
637              
638 0           print "$0 v$VERSION\n";
639 0           exit(0);
640             }
641              
642             #----------------------------------------------------------------------------
643             # Internal Methods
644              
645             sub _get_lastid {
646 0     0     my ($self,$id) = @_;
647 0           my $mode = $self->mode;
648              
649 0 0         unless( -f $self->lastmail ) {
650 0           mkpath(dirname($self->lastmail));
651 0           overwrite_file( $self->lastmail, 'daily=0,weekly=0,reports=0' );
652             }
653              
654 0 0         if (defined $id) {
655 0           my $text = read_file($self->lastmail);
656 0 0         if($text =~ m!$mode=\d+!) {
657 0           $text =~ s!($mode=)\d+!$1$id!;
658             } else {
659 0           $text .= ",$mode=$id"; # auto add mode
660             }
661 0           $text =~ s/\s+//g;
662 0           overwrite_file( $self->lastmail, $text );
663 0           return $id;
664             }
665              
666 0           my $text = read_file($self->lastmail);
667 0 0         return $id if(($id) = $text =~ m!$mode=(\d+)!);
668 0           return $self->_get_earliest(); # mode not found, find earliest id based on mode
669             }
670              
671             sub _set_lastid {
672 0     0     my ($self,$id) = @_;
673              
674 0 0         if(!defined $id) {
675 0           my @lastid = $self->{CPANPREFS}->get_query('array',$phrasebook{'LastReport'});
676 0 0         $id = @lastid ? $lastid[0]->[0] : 0;
677             }
678              
679 0           $self->_log( "INFO: new last_id=$id\n" );
680 0           $self->_log( "INFO: STOP checking reports\n" );
681              
682 0 0         return $id if($self->nomail);
683              
684 0           $self->_get_lastid($id);
685             }
686              
687             sub _get_earliest {
688 0     0     my $self = shift;
689 0           my $mode = $self->mode;
690              
691 0           my @date = localtime(time);
692 0           $date[5] += 1900;
693 0           $date[4] += 1;
694 0 0 0       if($mode eq 'monthly') {
    0          
695 0           $date[4] -= 1;
696 0           $date[3] = 1;
697             } elsif($mode eq 'daily' || $mode eq 'reports') {
698 0           $date[3] -= 1;
699             } else {
700 0           $date[3] -=7;
701             }
702              
703 0 0         if($date[3] < 1) {
704 0           $date[4] -= 1;
705 0 0 0       if($date[4] == 2 && $date[5] % 4) {
    0 0        
    0 0        
      0        
706 0           $date[3] = 28 - $date[3];
707             } elsif($date[3] == 2) {
708 0           $date[3] = 29 - $date[3];
709             } elsif($date[3] == 4 || $date[3] == 6 || $date[3] == 9 || $date[3] == 11) {
710 0           $date[3] = 30 - $date[3];
711             } else {
712 0           $date[3] = 31 - $date[3];
713             }
714 0 0         if($date[4] < 1) {
715 0           $date[4] = 12;
716 0           $date[5] -= 1;
717             }
718             }
719              
720 0           my $fulldate = sprintf "%04d%02d%02d000000", $date[5], $date[4], $date[3];
721 0           my @report = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetEarliest'}, $fulldate);
722 0 0         return 0 unless(@report);
723 0   0       return $report[0]->[0] || 0;
724             }
725              
726             sub _get_prefs {
727 0     0     my $self = shift;
728 0           my ($author,$dist) = @_;
729 0           my $active = 0;
730              
731 0 0         return unless($author);
732              
733             # get distribution defaults
734 0 0 0       if($author && $dist) {
735 0 0         if(defined $PREFS{$author}{dists}{$dist}) {
736 0           return $PREFS{$author}{dists}{$dist};
737             }
738              
739 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetDistPrefs'}, $author,$dist);
740 0 0         if(@rows) {
741 0           $PREFS{$author}{dists}{$dist} = $self->_parse_prefs($rows[0]);
742 0           return $PREFS{$author}{dists}{$dist};
743             }
744              
745             # fall through and assume author defaults
746             }
747              
748             # get author defaults
749 0 0         if($author) {
750 0 0         if(defined $PREFS{$author}{default}) {
751 0           return $PREFS{$author}{default};
752             }
753              
754 0           my @auth = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetAuthorPrefs'}, $author);
755 0 0         if(@auth) {
756 0   0       $PREFS{$author}{default}{active} = $auth[0]->{active} || 0;
757              
758 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetDefaultPrefs'}, $author);
759 0 0         if(@rows) {
760 0           $PREFS{$author}{default} = $self->_parse_prefs($rows[0]);
761 0   0       $PREFS{$author}{default}{active} = $rows[0]->{active} || 0;
762 0           return $PREFS{$author}{default};
763             } else {
764 0           $self->{CPANPREFS}->do_query($phrasebook{'InsertDistPrefs'}, $author, '-');
765 0           $active = $PREFS{$author}{default}{active};
766             }
767             }
768              
769             # fall through and assume new author
770             }
771              
772 0   0       $dist ||= '-';
773              
774             # use global defaults
775 0           my %prefs = (
776             active => $active,
777             ignored => 0,
778             report => 1,
779             grades => {'FAIL' => 1},
780             tuple => 'FIRST',
781             version => 'LATEST',
782             patches => 0,
783             perl => 'ALL',
784             platform => 'ALL',
785             );
786 0           $PREFS{$author}{dists}{$dist} = \%prefs;
787 0           return \%prefs;
788             }
789              
790             sub _parse_prefs {
791 0     0     my ($self,$row) = @_;
792 0           my %hash;
793              
794 0   0       $row->{grade} ||= 'FAIL';
795 0           my %grades = map {$_ => 1} split(',',$row->{grade});
  0            
796              
797 0           $hash{grades} = \%grades;
798 0           $hash{ignored} = $self->_defined_or($row->{ignored}, 0);
799 0           $hash{report} = $self->_defined_or($row->{report}, 1);
800 0           $hash{tuple} = $self->_defined_or($row->{tuple}, 'FIRST');
801 0           $hash{version} = $self->_defined_or($row->{version}, 'LATEST');
802 0           $hash{patches} = $self->_defined_or($row->{patches}, 0);
803 0           $hash{perl} = $self->_defined_or($row->{perl}, 'ALL');
804 0           $hash{platform} = $self->_defined_or($row->{platform}, 'ALL');
805              
806 0           return \%hash;
807             }
808              
809             sub _send_report {
810 0     0     my ($self,$author,$row) = @_;
811 0           my %tvars;
812              
813 0           my $nntpid = guid_to_nntp($row->{guid});
814              
815             # old NNTP article lookup
816 0 0         if($nntpid) {
817             # get article
818 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetArticle'}, $nntpid);
819              
820             #$self->_log( "ARTICLE: $nntpid: $rows[0]->{article}\n" );
821              
822             # disassemble article
823 0 0         $rows[0]->{article} = decode_qp($rows[0]->{article}) if($rows[0]->{article} =~ /=3D/);
824 0           my $mail = Email::Simple->new($rows[0]->{article});
825 0 0         return unless $mail;
826              
827             # get from & subject line
828 0           my $from = $mail->header("From");
829 0           my $subject = $mail->header("Subject");
830 0 0         return unless $subject;
831              
832 0           my ($address) = Email::Address->parse($from);
833 0           my $reply = sprintf "%s\@%s", $address->user, $address->host;
834              
835             # extract the body
836 0           my $encoding = $mail->header('Content-Transfer-Encoding');
837 0           my $body = $mail->body;
838 0 0 0       $body = decode_base64($body) if($encoding && $encoding eq 'base64');
839              
840             # set up new mail headers
841 0           my $pause = $self->pause->author($author);
842 0 0         %tvars = (
843             author => $author,
844             name => ($pause ? $pause->name : $author),
845             subject => $subject,
846             from => $reply,
847             body => $body,
848             reply => $reply
849             );
850              
851             # new Metabase lookup
852             } else {
853 0           my @rows = $self->{CPANPREFS}->get_query('hash',$phrasebook{'GetMetabaseByGUID'},$row->{guid});
854 0 0         return unless(@rows);
855              
856 0           my $data;
857 0           my $row = $rows[0];
858 0 0         if($row->{fact}) {
859 0           my $report;
860 0           eval { $report = $serializer->deserialize($row->{fact}); };
  0            
861 0           $self->_log( "WARN: Bad Sereal in metabase fact $row->{guid}\n" );
862 0 0         return if($@);
863              
864 0           $data = _dereference_report($report);
865             } else {
866 0           $data = $serializer->deserialize($row->{report});
867             }
868              
869 0 0         unless ( $data ) {
870 0           $self->_log( "WARN: Bad serialisation in metabase report $row->{guid}\n" );
871 0           return;
872             }
873            
874 0           my $fact = CPAN::Testers::Fact::LegacyReport->from_struct( $data->{'CPAN::Testers::Fact::LegacyReport'} );
875 0           my $body = $fact->{content}{textreport};
876              
877 0           my $report = CPAN::Testers::Fact::TestSummary->from_struct( $data->{'CPAN::Testers::Fact::TestSummary'} );
878 0           my $state = uc $report->{content}{grade};
879 0           my $osname = $report->{content}{osname};
880 0           my $perl = $report->{content}{perl_version};
881              
882 0           my $distro = Metabase::Resource->new( $report->{metadata}{core}{resource} );
883 0           my $dist = $distro->metadata->{dist_name};
884 0           my $version = $distro->metadata->{dist_version};
885 0           my $author2 = $distro->metadata->{author};
886              
887 0           my ($tester_name,$tester_email) = $self->_get_tester( $report->creator );
888              
889 0           my $subject = sprintf "%s %s-%s %s %s", $state, $dist, $version, $perl, $osname;
890              
891             # set up new mail headers
892 0 0         my $pause = $author2 ? $self->pause->author($author2) : $self->pause->author($author);
893 0 0         %tvars = (
894             author => $author,
895             name => ($pause ? $pause->name : $author),
896             subject => $subject,
897             from => $tester_email,
898             body => $body,
899             reply => $tester_email
900             );
901             }
902              
903             # send data
904 0           $self->_write_mail('report.eml',\%tvars);
905             }
906              
907             sub _dereference_report {
908 0     0     my ($report) = @_;
909 0           my %facts;
910              
911 0           eval {
912 0           my @facts = $report->facts();
913 0           for my $fact (@facts) {
914 0           my $name = ref $fact;
915 0           $facts{$name} = $fact->as_struct;
916 0           $facts{$name}{content} = decode_json($facts{$name}{content});
917             }
918             };
919              
920 0 0         return if($@);
921              
922 0           return \%facts;
923             }
924              
925             sub _write_mail {
926 0     0     my ($self,$template,$parms) = @_;
927              
928 0 0         unless($parms->{author}) {
929 0           $self->_log( "INFO: BAD: $parms->{author} [$parms->{name}]\n" );
930 0           $self->{counts}{BAD}++;
931 0           return;
932             }
933              
934 0   0       my $from = $parms->{from} || $FROM;
935 0   0       my $subject = $parms->{subject} || 'CPAN Testers Daily Reports';
936 0           my $cmd = qq!| $HOW $parms->{author}\@cpan.org!;
937              
938 0           $self->{counts}{MAILS}++;
939              
940 0           my $DATE = $self->_emaildate();
941 0           $DATE =~ s/\s+$//;
942              
943 0           my $sponsor = $self->_get_sponsor();
944 0           $self->_log( "INFO: Get Sponsor: ".Dumper($sponsor)."\n" );
945 0           $parms->{SPONSOR_CATEGORY} = $sponsor->{category};
946 0           $parms->{SPONSOR_NAME} = $sponsor->{title};
947 0           $parms->{SPONSOR_BODY} = $sponsor->{body};
948 0           $parms->{SPONSOR_HREF} = $sponsor->{href};
949 0           $parms->{SPONSOR_URL} = $sponsor->{url};
950              
951 0           my $text;
952 0 0         $self->tt->process( $template, $parms, \$text ) || die $self->tt->error;
953              
954 0   0       $parms->{name} ||= $parms->{author};
955              
956 0           my $body;
957 0 0         $body = "Reply-To: $parms->{reply}\n" if($parms->{reply});
958 0           $body .= $HEAD . $text;
959 0           $body =~ s/FROM/$from/g;
960 0           $body =~ s/NAME/$parms->{name}/g;
961 0           $body =~ s/EMAIL/$parms->{author}\@cpan.org/g;
962 0           $body =~ s/DATE/$DATE/g;
963 0           $body =~ s/SUBJECT/$subject/g;
964              
965 0 0         if($self->nomail) {
    0          
966 0           $self->_log( "INFO: TEST: $parms->{author}\n" );
967 0           $self->{counts}{TEST}++;
968 0 0         my $fh = IO::File->new('mailer-debug.log','a+') or die "Cannot write to debug file [mailer-debug.log]: $!\n";
969 0           print $fh $body;
970 0           $fh->close;
971            
972             } elsif(my $fh = IO::File->new($cmd)) {
973 0           print $fh $body;
974 0           $fh->close;
975 0           $self->_log( "INFO: GOOD: $parms->{author}\n" );
976 0           $self->{counts}{GOOD}++;
977              
978             } else {
979 0           $self->_log( "INFO: BAD: $parms->{author}\n" );
980 0           $self->{counts}{BAD}++;
981             }
982             }
983              
984             sub _emaildate {
985 0     0     my $self = shift;
986 0           my $t = localtime;
987 0           return $t->strftime("%a, %d %b %Y %H:%M:%S +0000");
988             }
989              
990             sub _download_mailrc {
991 0     0     my $self = shift;
992 0           my $file = $self->mailrc;
993 0           my $data;
994              
995 0 0 0       if($file && -f $file) {
996 0           $data = read_file($file);
997              
998             } else {
999 0           my $url = 'http://www.cpan.org/authors/01mailrc.txt.gz';
1000 0           my $ua = LWP::UserAgent->new;
1001 0           $ua->timeout(180);
1002 0           my $response = $ua->get($url);
1003              
1004 0 0         if ($response->is_success) {
1005 0           my $gzipped = $response->content;
1006 0           $data = Compress::Zlib::memGunzip($gzipped);
1007 0 0         die "Error uncompressing data from $url" unless $data;
1008             } else {
1009 0           die "Error fetching $url";
1010             }
1011             }
1012              
1013 0           my $p = Parse::CPAN::Authors->new($data);
1014 0 0         die "Cannot parse data from 01mailrc.txt" unless($p);
1015 0           return $p;
1016             }
1017              
1018             sub _load_testers {
1019 0     0     my $self = shift;
1020 0           my $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetTesters'});
1021 0           while(my $row = $next->()) {
1022 0   0       $self->{testers}{$row->{resource}}{name} ||= $row->{fullname};
1023 0   0       $self->{testers}{$row->{resource}}{email} ||= $row->{email};
1024             }
1025             }
1026              
1027             sub _get_tester {
1028 0     0     my ($self,$creator) = @_;
1029              
1030 0 0 0       return unless($creator && $self->{testers}{$creator});
1031 0           return $self->{testers}{$creator}{name},$self->{testers}{$creator}{email};
1032             }
1033              
1034             sub _load_authors {
1035 0     0     my $self = shift;
1036 0           my $next = $self->{CPANPREFS}->iterator('hash',$phrasebook{'GetAuthors'});
1037 0           while(my $row = $next->()) {
1038 0           $AUTHORS{$row->{dist}}{$row->{version}} = $row->{author};
1039             }
1040             }
1041              
1042             sub _get_author {
1043 0     0     my ($self,$dist,$vers) = @_;
1044 0 0 0       return unless($dist && $vers);
1045 0           return $AUTHORS{$dist}{$vers};
1046             }
1047              
1048             sub _get_authorX {
1049 0     0     my $self = shift;
1050 0           my ($dist,$vers) = @_;
1051 0 0 0       return unless($dist && $vers);
1052              
1053 0 0 0       unless($AUTHORS{$dist} && $AUTHORS{$dist}{$vers}) {
1054 0           my @author = $self->{CPANPREFS}->get_query('array',$phrasebook{'GetAuthor'}, $dist, $vers);
1055 0 0         $AUTHORS{$dist}{$vers} = @author ? $author[0]->[0] : undef;
1056             }
1057 0           return $AUTHORS{$dist}{$vers};
1058             }
1059              
1060             sub _load_sponsors {
1061 0     0     my $self = shift;
1062 0           my $json;
1063              
1064 0           my $mech = WWW::Mechanize->new();
1065 0           $mech->agent_alias( 'Linux Mozilla' );
1066 0           eval { $mech->get( $IHEART ) };
  0            
1067              
1068             # if the network connection failed...
1069 0 0 0       if($@ || !$mech->success() || !$mech->content()) {
      0        
1070 0 0         if(-f $sponsorfile) {
1071 0           $json = read_file($sponsorfile);
1072             } else {
1073 0           return;
1074             }
1075             } else {
1076 0           $json = $mech->content();
1077             }
1078              
1079 0           my $data = decode_json($json);
1080              
1081 0 0         return unless($data);
1082              
1083 0           for my $item (@$data) {
1084 0           for my $link (@{$item->{links}}) {
  0            
1085             push @SPONSORS, {
1086             category => $item->{category},
1087             title => $link->{title},
1088             body => $link->{body},
1089             href => $link->{href},
1090             url => $link->{href}
1091 0           };
1092              
1093 0 0         $SPONSORS[-1]{url} =~ s!^https?://(?:www\.)?([^/]+).*!$1! if($SPONSORS[-1]{url});
1094 0 0         $SPONSORS[-1]{body} =~ s!</p>\s*<p[^>]*>!!g if($SPONSORS[-1]{body});
1095 0 0         $SPONSORS[-1]{body} =~ s!<[^>]+>!!g if($SPONSORS[-1]{body});
1096             }
1097             }
1098              
1099             # save file in case the network connection fails
1100 0           overwrite_file($sponsorfile, $json);
1101              
1102             #$self->_log( "INFO: " . scalar(@SPONSORS) . " Sponsors loaded\n" );
1103             #$self->_log( "INFO: Sponsors: " . Dumper(\@SPONSORS) );
1104              
1105 0           $MT = Math::Random::MT->new(time);
1106             }
1107              
1108             sub _get_sponsor {
1109 0     0     my $self = shift;
1110 0           my $rand = $MT->rand(scalar(@SPONSORS));
1111 0           $self->_log( "INFO: Sponsors: rand=$rand: " . Dumper($SPONSORS[$rand]) );
1112 0           return $SPONSORS[$rand];
1113             }
1114              
1115             sub _log {
1116 0     0     my $self = shift;
1117 0 0         my $log = $self->logfile or return;
1118 0 0         mkpath(dirname($log)) unless(-f $log);
1119              
1120 0           my $t = localtime;
1121 0           my $s = $t->strftime("%Y/%m/%d %H:%M:%S");
1122              
1123 0 0         my $mode = $self->logclean ? 'w+' : 'a+';
1124 0           $self->logclean(0);
1125              
1126 0 0         my $fh = IO::File->new($log,$mode) or die "Cannot write to log file [$log]: $!\n";
1127 0           print $fh "$s: " . join(' ', @_);
1128 0           $fh->close;
1129             }
1130              
1131             sub _defined_or {
1132 0     0     my $self = shift;
1133 0           while(@_) {
1134 0           my $value = shift;
1135 0 0         return $value if(defined $value);
1136             }
1137              
1138 0           return;
1139             }
1140              
1141             1;
1142              
1143             __END__
1144              
1145             =head1 INTERFACE
1146              
1147             =head2 The Constructor
1148              
1149             =over
1150              
1151             =item * new
1152              
1153             Instatiates the object CPAN::WWW::Testers. Requires a hash of parameters, with
1154             'config' being the only mandatory key. Note that 'config' can be anything that
1155             L<Config::IniFiles> accepts for the I<-file> option.
1156              
1157             =back
1158              
1159             =head2 Public Methods
1160              
1161             =over 4
1162              
1163             =item * check_reports
1164              
1165             The core method that analyses the reports and constructs the mails.
1166              
1167             =item * check_counts
1168              
1169             Prints a summary of the processing.
1170              
1171             =item * help
1172              
1173             Using the command line option, --help or -h, displays a help screen with
1174             instructions of the command line arguments. See the Configuration section
1175             for further details.
1176              
1177             =back
1178              
1179             =head2 Accessor Methods
1180              
1181             =over 4
1182              
1183             =item * lastfile
1184              
1185             Path to the file containing the last NNTPID processed.
1186              
1187             =item * verbose
1188              
1189             Provides the current verbose configuration setting.
1190              
1191             =item * nomail
1192              
1193             Provides the current nomail configuration setting.
1194              
1195             =item * test
1196              
1197             Provides a single test ID, if not all NNTPIDs need testing.
1198              
1199             =item * logfile
1200              
1201             Path to output log file for progress and debugging messages.
1202              
1203             =item * logclean
1204              
1205             If set to a true value will create/overwrite the logfile, otherwise will
1206             append any messages.
1207              
1208             =item * mode
1209              
1210             Provides the current mode being executed.
1211              
1212             =item * mailrc
1213              
1214             Path to the 01mailrc.txt file.
1215              
1216             =item * tt
1217              
1218             Provides the Template Toolkit object.
1219              
1220             =item * pause
1221              
1222             Provides the Parse::CPAN::Authors object.
1223              
1224             =back
1225              
1226             =head2 Internal Methods
1227              
1228             =over 4
1229              
1230             =item * _get_lastid
1231              
1232             Returns the last NNTPID processed for the current mode.
1233              
1234             =item * _set_lastid
1235              
1236             Sets the given NNTPID for the current mode.
1237              
1238             =item * _get_author
1239              
1240             Returns the author of a given distribution/version.
1241              
1242             =item * _get_prefs
1243              
1244             Returns the author preferences.
1245              
1246             =item * _parse_prefs
1247              
1248             Parse a preferences record and returns a hash instance.
1249              
1250             =item * _send_report
1251              
1252             Repackages a report as an email for an individual author.
1253              
1254             =item * _write_mail
1255              
1256             Composes and sends a mail message.
1257              
1258             =item * _emaildate
1259              
1260             Returns an RFC 2822 compliant formatted date string.
1261              
1262             =item * _download_mailrc
1263              
1264             Downloads and/or reads a copy of the 01mailrc.txt file.
1265              
1266             =back
1267              
1268             =head1 SEE ALSO
1269              
1270             L<CPAN::Testers::Data::Generator>
1271             L<CPAN::Testers::WWW::Statistics>
1272              
1273             F<http://prefs.cpantesters.org/>,
1274             F<http://www.cpantesters.org/>,
1275             F<http://stats.cpantesters.org/>,
1276             F<http://wiki.cpantesters.org/>,
1277             F<http://blog.cpantesters.org/>
1278              
1279             =head1 BUGS, PATCHES & FIXES
1280              
1281             There are no known bugs at the time of this release. However, if you spot a
1282             bug or are experiencing difficulties, that is not explained within the POD
1283             documentation, please send bug reports and patches to the RT Queue (see below).
1284              
1285             Fixes are dependent upon their severity and my availability. Should a fix not
1286             be forthcoming, please feel free to (politely) remind me.
1287              
1288             RT Queue -
1289             http://rt.cpan.org/Public/Dist/Display.html?Name=CPAN-Testers-WWW-Reports-Mailer
1290              
1291             =head1 CPAN TESTERS FUND
1292              
1293             CPAN Testers wouldn't exist without the help and support of the Perl
1294             community. However, since 2008 CPAN Testers has grown far beyond the
1295             expectations of it's original creators. As a consequence it now requires
1296             considerable funding to help support the infrastructure.
1297              
1298             In early 2012 the Enlightened Perl Organisation very kindly set-up a
1299             CPAN Testers Fund within their donatation structure, to help the project
1300             cover the costs of servers and services.
1301              
1302             If you would like to donate to the CPAN Testers Fund, please follow the link
1303             below to the Enlightened Perl Organisation's donation site.
1304              
1305             F<https://members.enlightenedperl.org/drupal/donate-cpan-testers>
1306              
1307             If your company would like to support us, you can donate financially via the
1308             fund link above, or if you have servers or services that we might use, please
1309             send an email to admin@cpantesters.org with details.
1310              
1311             Our full list of current sponsors can be found at our I <3 CPAN Testers site.
1312              
1313             F<http://iheart.cpantesters.org>
1314              
1315             =head1 AUTHOR
1316              
1317             Barbie, <barbie@missbarbell.co.uk> for
1318             Miss Barbell Productions, L<http://www.missbarbell.co.uk/>
1319              
1320             =head1 COPYRIGHT & LICENSE
1321              
1322             Copyright (C) 2008-2016 Barbie for Miss Barbell Productions.
1323              
1324             This distribution is free software; you can redistribute it and/or
1325             modify it under the Artistic Licence v2.
1326              
1327             =cut