File Coverage

blib/lib/MT/Import/Mbox.pm
Criterion Covered Total %
statement 24 210 11.4
branch 0 50 0.0
condition 0 9 0.0
subroutine 8 22 36.3
pod 1 13 7.6
total 33 304 10.8


line stmt bran cond sub pod time code
1             # $Id: Mbox.pm,v 1.13 2005/12/03 19:10:27 asc Exp $
2 1     1   1666 use strict;
  1         2  
  1         65  
3              
4             package MT::Import::Mbox;
5 1     1   7 use base qw (MT::Import::Base);
  1         2  
  1         1096  
6              
7             $MT::Import::Mbox::VERSION = '1.01';
8              
9             =head1 NAME
10              
11             MT::Import::Mbox - import one or more mbox messages in to a Movable Type weblog
12              
13             =head1 SYNOPSIS
14              
15             use strict;
16             use MT::Import::Mbox;
17              
18             my $cfg = Config::Simple->new("/path/to/mbox.cfg");
19             my $mt = MT::Import::Mbox->new($cfg);
20             $mt->verbose(1);
21              
22             $mt->import_mbox("/path/to/mbox");
23             $mt->rebuild();
24              
25             =head1 DESCRIPTION
26              
27             Import one or more mbox messages in to a Movable Type weblog.
28              
29             =head1 MOTIVATION
30              
31             I keep my email in a dated (YYYY/MM/DD) space. In a world where every
32             other way of organizing email messages kind of sucks, this one doesn't
33             necessarily suck any less but at least it's consistent.
34              
35             The downside to doing things this way is that there aren't any email
36             clients that are capable of doing multi-folder searches. When I need
37             to find something I am reduced to using tools like grep which, despite
38             having a certain charm, is pretty painful.
39              
40             Movable Type, with its built-in search and trackback widget seemed like
41             an interesting way to create a threaded read-only archive of my mail. I
42             could have built something from scratch (and I have) but MT was most
43             of the puzzle already completed.
44              
45             The idea of using tags and comments for annotating an email was also
46             intriguing. Using custom X-headers to tag an email from Ted as "asshole"
47             is great until you need to reply to that message and forget to purge
48             your personal notes.
49              
50             Also I was curious to see if, and how, it could be done and I hadn't played
51             with the MT source in a couple years.
52              
53             =head1 EMAIL TO MT MAPPINGS
54              
55             =over 4
56              
57             =item * B
58              
59             Message threads are handled using Movable Type's trackback features.
60              
61             =item * B
62              
63             Normalized email addresses are treated as Movable Type authors and
64             added to the database, as need.
65              
66             =item * B
67              
68             Normalized email addresses are treated as the primary category for
69             a message/post. The message's 'directional' headers for are treated
70             as the secondary category. For example :
71              
72             + foo@example.com
73              
74             - From
75             - Cc
76              
77             =item * B
78              
79             Multi-part MIME attachments are parsed using the Email::MIME package. In the
80             event that no attachments are found it returns the body of the orginal message
81             as a 'part', or attachment.
82              
83             If only one attachment exists it is used to generate the body of the entry.
84              
85             If the first attachment content-type is determined to be either plain-text
86             or HTML it is used to generate the body of the entry.
87              
88             All other attachments are written to disk. Attachments are written to disk as :
89              
90             /your/static/weblog/attachments/$YYYY/$MM/$DD/$MSG_ID/$FNAME
91              
92             $YYYY is the four digit year for the message as determined by its 'Date' header.
93             $MM and $DD the two digit month and day respectively.
94              
95             $MSG_ID is an MD5 digest of a normalized form of the message's Message-ID
96             header.
97              
98             $FNAME is an MD5 digest of the attachment's body. If it is possible to
99             determine the attachment's content-type then a suitable extension is
100             appended to $FNAME.
101              
102             A messsage's headers are stored as a separate attachment in :
103              
104             /your/static/weblog/attachments/$YYYY/$MM/$DD/$MSG_ID/headers.txt
105              
106             =back
107              
108             =head1 SETUP
109              
110             =head2 Versioning
111              
112             This package is designed to be used with Movable Type 3.2
113              
114             =head2 Templates
115              
116             =over 4
117              
118             =item * B template.
119              
120             You will need to copy and paste, or link to, the custom version
121             of this file located in :
122              
123             /path/to/MT-Import-Mbox-1.01/templates/archives.html
124              
125             =item * B template
126              
127             Ensure that it is enabled.
128              
129             You will need to copy and paste, or link to, the custom version
130             of this file located in :
131              
132             /path/to/MT-Import-Mbox-1.01/templates/date-based-archive.html
133              
134             =item * B template
135              
136             Ensure that it is enabled.
137              
138             You will need to copy and paste, or link to, the custom version
139             of this file located in :
140              
141             /path/to/MT-Import-Mbox-1.01/templates/date-based-archive-monthly.html
142              
143             =item * B template
144              
145             Ensure that it is enabled.
146              
147             =item * B template
148              
149             You will need to copy and paste, or link to, the custom version
150             of this file located in :
151              
152             /path/to/MT-Import-Mbox-1.01/templates/individual-entry-archive.html
153              
154             =back
155              
156             I
157              
158             =head2 Plugins
159              
160             In order to rebuild your templates with bi-directional trackback
161             threading, you will need to install the B plugin.
162              
163             cp /path/to/MT-Import-Mbox-1.01/plugins/pinged-by-entry \
164             /path/to/your/cgi-bin/mt/plugins/
165              
166             I
167              
168             =head2 Permissions
169              
170             Ensure that your MT installation is configured to allow both the
171             default CGI scripts B the scripts using this library sufficient
172             permissions to create an modify files in your (MT) static archive.
173              
174             =cut
175              
176 1     1   43713 use Digest::MD5 qw (md5_hex);
  1         2  
  1         70  
