File Coverage

blib/lib/CPAN/Testers/WWW/Reports/Mailer.pm
Criterion Covered Total %
statement 105 564 18.6
branch 8 264 3.0
condition 1 138 0.7
subroutine 32 53 60.3
pod 4 4 100.0
total 150 1023 14.6


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

\s*]*>!!g if($SPONSORS[-1]{body});
1046 0 0         $SPONSORS[-1]{body} =~ s!<[^>]+>!!g if($SPONSORS[-1]{body});
1047             }
1048             }
1049              
1050 0           $self->_log( "INFO: " . scalar(@SPONSORS) . " Sponsors loaded\n" );
1051 0           $self->_log( "INFO: Sponsors: " . Dumper(\@SPONSORS) );
1052              
1053 0           $MT = Math::Random::MT->new(time);
1054             }
1055              
1056             sub _get_sponsor {
1057 0     0     my $self = shift;
1058 0           my $rand = $MT->rand(scalar(@SPONSORS));
1059 0           $self->_log( "INFO: Sponsors: rand=$rand: " . Dumper($SPONSORS[$rand]) );
1060 0           return $SPONSORS[$rand];
1061             }
1062              
1063             sub _log {
1064 0     0     my $self = shift;
1065 0 0         my $log = $self->logfile or return;
1066 0 0         mkpath(dirname($log)) unless(-f $log);
1067              
1068 0           my $t = localtime;
1069 0           my $s = $t->strftime("%Y/%m/%d %H:%M:%S");
1070              
1071 0 0         my $mode = $self->logclean ? 'w+' : 'a+';
1072 0           $self->logclean(0);
1073              
1074 0 0         my $fh = IO::File->new($log,$mode) or die "Cannot write to log file [$log]: $!\n";
1075 0           print $fh "$s: " . join(' ', @_);
1076 0           $fh->close;
1077             }
1078              
1079             sub _defined_or {
1080 0     0     my $self = shift;
1081 0           while(@_) {
1082 0           my $value = shift;
1083 0 0         return $value if(defined $value);
1084             }
1085              
1086 0           return;
1087             }
1088              
1089             1;
1090              
1091             __END__