File Coverage

blib/lib/Mariachi.pm
Criterion Covered Total %
statement 45 315 14.2
branch 0 70 0.0
condition 0 26 0.0
subroutine 15 49 30.6
pod 16 18 88.8
total 76 478 15.9


line stmt bran cond sub pod time code
1 1     1   1365 use strict;
  1         3  
  1         38  
2             package Mariachi;
3 1     1   685 use Email::Thread;
  1         53422  
  1         30  
4 1     1   986 use Template;
  1         23636  
  1         84  
5 1     1   1193 use Time::HiRes qw( gettimeofday tv_interval );
  1         2153  
  1         6  
6 1     1   1281 use Storable qw( store retrieve );
  1         3687  
  1         81  
7 1     1   8 use File::Path qw( mkpath );
  1         1  
  1         55  
8 1     1   894 use File::Copy qw( copy move );
  1         2630  
  1         66  
9 1     1   6 use File::Find::Rule;
  1         2  
  1         12  
10 1     1   48 use File::Basename;
  1         2  
  1         65  
11 1     1   5 use base qw( Class::Accessor::Fast );
  1         2  
  1         3882  
12             our $VERSION = '0.52';
13              
14             __PACKAGE__->mk_accessors( qw( config messages rootset
15             start_time last_time tt ) );
16              
17             =head1 NAME
18              
19             Mariachi - all dancing mail archive generator
20              
21             =head1 DESCRIPTION
22              
23             =head1 ACESSORS
24              
25             =head2 ->config
26              
27             An L object containing the current configuration. See
28             L for details of the configurable items.
29              
30             =head2 ->messages
31              
32             The current set of messages
33              
34             =head2 ->rootset
35              
36             The rootset of threaded messages
37              
38             =head2 ->start_time
39              
40             =head2 ->last_time
41              
42             Used internally by the C<_bench> method
43              
44              
45             =head1 METHODS
46              
47             All of these are instance methods, unless stated.
48              
49             =head2 ->new( %initial_values )
50              
51             your general class-method constructor
52              
53             =cut
54              
55             sub new {
56 0     0 1   my $class = shift;
57 0           $class->SUPER::new({@_});
58             }
59              
60             sub _bench {
61 0     0     my $self = shift;
62 0           my $message = shift;
63              
64 0           my $now = [gettimeofday];
65 0           my $start = $self->start_time;
66 0   0       my $last = $self->last_time || $now;
67 0 0         $start = $self->start_time($now) unless $start;
68              
69 0           printf "%-50s %.3f elapsed %.3f total\n",
70             $message, tv_interval( $last, $now ), tv_interval( $start, $now );
71              
72 0           $self->last_time($now);
73             }
74              
75             =head2 ->load
76              
77             populate C from C
78              
79             =cut
80              
81             sub load {
82 0     0 1   my $self = shift;
83              
84 0 0         my $folder = Mariachi::Folder->new( $self->config->input )
85             or die "Unable to open ".$self->config->input;
86              
87 0           $| = 1;
88 0           my $cache;
89 0 0         $cache = $self->config->input.".cache" if $ENV{M_CACHE};
90 0 0 0       if ($cache && -e $cache && !$self->config->refresh) {
      0        
91 0           print "pulling in $cache\n";
92 0           $self->messages( retrieve( $cache ) );
93 0           return;
94             }
95              
96 0           my $count = 0;
97 0           my @msgs;
98 0           while (my $msg = $folder->next_message) {
99 0           push @msgs, $msg;
100 0 0         print STDERR "\r$count messages" if ++$count % 100 == 0;
101             }
102 0           print STDERR "\n";
103              
104 0 0         if ($cache) {
105 0           print "caching\n";
106 0           store( \@msgs, $cache );
107             }
108              
109 0           $self->messages( \@msgs );
110             }
111              
112             =head2 ->dedupe
113              
114             remove duplicates from C
115              
116             =cut
117              
118             sub dedupe {
119 0     0 1   my $self = shift;
120              
121 0           my (%seen, @new, $dropped);
122 0           $dropped = 0;
123 0           for my $mail (@{ $self->messages }) {
  0            
124 0           my $msgid = $mail->header('message-id');
125 0 0         if ($seen{$msgid}++) {
126 0           $dropped++;
127 0           next;
128             }
129 0           push @new, $mail;
130             }
131 0           print "dropped $dropped duplicate messages\n";
132 0           $self->messages(\@new);
133             }
134              
135             =head2 ->sanitise
136              
137             some messages have been near mail2news gateways, which means that some
138             message ids in the C and C headers get munged
139             like so: <$group/$message_id>
140              
141             fix this in C
142              
143             =cut
144              
145             sub sanitise {
146 0     0 1   my $self = shift;
147              
148 0           for my $mail (@{ $self->messages }) {
  0            
149 0           for (qw( references in_reply_to )) {
150 0 0         my $hdr = $mail->header($_) or next;
151 0           my $before = $hdr;
152 0 0         $hdr =~ s{<[^>]*?/}{<}g or next;
153             #print "$_ $before$_: $hdr";
154 0           $mail->header_set($_, $hdr);
155             }
156             }
157             }
158              
159             =head2 ->thread
160              
161             populate C with an Email::Thread::Containers created from
162             C
163              
164             =cut
165              
166             # the Fisher-Yates shuffle from perlfaq4
167             sub _shuffle {
168 0     0     my $array = shift;
169 0           my $i;
170 0           for ($i = @$array; --$i; ) {
171 0           my $j = int rand ($i+1);
172 0           @$array[$i,$j] = @$array[$j,$i];
173             }
174             }
175              
176             sub thread {
177 0     0 1   my $self = shift;
178             #_shuffle $self->messages;
179 0           my $threader = Email::Thread->new( @{ $self->messages } );
  0            
180 0           $threader->thread;
181 0           $self->rootset( [ grep { $_->topmost } $threader->rootset ] );
  0            
182             }
183              
184             =head2 ->order
185              
186             order C by date
187              
188             =cut
189              
190             sub order {
191 0     0 1   my $self = shift;
192              
193 0           my @rootset = @{ $self->rootset };
  0            
194             $_->order_children(
195             sub {
196             sort {
197 0     0     eval { $a->topmost->message->epoch_date } <=>
  0            
198 0           eval { $b->topmost->message->epoch_date }
  0            
199             } @_
200 0           }) for @rootset;
201              
202             # we actually want the root set to be ordered latest first
203 0           @rootset = sort {
204 0           $b->topmost->message->epoch_date <=> $a->topmost->message->epoch_date
205             } @rootset;
206              
207             # And optionally reverse the order
208 0 0         @rootset = reverse(@rootset)
209             if $self->config->reverse;
210              
211 0           $self->rootset( \@rootset );
212             }
213              
214             =head2 ->sanity
215              
216             (in)sanity test - check everything in C is reachable when
217             walking C
218              
219             =cut
220              
221             sub sanity {
222 0     0 1   my $self = shift;
223              
224 0           my %mails = map { $_ => $_ } @{ $self->messages };
  0            
  0            
225 0           my $count;
226             my $check = sub {
227 0 0   0     my $cont = shift or return;
228 0 0         my $mail = $cont->message or return;
229 0           ++$count;
230             #print STDERR "\rverify $count";
231 0   0       delete $mails{ $mail || '' };
232 0           };
233 0           $_->iterate_down( $check ) for @{ $self->rootset };
  0            
234 0           undef $check;
235             #print STDERR "\n";
236              
237 0 0         return unless %mails;
238 0           die "\nDidn't see ".(scalar keys %mails)." messages";
239 0           print join "\n", map {
240 0           my @ancestors;
241 0           my $x = $_->container;
242 0           my %seen;
243             my $last;
244 0           while ($x) {
245 0 0         if ($seen{$x}++) { push @ancestors, "$x ancestor loop!\n"; last }
  0            
  0            
246 0           my $extra = $x->{id};
247 0           $extra .= " one-way"
248 0 0 0       if $last && !grep { $last == $_ } $x->children;
249 0           push @ancestors, $x." $extra";
250 0           $last = $x;
251 0           $x = $x->parent;
252             }
253 0           $_->header("message-id"), @ancestors
254             } values %mails;
255              
256             }
257              
258             =head2 ->strand
259              
260             run a strand through all C - wander over C setting
261             the Message ->next and ->prev links
262              
263             =cut
264              
265             sub strand {
266 0     0 1   my $self = shift;
267              
268 0           my $prev;
269 0           for my $root (@{ $self->rootset }) {
  0            
270             my $sub = sub {
271 0 0   0     my $mail = $_[0]->message or return;
272 0 0         $prev->next($mail) if $prev;
273 0           $mail->prev($prev);
274 0           $mail->root($root);
275 0           $prev = $mail;
276 0           };
277              
278 0           $root->iterate_down( $sub );
279 0           undef $sub;
280             }
281             }
282              
283             =head2 ->split_deep
284              
285             wander over C reparenting subthreads that are
286             considered too deep
287              
288             =cut
289              
290             sub split_deep {
291 0     0 1   my $self = shift;
292              
293 0           my @toodeep;
294 0           for my $root (@{ $self->rootset }) {
  0            
295             my $sub = sub {
296 0     0     my ($cont, $depth) = @_;
297              
298             # only note first entries
299 0 0 0       if ($depth && ($depth % 6 == 0)
      0        
300             && $cont->parent->child == $cont) {
301 0           push @toodeep, $cont;
302             }
303 0           };
304              
305 0           $root->iterate_down( $sub );
306 0           undef $sub;
307             }
308              
309 0           print "splicing threads in ", scalar @toodeep, " places\n";
310 0           for (@toodeep) {
311             # the top one needs to be empty, because we're cheating.
312             # to keep references straight, we'll move its content
313 0           my $top = $_->topmost;
314 0 0         my $root = $top->message->root or die "batshit!";
315 0 0         if ($root->message) {
316 0           my $new = Mail::Thread::Container->new($root->messageid);
317 0           $root->messageid('dummy');
318 0           $new->message($root->message);
319 0           $root->message(undef);
320 0           $new->child($root->child);
321 0           $root->child($new);
322 0           $root = $new;
323             }
324 0           $root->add_child( $_ );
325             }
326             }
327              
328              
329             =head2 ->copy_files
330              
331             copy files into the output dir
332              
333             =cut
334              
335              
336             sub copy_files {
337 0     0 1   my $self = shift;
338              
339 0           for my $dir (@{ $self->config->templates }) {
  0            
340 0           my @files = map {
341 0           s{$dir/?}{}; $_
  0            
342             } find( or => [ find( directory =>
343             name => [ qw( CVS .svn ) ],
344             prune =>
345             discard => ),
346             find( file => '!name' => [ '*.tt2', '*~', '*.bak' ] )
347             ],
348             in => $dir );
349 0           for (@files) {
350 0           mkpath dirname $self->config->output . "/$_";
351 0 0         copy( "$dir/$_", $self->config->output . "/$_" )
352             or die "couldn't copy $dir/$_ $!";
353             }
354             }
355             }
356              
357              
358             =head2 init_tt
359              
360             =cut
361              
362             sub init_tt {
363 0     0 1   my $self = shift;
364              
365 0           $self->tt(
366             Template->new(
367 0           INCLUDE_PATH => join(':', reverse @{ $self->config->templates }),
368             RECURSION => 1
369             )
370             );
371             }
372              
373              
374             =head2 generate_pages( $template, $filename, %data )
375              
376             =cut
377              
378             sub nthpage {
379 0     0 0   my $self = shift;
380 0           my $n = shift;
381 0           my $page = shift;
382 0 0         return $page if $n == 1;
383 0           --$n;
384 0           $page =~ s/\./_$n./;
385 0           return $page;
386             }
387              
388             sub generate_pages {
389 0     0 1   my $self = shift;
390 0           my $template = shift;
391 0           my $spool = shift;
392              
393 0           my $again;
394 0           do {
395 0           my $file = $spool;
396             $self->tt->process(
397             $template,
398             { @_,
399             mariachi => $self,
400             spool => $spool,
401             # callbacktastic
402 0     0     nthpage => sub { $self->nthpage( shift, $spool ) },
403 0     0     again => sub { $again },
404 0     0     file => sub { $file },
405 0     0     set_again => sub { $again = shift; return },
  0            
406 0     0     set_file => sub { $file = shift; return }, },
  0            
407 0 0         $self->config->output . "/$$.tmp" )
408             or die $self->tt->error;
409              
410 0           mkpath dirname $self->config->output . "/$file";
411 0 0         move $self->config->output . "/$$.tmp", $self->config->output . "/$file"
412             or die "$!";
413             } while $again;
414             }
415              
416              
417             =head2 ->generate_lurker_index
418              
419             =cut
420              
421             sub generate_lurker {
422 0     0 0   my $self = shift;
423              
424 0 0         return unless $self->config->lurker;
425              
426 0           my $l = Mariachi::Lurker->new;
427 0           $self->generate_pages(
428             'lurker.tt2', 'lurker.html',
429             content => [
430 0           map { [ $l->arrange( $_ ) ] } @{ $self->rootset }
  0            
431             ],
432             perpage => 10,
433             );
434             }
435              
436              
437             =head2 ->generate_thread
438              
439             =cut
440              
441             sub generate_thread {
442 0     0 1   my $self = shift;
443              
444 0           $self->generate_pages(
445             'index.tt2', 'index.html',
446             content => $self->rootset,
447             perpage => 20,
448             );
449             }
450              
451              
452             =head2 ->generate_date
453              
454             =cut
455              
456             sub generate_date {
457 0     0 1   my $self = shift;
458              
459 0           my %touched_dates;
460             my %dates;
461              
462             # wander things to find dirty threads, and dates
463 0           for my $root (@{ $self->rootset }) {
  0            
464 0           my $sub;
465             $sub = sub {
466 0 0   0     my $c = shift or return;
467              
468 0 0         if (my $mail = $c->message) {
469             # mark the thread dirty, if the message is new
470 0 0 0       unless (-e $self->config->output."/".$mail->filename &&
471             !$self->config->refresh) {
472             # dirty up the date indexes
473 0           $touched_dates{ $mail->year } = 1;
474 0           $touched_dates{ $mail->month } = 1;
475 0           $touched_dates{ $mail->day } = 1;
476             }
477              
478             # add things to the date indexes
479 0           push @{ $dates{ $mail->year } }, $mail;
  0            
480 0           push @{ $dates{ $mail->month } }, $mail;
  0            
481 0           push @{ $dates{ $mail->day } }, $mail;
  0            
482             }
483 0           };
484 0           $root->iterate_down($sub);
485 0           undef $sub; # since we closed over ourself, we'll have to be specific
486             }
487              
488 0           for ( keys %touched_dates ) {
489 0           my @mails = sort {
490 0           $a->epoch_date <=> $b->epoch_date
491 0           } @{ $dates{$_} };
492              
493 0           my @depth = split m!/!;
494 0           $self->generate_pages( 'date.tt2', "$_/index.html",
495             archive_date => $_,
496             content => \@mails,
497             base => "../" x @depth,
498             perpage => 20,
499             );
500             }
501             }
502              
503             =head2 ->generate_bodies
504              
505             render thread tree into the directory of C
506              
507             =cut
508              
509             sub generate_bodies {
510 0     0 1   my $self = shift;
511              
512 0           my %touched_threads;
513             # wander things to find dirty threads
514 0           for my $root (@{ $self->rootset }) {
  0            
515 0           my $sub;
516             $sub = sub {
517 0 0   0     if (my $mail = eval { $_[0]->message }) {
  0            
518             # mark the thread dirty, if the message is new
519 0 0 0       $touched_threads{ $root } = $root
520             unless -e $self->config->output."/".$mail->filename
521             && !$self->config->refresh;
522             }
523 0           };
524 0           $root->iterate_down($sub);
525 0           undef $sub; # since we closed over ourself, we'll have to be specific
526             }
527              
528             # figure out adjacent dirty threads
529 0           my @threads = @{ $self->rootset };
  0            
530 0           for my $i (grep { $touched_threads{ $threads[$_] } } 0..$#threads) {
  0            
531 0 0         $touched_threads{ $threads[$i-1] } = $threads[$i-1] if $i > 0;
532 0 0         $touched_threads{ $threads[$i+1] } = $threads[$i+1] if $i+1 < @threads;
533             }
534              
535             # and then render all the messages in the dirty threads
536 0           my $count = 0;
537 0           my $tt = $self->tt;
538 0           for my $root (values %touched_threads) {
539             my $sub = sub {
540 0 0   0     my $mail = $_[0]->message or return;
541 0 0         print STDERR "\rmessage $count" if ++$count % 100 == 0;
542              
543 0 0         $tt->process('message.tt2',
544             { base => '../../../',
545             mariachi => $self,
546             thread => $root,
547             message => $mail,
548             container => $_[0],
549             },
550             $self->config->output . "/" . $mail->filename)
551             or die $tt->error;
552 0           };
553 0           $root->recurse_down( $sub );
554 0           undef $sub;
555             }
556 0           print STDERR "\n";
557             }
558              
559             =head2 ->perform
560              
561             do all the right steps
562              
563             =cut
564              
565             sub perform {
566 0     0 1   my $self = shift;
567              
568 0           $self->_bench("reticulating splines");
569 0           $self->load; $self->_bench("load ".scalar @{ $self->messages });
  0            
  0            
570 0           $self->dedupe; $self->_bench("dedupe");
  0            
571             #$self->sanitise; $self->_bench("sanitise");
572 0           $self->thread; $self->_bench("thread");
  0            
573 0           $self->sanity; $self->_bench("sanity");
  0            
574 0           $self->order; $self->_bench("order");
  0            
575 0           $self->sanity; $self->_bench("sanity");
  0            
576 0           $self->copy_files; $self->_bench("copy files");
  0            
577 0           $self->init_tt; $self->_bench("tt init");
  0            
578 0           $self->generate_lurker; $self->_bench("lurker output");
  0            
579 0           $self->strand; $self->_bench("strand");
  0            
580 0           $self->split_deep; $self->_bench("deep threads split up");
  0            
581 0           $self->sanity; $self->_bench("sanity");
  0            
582 0           $self->order; $self->_bench("order");
  0            
583 0           $self->generate_thread; $self->_bench("regular thread indexes");
  0            
584 0           $self->generate_date; $self->_bench("date indexes");
  0            
585 0           $self->generate_bodies; $self->_bench("messages");
  0            
586             }
587              
588             package Mariachi::Folder;
589 1     1   9 use Mariachi::Message;
  1         3  
  1         27  
590 1     1   871 use Email::Folder;
  1         1860  
  1         28  
591 1     1   5 use base 'Email::Folder';
  1         3  
  1         116  
592              
593 0     0     sub bless_message { Mariachi::Message->new($_[1]) }
594              
595             package Mariachi::Lurker;
596 1     1   819 use Mail::Thread::Chronological;
  1         2196  
  1         33  
597 1     1   7 use base 'Mail::Thread::Chronological';
  1         1  
  1         118  
598              
599 0     0     sub extract_time { $_[1]->message->epoch_date }
600              
601             1;
602              
603             __END__