177              
178 1     1   2142 use Email::Folder;
  1         28169  
  1         39  
179 1     1   995 use Email::Find;
  1         165401  
  1         72  
180 1     1   1318 use Email::MIME;
  1         57806  
  1         46  
181              
182 1     1   1619 use File::Find::Rule;
  1         23994  
  1         10  
183 1     1   1705 use File::Temp qw (:POSIX);
  1         11574  
  1         2867  
184              
185             =head1 PACKAGE METHODS
186              
187             =head2 __PACKAGE__->new()
188              
189             =head1 CONFIG OPTIONS
190              
191             =head2 mt
192              
193             =over 4
194              
195             =item * B
196              
197             String. I
198              
199             The path to your Movable Type installation.
200              
201             =item * B
202              
203             Int. I
204              
205             The numberic ID of the Movable Type weblog you are posting to.
206              
207             =item * B
208              
209             Int. I
210              
211             The numberic ID of a Movable Type author with permissions to add
212             new authors to the Movable Type weblog you are posting to.
213              
214             =item * B
215              
216             String.
217              
218             The password to assign to any new authors you add to the Movable Type
219             weblog you are posting to.
220              
221             Default is "I".
222              
223             =item * B
224              
225             Int.
226              
227             The permissions set to grant any new authors you add to the Movable Type
228             weblog you are posting to.
229              
230             Default is I<514>, or the ability to add new categories.
231              
232             =back
233              
234             =head2 importer
235              
236             =over 4
237              
238             =item * B
239              
240             Boolean.
241              
242             Force a message to be reindexed, including any trackback pings and
243             attachments. If an entry matching the message id already exists in the
244             database it should only ever update or overwrite I data.
245              
246             Default is I
247              
248             =item * B
249              
250             Boolean.
251              
252             Enabled verbose logging.
253              
254             Default is I
255              
256             =back
257              
258             =head2 email
259              
260             =over 4
261              
262             =item * B
263              
264             A comma-separated list of email addresses that when present in the B
265             header will cause a message/post's primary category to be set as I
266             rather than I
267              
268             =back
269              
270             =cut
271              
272             # Defined in MT::Import::Base
273              
274             sub init {
275 0     0 0   my $self = shift;
276              
277 0 0         if (! $self->SUPER::init(@_)) {
278 0           return 0;
279             }
280              
281 0           my @personal = $self->{cfg}->param("email.personal");
282 0           $self->{'__personal'} = \@personal;
283            
284 0           return 1;
285             }
286              
287             =head1 OBJECT METHODS (you should care about)
288              
289             =cut
290              
291             =head2 $obj->import_mbox($path_to_mbox,%args)
292              
293             Where I<%args> are whatever valid parameters you can pass to
294             the I object constructor.
295              
296             =cut
297              
298             sub import_mbox {
299 0     0 1   my $self = shift;
300 0           my $mbox = shift;
301            
302 0           my $folder = Email::Folder->new($mbox,@_);
303            
304 0           while (my $msg = $folder->next_message()) {
305 0           $self->import_msg($msg);
306             }
307            
308 0           return 1;
309             }
310              
311             =head2 $obj->import_msg(Email::Simple)
312              
313             Returns an I object.
314              
315             =cut
316              
317             sub import_msg () {
318 0     0 0   my $self = shift;
319 0           my $msg = shift;
320            
321             #
322            
323 0           my $msg_id = $self->mk_id($msg->header("Message-Id"));
324            
325 0           my $entry = MT::Entry->load({blog_id => $self->blog_id(),
326             basename => $msg_id});
327            
328 0 0 0       if (($entry) && (! $self->{cfg}->param("importer.force"))) {
329 0           $self->log()->notice(sprintf("message %s already exists in the database with ID %d, skipping",
330             $msg_id,$entry->id()));
331            
332             # FIX ME : rebuild?
333 0           $self->imported($entry->id());
334 0           return 1;
335             }
336              
337             #
338              
339 0           my $reply = $self->mk_id($msg->header("In-Reply-To"));
340              
341 0           my $author = $msg->header("From");
342 0           my $first = ($self->find_addrs($author))[0];
343              
344 0           $author = $self->mk_author($first,$first);
345              
346 0   0       $entry ||= MT::Entry->new();
347 0           $entry->title($msg->header("Subject"));
348              
349 0           my $parsed = Email::MIME->new($msg->as_string());
350 0           my @parts = $parsed->parts();
351              
352             #
353            
354 0 0         if (scalar(@parts)==1) {
    0          
355 0           my $txt = $parts[0]->body();
356 0           $txt .= $self->mk_head($msg);
357 0           $self->set_entry_text($entry,$txt);
358             }
359            
360             elsif ($self->mk_extension($parts[0]->content_type()) =~ /^(txt|html)$/) {
361 0           my $first = shift @parts;
362            
363 0           my $txt = $first->body();
364 0           $txt .= $self->mk_uploads_text($msg,@parts);
365 0           $txt .= $self->mk_head($msg);
366              
367 0           $self->set_entry_text($entry,$txt);
368             }
369              
370             else {
371 0           my $txt = $self->mk_uploads_text($msg,@parts);
372 0           $txt .= $self->mk_head($msg);
373            
374 0           $self->set_entry_text($entry,$txt);
375             }
376              
377             #
378              
379 0           $entry->author_id($author->id());
380 0           $entry->blog_id($self->blog_id());
381            
382 0           $entry->allow_pings(1);
383 0           $entry->created_on($self->mk_date($msg->header("Date")));
384            
385 0           $entry->previous(1);
386 0           $entry->next(1);
387            
388 0           $entry->basename($msg_id);
389 0           $entry->status(MT::Entry::RELEASE());
390              
391 0 0         if (! $entry->save()) {
392 0           $self->log()->error("Can't save entry for message $msg_id, $!");
393 0           return 0;
394             }
395            
396 0           $self->log()->info(sprintf("Created entry %d for message %s\n",
397             $entry->id(),$msg_id));
398              
399             #
400              
401 0           my $tb = MT::Trackback->load({entry_id=>$entry->id()});
402              
403 0 0         if (! $tb) {
404 0           $tb = MT::Trackback->new();
405 0           $tb->blog_id($entry->blog_id());
406 0           $tb->entry_id($entry->id());
407 0           $tb->category_id(0);
408 0           $tb->title($entry->title());
409 0           $tb->description($entry->get_excerpt());
410 0           $tb->url($entry->permalink());
411 0           $tb->is_disabled(0);
412            
413 0 0         if (! $tb->save()) {
414 0           $self->log()->error("can not save trackback!, $!");
415             }
416             }
417            
418             #
419              
420 0           my $blog = MT::Blog->load($self->blog_id());
421 0           $blog->touch();
422            
423 0 0         if (! $blog->save()) {
424 0           $self->log()->error("Can't save , $!");
425 0           return 0;
426             }
427              
428             #
429              
430 0           my $from = $author->name();
431 0 0         my $pri = ($self->is_personal_address($from)) ? "Sent" : "Received";
432              
433 0           my $categories = $self->mk_categories($msg);
434            
435 0 0         if (scalar(@$categories)) {
436 0           $pri = $self->mk_category($pri,0);
437 0           $self->place_category($entry,$pri,1);
438              
439 0           foreach my $c (@$categories) {
440 0           $self->place_category($entry,$c,0);
441             }
442             }
443              
444 0 0         if ($reply) {
445 0           $self->ping_for_reply($entry,$reply,$msg->header("From"));
446             }
447            
448             #
449            
450 0           $self->imported($entry->id());
451 0           return $entry;
452             }
453              
454             sub is_personal_address {
455 0     0 0   my $self = shift;
456 0           my $addr = shift;
457              
458 0 0         return (grep /^$addr$/, @{$self->{'__personal'}}) ? 1 : 0;
  0            
459             }
460              
461             sub upload_part {
462 0     0 0   my $self = shift;
463 0           my $msg = shift;
464 0           my $part = shift;
465            
466 0 0         if ($part->body() eq "This is a multi-part message in MIME format.") {
467 0           return undef;
468             }
469            
470 0           my ($root,$url) = $self->mk_upload_root($msg);
471            
472 0           my $fname = $part->filename();
473 0           my $ext = undef;
474            
475 0 0         if (! $fname) {
476            
477 0           $fname = md5_hex($part->body());
478            
479 0 0         if ($ext = $self->mk_extension($part->content_type())) {
480 0           $fname = sprintf("%s.%s",$fname,$ext);
481             }
482             }
483            
484 0           $url .= "/$fname";
485            
486 0           my $full_path = File::Spec->catfile($root,$fname);
487            
488 0           my $fh = tmpfile();
489 0           $fh->print($part->body());
490              
491 0           my $uploaded = $self->upload_file(\*$fh,$full_path);
492              
493 0 0         if (! $uploaded) {
494 0           $self->log()->error("failed to upload part, $!");
495 0           return undef;
496             }
497            
498             #
499            
500 0           return qq();
501             }
502              
503             sub mk_id {
504 0     0 0   my $self = shift;
505 0           my $msg_id = shift;
506            
507 0           $msg_id =~ s/^
508 0           $msg_id =~ s/>$//;
509 0 0         return ($msg_id) ? md5_hex($msg_id) : undef;
510             }
511              
512             sub mk_categories {
513 0     0 0   my $self = shift;
514 0           my $msg = shift;
515              
516 0           my @cats = ();
517              
518 0           foreach my $header ("To","From","Cc","Bcc") {
519            
520 0           my @addrs = $self->find_addrs($msg->header($header));
521            
522 0 0         if (! @addrs) {
523 0           next;
524             }
525            
526 0           foreach my $addr (@addrs) {
527 0           my $cat = $self->mk_category(lc($addr),0);
528 0           push @cats, $cat;
529            
530 0           $self->log()->info(sprintf("add category : %s (%d)\n",$cat->label(),$cat->id()));
531              
532 0           my $rel = $self->mk_category($header,$cat->id());
533 0           push @cats, $rel;
534             }
535             }
536              
537 0           return \@cats;
538             }
539              
540             sub find_addrs {
541 0     0 0   my $self = shift;
542 0           my $str = shift;
543            
544 0           my %addrs = ();
545            
546             my $cb = sub {
547 0     0     my $email = shift;
548 0 0         if (my $fmt = $email->format()) {
549 0           $addrs{ $fmt } ++;
550             }
551 0           };
552            
553 0           my $finder = Email::Find->new($cb);
554 0           $finder->find(\$str);
555            
556 0           return keys %addrs;
557             }
558              
559             sub mk_extension {
560 0     0 0   my $self = shift;
561 0           my $type = shift;
562            
563 0 0         if ($type =~ m!text/plain!) {
    0          
    0          
    0          
564 0           return "txt";
565             }
566            
567             elsif ($type =~ m!text/html!) {
568 0           return "html";
569             }
570            
571             elsif ($type =~ m!image/(.*)!) {
572 0           return $1;
573             }
574            
575             elsif ($type =~ m!application/(.*)!) {
576 0           return $1;
577             }
578            
579             else {
580 0           return undef;
581             }
582             }
583              
584             sub mk_uploads_text {
585 0     0 0   my $self = shift;
586 0           my $msg = shift;
587 0           my @parts = @_;
588            
589 0           my $txt = "";
590            
591 0           map {
592 0           $txt .= $self->upload_part($msg,$_);
593             } @parts;
594            
595 0           return $txt;
596             }
597              
598             sub set_entry_text {
599 0     0 0   my $self = shift;
600 0           my $entry = shift;
601 0           my $txt = shift;
602              
603 0 0         if (length($txt)>200) {
604 0           my $excerpt = substr($txt,0,200);
605 0           $entry->excerpt($excerpt);
606 0           $entry->text($excerpt);
607 0           $entry->text_more($txt);
608             }
609              
610             else {
611 0           $entry->text($txt);
612             }
613              
614 0           return 1;
615             }
616              
617             sub mk_head {
618 0     0 0   my $self = shift;
619 0           my $msg = shift;
620            
621 0           my ($root,$url) = $self->mk_upload_root($msg);
622            
623 0           my $fname = "headers.txt";
624 0           my $path = File::Spec->catfile($root,$fname);
625            
626 0           open FH, ">$path";
627 0           print FH $msg->_headers_as_string();
628 0           close FH;
629            
630 0           return qq();
631             }
632              
633             sub mk_upload_root {
634 0     0 0   my $self = shift;
635 0           my $msg = shift;
636            
637 0           my $msg_id = $self->mk_id($msg->header("Message-Id"));
638 0           my $msg_date = $self->mk_date($msg->header("Date"));
639            
640 0           $msg_date =~ m!^(\d{4})(\d{2})(\d{2})!;
641 0           my ($yyyy,$mm,$dd) = ($1,$2,$3);
642            
643 0           my $blog = MT::Blog->load($self->blog_id());
644 0           my $fmgr = $blog->file_mgr();
645            
646 0           my $root = $blog->site_path();
647 0           my $url = $blog->site_url();
648            
649 0           $root = File::Spec->catdir($root,"attachments",$yyyy,$mm,$dd,$msg_id);
650 0           $url = "$url/attachments/$yyyy/$mm/$dd/$msg_id";
651            
652 0           $self->log()->debug("attachment root : $root\n");
653 0           $self->log()->debug("attachment URL : $url\n");
654            
655 0 0 0       if ((! $fmgr->exists($root)) && (! $fmgr->mkpath($root))) {
656 0           $self->log()->error("Failed to create '$root', $!");
657             }
658            
659 0           return ($root,$url);
660             }
661              
662             =head1 VERSION
663              
664             1.01
665              
666             =head1 DATE
667              
668             $Date: 2005/12/03 19:10:27 $
669              
670             =head1 AUTHOR
671              
672             Aaron Straup Cope Eascope@cpan.orgE
673              
674             =head1 SEE ALSO
675              
676             L
677              
678             http://pep.perl.org
679              
680             http://www.movabletype.org
681              
682             =head1 TO DO
683              
684             Write message body to disk and store as an attachment Render the
685             static version using MTInclude. This should allow for better
686             searching and indexing by third-party tools.
687              
688             =head1 BUGS
689              
690             Probably, or at least a handful of special-cases. Patches
691             are welcome. Please report all bugs via :
692              
693             L
694              
695             =head1 LICENSE
696              
697             Copyright (c) 2005 Aaron Straup Cope. All Rights Reserved.
698              
699             This is free software, you may use it and distribute it under
700             the same terms as Perl itself.
701              
702             =cut
703              
704             __END__