File Coverage

blib/lib/IMAP/BodyStructure.pm
Criterion Covered Total %
statement 184 193 95.3
branch 68 78 87.1
condition 15 24 62.5
subroutine 27 29 93.1
pod 8 8 100.0
total 302 332 90.9


line stmt bran cond sub pod time code
1             package IMAP::BodyStructure;
2 2     2   49579 use strict;
  2         5  
  2         77  
3              
4             =head1 NAME
5              
6             IMAP::BodyStructure - IMAP4-compatible BODYSTRUCTURE and ENVELOPE parser
7              
8             =head1 SYNOPSIS
9            
10             use IMAP::BodyStructure;
11              
12             # $imap is a low-level IMAP-client with an ability to fetch items
13             # by message uids
14              
15             my $bs = new IMAP::BodyStructure
16             $imap->imap_fetch($msg_uid,
17             'BODYSTRUCTURE', 1)->[0]->{BODYSTRUCTURE};
18              
19             print "[UID:$msg_uid] message is in Russian. Sure.\n"
20             if $bs->charset =~ /(?:koi8-r|windows-1251)/i;
21              
22             my $part = $bs->part_at('1.3');
23             $part->type =~ m#^image/#
24             and print "The 3rd part is an image named \""
25             . $part->filename . "\"\n";
26              
27             =head1 DESCRIPTION
28              
29             An IMAP4-compatible IMAP server MUST include a full MIME-parser which
30             parses the messages inside IMAP mailboxes and is accessible via
31             BODYSTRUCTURE fetch item. This module provides a Perl interface to
32             parse the output of IMAP4 MIME-parser. Hope no one will have problems
33             with parsing this doc.
34              
35             It is a rather straightforward C-style parser and is
36             therefore much, much faster then the venerable L
37             which is based on a L grammar. I believe it is also
38             more correct when parsing nested multipart C parts. See
39             testsuite if interested.
40              
41             I'd also like to emphasize that I
42             client!> You will need to employ one from CPAN, there are many. A
43             section with examples of getting to a BODYSTRUCTURE fetch item with
44             various Perl IMAP clients available on CPAN would greatly
45             enhance this document.
46              
47             =head1 INTERFACE
48              
49             =cut
50              
51 2     2   46 use 5.005;
  2         8  
52              
53 2     2   10 use vars qw/$VERSION/;
  2         8  
  2         265  
54              
55             $VERSION = '1.03';
56              
57             sub _get_envelope($\$);
58             sub _get_bodystructure(\$;$$);
59             sub _get_npairs(\$);
60             sub _get_ndisp(\$);
61             sub _get_nstring(\$);
62             sub _get_lang(\$);
63              
64             =head2 METHODS
65              
66             =over 4
67              
68             =item new($)
69              
70             The constructor does most of the work here. It initializes the
71             hierarchial data structure representing all the message parts and their
72             properties. It takes one argument which should be a string returned
73             by IMAP server in reply to a FETCH command with BODYSTRUCTURE item.
74              
75             All the parts on all the levels are represented by IMAP::BodyStructure
76             objects and that enables the uniform access to them. It is a direct
77             implementation of the Composite Design Pattern.
78              
79             =cut
80              
81 2         13 use fields qw/type encoding size disp params parts desc bodystructure
82 2     2   1883 part_id cid textlines md5 lang loc envelope/;
  2         3442  
83              
84             sub new {
85 13     13 1 620 my $class = shift;
86 13   33     58 $class = ref $class || $class;
87 13         22 my $imap_str= shift;
88              
89 13         31 return _get_bodystructure($imap_str, $class);
90             }
91              
92             =item type()
93              
94             Returns the MIME type of the part. Expect something like C
95             or C.
96              
97             =item encoding()
98              
99             Returns the MIME encoding of the part. This is usually one of '7bit',
100             '8bit', 'base64' or 'quoted-printable'.
101              
102             =item size()
103              
104             Returns the size of the part in octets. It is I the size of the
105             data in the part, which may be encoded as quoted-printable leaving us
106             without an obvious method of calculating the exact size of original
107             data.
108              
109             =cut
110              
111             for my $field (qw/type encoding size/) {
112 2     2   400 no strict 'refs';
  2         3  
  2         4651  
113 123     123   406 *$field = sub { return $_[0]->{$field} };
114             }
115              
116             =item disp()
117              
118             Returns the content-disposition of the part. One of 'inline' or
119             'attachment', usually. Defaults to inline, but you should remember
120             that if there IS a disposition but you cannot recognize it than act as
121             if it's 'attachment'. And use case-insensitive comparisons.
122              
123             =cut
124              
125             sub disp {
126 2     2 1 3 my $self = shift;
127              
128 2 50 50     15 return $self->{disp} ? $self->{disp}->[0] || 'inline' : 'inline';
129             }
130              
131             =item charset()
132              
133             Returns the charset of the part OR the charset of the first nested
134             part. This looks like a good heuristic really. Charset is something
135             resembling 'UTF-8', 'US-ASCII', 'ISO-8859-13' or 'KOI8-R'. The standard
136             does not say it should be uppercase, by the way.
137              
138             Can be undefined.
139              
140             =cut
141              
142             sub charset {
143 2     2 1 343 my $self = shift;
144              
145             # get charset from params OR dive into the first part
146             return $self->{params}->{charset}
147 2   0     16 || ($self->{parts} && @{$self->{parts}} && $self->{parts}->[0]->charset)
148             || undef; # please oh please, no '' or '0' charsets
149             }
150              
151             =item filename()
152              
153             Returns the filename specified as a part of Content-Disposition
154             header.
155              
156             Can be undefined.
157              
158             =cut
159              
160             sub filename {
161 2     2 1 3 my $self = shift;
162              
163 2         11 return $self->{disp}->[1]->{filename};
164             }
165              
166             =item description()
167              
168             Returns the description of the part.
169              
170             =cut
171              
172             sub description {
173 0     0 1 0 my $self = shift;
174              
175 0         0 return $self->{desc};
176             }
177              
178             =item parts(;$)
179              
180             This sub acts differently depending on whether you pass it an
181             argument or not.
182              
183             Without any arguments it returns a list of parts in list context and
184             the number in scalar context.
185              
186             Specifying a scalar argument allows you to get an individual part with
187             that index.
188              
189             I
190             etc. but IMAP::BodyStructure objects containing information about the
191             message parts which was extracted from parsing BODYSTRUCTURE IMAP
192             response!>
193              
194             =cut
195              
196             sub parts {
197 14     14 1 19 my $self = shift;
198 14         16 my $arg = shift;
199              
200 14 100       30 if (defined $arg) {
201 13         57 return $self->{parts}->[$arg];
202             } else {
203 1 50       3 return wantarray ? @{$self->{parts}} : scalar @{$self->{parts}};
  0         0  
  1         5  
204             }
205             }
206              
207             =item part_at($)
208              
209             This method returns a message part by its path. A path to a part in
210             the hierarchy is a dot-separated string of part indices. See L for
211             an example. A nested C does not add a hierarchy level
212             UNLESS it is a single part of another C part (with no
213             C levels in between). Instead, it has an additional
214             C<.TEXT> part which refers to the internal IMAP::BodyStructure object.
215             Look, here is an outline of an example message structure with part
216             paths alongside each part.
217              
218             multipart/mixed 1
219             text/plain 1.1
220             application/msword 1.2
221             message/rfc822 1.3
222             multipart/alternative 1.3.TEXT
223             text/plain 1.3.1
224             multipart/related 1.3.2
225             text/html 1.3.2.1
226             image/png 1.3.2.2
227             image/png 1.3.2.3
228              
229             This is a text email with two attachments, one being an MS Word document,
230             and the other is itself a message (probably a forward) which is composed in a
231             graphical MUA and contains two alternative representations, one
232             plain text fallback and one HTML with images (bundled as a
233             C).
234              
235             Another one with several levels of C. This one is hard
236             to compose in a modern MUA, however.
237              
238             multipart/mixed 1
239             text/plain 1.1
240             message/rfc822 1.2
241             message/rfc822 1.2.TEXT
242             text/plain 1.2.1
243              
244             =cut
245              
246             sub part_at {
247 33     33 1 985 my $self = shift;
248 33         44 my $path = shift;
249              
250 33         92 return $self->_part_at(split /\./, $path);
251             }
252              
253             sub _part_at {
254 77     77   83 my $self = shift;
255 77         129 my @parts = @_;
256              
257 77 100       215 return $self unless @parts; # (cond ((null? l) s)
258              
259 60         80 my $part_num = shift @parts; # (car l)
260              
261 60 100       113 if ($self->type =~ /^multipart\//) {
    100          
262 33 100       85 if (exists $self->{parts}->[$part_num - 1]) {
263 27         70 return $self->{parts}->[$part_num - 1]->_part_at(@parts);
264             } else {
265 6         28 return;
266             }
267             } elsif ($self->type eq 'message/rfc822') {
268 22 100       61 return $self->{bodystructure} if $part_num eq 'TEXT';
269              
270 18 100       36 if ($self->{bodystructure}->type =~ m{^ multipart/ | ^ message/rfc822 \z}xms) {
271 12         27 return $self->{bodystructure}->_part_at($part_num, @parts);
272             } else {
273 6 100       24 return $part_num == 1 ? $self->{bodystructure}->_part_at(@parts) : undef;
274             }
275             } else {
276             # there's no included parts in single non-rfc822 parts
277             # so if you still want one you get undef
278 5 100 66     32 if ($part_num && $part_num ne '1' || @parts) {
      100        
279 3         18 return;
280             } else {
281 2         10 return $self;
282             }
283             }
284             }
285              
286             =item part_path()
287              
288             Returns the part path to the current part.
289              
290             =back
291              
292             =head2 DATA MEMBERS
293              
294             These are additional pieces of information returned by IMAP server and
295             parsed. They are rarely used, though (and rarely defined too, btw), so
296             I chose not to provide access methods for them.
297              
298             =over 4
299              
300             =item params
301              
302             This is a hashref of MIME parameters. The only interesting param is
303             charset and there's a shortcut method for it.
304              
305             =item lang
306              
307             Content language.
308              
309             =item loc
310              
311             Content location.
312              
313             =item cid
314              
315             Content ID.
316              
317             =item md5
318              
319             Content MD5. No one seems to bother with calculating and it is usually
320             undefined.
321              
322             =back
323              
324             B and B members exist only in singlepart parts.
325              
326             =cut
327              
328             sub part_path {
329 0     0 1 0 my $self = shift;
330              
331 0         0 return $self->{part_id};
332             }
333              
334             sub _get_envelope($\$) {
335 7     7   429 eval "$_[0]::Envelope->new(\$_[1])";
336             }
337              
338             sub _get_bodystructure(\$;$$) {
339 66     66   79 my $str = shift;
340 66   50     142 my $class = shift || __PACKAGE__;
341 66         72 my $id = shift;
342              
343 66         156 my __PACKAGE__ $bs = fields::new($class);
344 66   100     13478 $bs->{part_id} = $id || 1; # !defined $id --> top-level message
345             # and single-part has one part with part_id 1
346              
347 66 100       140 my $id_prefix = $id ? "$id." : '';
348              
349 66 100       325 $$str =~ m/\G\s*(?:\(BODYSTRUCTURE\s*)?\(/gc
350             or return 0;
351              
352 53         92 $bs->{parts} = [];
353 53 100       134 if ($$str =~ /\G(?=\()/gc) {
354             # multipart
355 13         21 $bs->{type} = 'multipart/';
356 13         14 my $part_id = 1;
357 13         25 $id_prefix =~ s/\.?TEXT//;
358 13         53 while (my $part_bs = _get_bodystructure($$str, $class, $id_prefix . $part_id++)) {
359 33         34 push @{$bs->{parts}}, $part_bs;
  33         135  
360             }
361              
362 13         33 $bs->{type} .= lc(_get_nstring($$str));
363 13         35 $bs->{params} = _get_npairs($$str);
364 13         27 $bs->{disp} = _get_ndisp($$str);
365 13         25 $bs->{lang} = _get_lang($$str);
366 13         25 $bs->{loc} = _get_nstring($$str);
367             } else {
368 40         94 $bs->{type} = lc (_get_nstring($$str) . '/' . _get_nstring($$str));
369 40         88 $bs->{params} = _get_npairs($$str);
370 40         76 $bs->{cid} = _get_nstring($$str);
371 40         69 $bs->{desc} = _get_nstring($$str);
372 40         70 $bs->{encoding} = _get_nstring($$str);
373 40         93 $bs->{size} = _get_nstring($$str);
374              
375 40 100       147 if ($bs->{type} eq 'message/rfc822') {
    100          
376 7         18 $bs->{envelope} = _get_envelope($class, $$str);
377 7 100       29 if ($id_prefix =~ s/\.?TEXT//) {
378 1         4 $bs->{bodystructure} = _get_bodystructure($$str, $class, $id_prefix . '1');
379             } else {
380 6         19 $bs->{bodystructure} = _get_bodystructure($$str, $class, $id_prefix . 'TEXT');
381             }
382 7         31 $bs->{textlines} = _get_nstring($$str);
383             } elsif ($bs->{type} =~ /^text\//) {
384 23         41 $bs->{textlines} = _get_nstring($$str);
385             }
386              
387 40         69 $bs->{md5} = _get_nstring($$str);
388 40         72 $bs->{disp} = _get_ndisp($$str);
389 40         78 $bs->{lang} = _get_lang($$str);
390 40         66 $bs->{loc} = _get_nstring($$str);
391             }
392              
393 53         105 $$str =~ m/\G\s*\)/gc;
394              
395 53         166 return $bs;
396             }
397              
398             sub _get_ndisp(\$) {
399 53     53   61 my $str = shift;
400              
401 53         83 $$str =~ /\G\s+/gc;
402              
403 53 100       157 if ($$str =~ /\GNIL/gc) {
    50          
404 23         36 return undef;
405             } elsif ($$str =~ m/\G\s*\(/gc) {
406 30         33 my @disp;
407              
408 30         53 $disp[0] = _get_nstring($$str);
409 30         57 $disp[1] = _get_npairs($$str);
410              
411 30         59 $$str =~ m/\G\s*\)/gc;
412 30         60 return \@disp;
413             }
414            
415 0         0 return 0;
416             }
417              
418             sub _get_npairs(\$) {
419 83     83   92 my $str = shift;
420              
421 83         139 $$str =~ /\G\s+/gc;
422              
423 83 100       262 if ($$str =~ /\GNIL/gc) {
    50          
424 27         46 return undef;
425             } elsif ($$str =~ m/\G\s*\(/gc) {
426 56         59 my %r;
427 56         57 while ('fareva') {
428 114         192 my ($key, $data) = (_get_nstring($$str), _get_nstring($$str));
429 114 100       248 $key or last;
430              
431 58         132 $r{$key} = $data;
432             }
433              
434 56         110 $$str =~ m/\G\s*\)/gc;
435 56         114 return \%r;
436             }
437            
438 0         0 return 0;
439             }
440              
441             sub _get_nstring(\$) {
442 791     791   6591 my $str = $_[0];
443              
444             # nstring = string / nil
445             # nil = "NIL"
446             # string = quoted / literal
447             # quoted = DQUOTE *QUOTED-CHAR DQUOTE
448             # QUOTED-CHAR = /
449             # "\" quoted-specials
450             # quoted-specials = DQUOTE / "\"
451             # literal = "{" number "}" CRLF *CHAR8
452             # ; Number represents the number of CHAR8s
453              
454             # astring = 1*(any CHAR except "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials)
455              
456 791         1286 $$str =~ /\G\s+/gc;
457              
458 791 100       3019 if ($$str =~ /\GNIL/gc) {
    100          
    100          
    100          
459 206         379 return undef;
460             } elsif ($$str =~ m/\G(\"(?>[^\\\"]*(?:\\.[^\\\"]*)*)\")/gc) { # delimited re ala Regexp::Common::delimited + (?>...)
461 343         610 return _unescape($1);
462             } elsif ($$str =~ /\G\{(\d+)\}\r\n/gc) {
463 5         9 my $pos = pos($$str);
464 5         48 my $data = substr $$str, $pos, $1;
465 5         16 pos($$str) = $pos + $1;
466 5         15 return $data;
467             } elsif ($$str =~ /\G([^"\(\)\{ \%\*\"\\\x00-\x1F]+)/gc) {
468 71         179 return $1;
469             }
470              
471 166         287 return 0;
472             }
473              
474             sub _get_lang(\$) {
475 53     53   64 my $str = $_[0];
476              
477             # body-fld-lang = nstring / "(" string *(SP string) ")"
478              
479 53 100       137 if ($$str =~ m/\G\s*\(/gc) {
480 1         1 my @a;
481 1         2 while ('fareva') {
482 3         6 my $data = _get_nstring($$str);
483 3 100       7 $data or last;
484              
485 2         3 push @a, $data;
486             }
487              
488 1         3 $$str =~ m/\G\s*\)/gc;
489 1         3 return \@a;
490             }
491              
492 52 100       83 if (my $data = _get_nstring($$str)) {
493 1         3 return [$data];
494             }
495              
496 51         105 return [];
497             }
498              
499             sub _unescape {
500 343     343   576 my $str = shift;
501              
502 343         760 $str =~ s/^"//;
503 343         768 $str =~ s/"$//;
504 343         432 $str =~ s/\\\"/\"/g;
505 343         405 $str =~ s/\\\\/\\/g;
506              
507 343         885 return $str;
508             }
509              
510             =over 4
511              
512             =item get_enveleope($)
513              
514             Parses a string into IMAP::BodyStructure::Envelope object. See below.
515              
516             =back
517              
518             =head2 IMAP::BodyStructure::Envelope CLASS
519              
520             Every message on an IMAP server has an envelope. You can get it
521             using ENVELOPE fetch item or, and this is relevant, from BODYSTRUCTURE
522             response in case there are some nested messages (parts with type of
523             C). So, if we have a part with such a type then the
524             corresponding IMAP::BodyStructure object always has
525             B data member which is, in turn, an object of
526             IMAP::BodyStructure::Envelope.
527              
528             You can of course use this satellite class on its own, this is very
529             useful when generating meaningful message lists in IMAP folders.
530              
531             =cut
532              
533             package IMAP::BodyStructure::Envelope;
534              
535             sub _get_nstring(\$); # proto
536              
537             *_get_nstring = \&IMAP::BodyStructure::_get_nstring;
538              
539             sub _get_naddrlist(\$);
540             sub _get_naddress(\$);
541              
542 2     2   13 use vars qw/@envelope_addrs/;
  2         4  
  2         202  
543             @envelope_addrs = qw/from sender reply_to to cc bcc/;
544              
545             =head2 METHODS
546              
547             =over 4
548              
549             =item new($)
550              
551             The constructor create Envelope object from string which should be an
552             IMAP server respone to a fetch with ENVELOPE item or a substring of
553             BODYSTRUCTURE response for a message with message/rfc822 parts inside.
554              
555             =back
556              
557             =head2 DATA MEMBERS
558              
559             =over 4
560              
561             =item date
562              
563             Date of the message as specified in the envelope. Not the IMAP
564             INTERNALDATE, be careful!
565              
566             =item subject
567              
568             Subject of the message, may be RFC2047 encoded, of course.
569              
570             =item message_id
571              
572             =item in_reply_to
573              
574             Message-IDs of the current message and the message in reply to which
575             this one was composed.
576              
577             =item to, from, cc, bcc, sender, reply_to
578              
579             These are the so called address-lists or just arrays of addresses.
580             Remember, a message may be addressed to lots of people.
581              
582             Each address is a hash of four elements:
583              
584             =over 4
585              
586             =item name
587              
588             The informal part, "A.U.Thor" from "A.U.Thor, "
589              
590             =item sroute
591              
592             Source-routing information, not used. (By the way, IMAP4r1 spec was
593             born after the last email address with sroute passed away.)
594              
595             =item account
596              
597             The part before @.
598              
599             =item host
600              
601             The part after @.
602              
603             =item full
604              
605             The full address for display purposes.
606              
607             =back
608              
609             =back
610              
611             =cut
612              
613 2     2   12 use fields qw/from sender reply_to to cc bcc date subject in_reply_to message_id/;
  2         8  
  2         10  
614              
615             sub new(\$) {
616 7     7   11 my $class = shift;
617 7         10 my $str = shift;
618            
619 7 50       31 $$str =~ m/\G\s*(?:\(ENVELOPE)?\s*\(/gc
620             or return 0;
621              
622 7         16 my __PACKAGE__ $self = fields::new($class);
623              
624 7         466 $self->{'date'} = _get_nstring($$str);
625 7         16 $self->{'subject'} = _get_nstring($$str);
626              
627 7         18 foreach my $header (@envelope_addrs) {
628 42         68 $self->{$header} = _get_naddrlist($$str);
629             }
630              
631 7         16 $self->{'in_reply_to'} = _get_nstring($$str);
632 7         13 $self->{'message_id'} = _get_nstring($$str);
633              
634 7         16 $$str =~ m/\G\s*\)/gc;
635              
636 7         79 return $self;
637             }
638              
639             sub _get_naddress(\$) {
640 32     32   44 my $str = shift;
641              
642 32 50       107 if ($$str =~ /\GNIL/gc) {
    100          
643 0         0 return undef;
644             } elsif ($$str =~ m/\G\s*\(/gc) {
645 16         28 my %addr = (
646             name => _get_nstring($$str),
647             sroute => _get_nstring($$str),
648             account => _get_nstring($$str),
649             host => _get_nstring($$str),
650             );
651             $addr{address} = ($addr{account}
652 16 50 100     80 ? "$addr{account}@" . ($addr{host} || '')
653             : '');
654              
655 16         34 $addr{full} = _format_address($addr{name}, $addr{address});
656              
657 16         38 $$str =~ m/\G\s*\)/gc;
658 16         46 return \%addr;
659             }
660 16         37 return 0;
661             }
662              
663             sub _get_naddrlist(\$) {
664 42     42   45 my $str = shift;
665            
666 42         63 $$str =~ /\G\s+/gc;
667              
668 42 100       106 if ($$str =~ /\GNIL/gc) {
    50          
669 26         53 return undef;
670             } elsif ($$str =~ m/\G\s*\(/gc) {
671 16         25 my @addrs = ();
672 16         28 while (my $addr = _get_naddress($$str)) {
673 16         38 push @addrs, $addr;
674             }
675              
676 16         30 $$str =~ m/\G\s*\)/gc;
677 16         44 return \@addrs;
678             }
679 0         0 return 0;
680             }
681              
682             my $rfc2822_atext = q(a-zA-Z0-9!#$%&'*+/=?^_`{|}~-); # simple non-interpolating string (think apostrophs)
683             my $rfc2822_atom = qr/[$rfc2822_atext]+/; # straight from rfc2822
684              
685 2     2   1697 use constant EMPTY_STR => q{};
  2         4  
  2         538  
686             sub _format_address {
687 16     16   23 my ($phrase, $email) = @_;
688              
689 16 100 66     63 if (defined $phrase && $phrase ne EMPTY_STR) {
690 13 50       30 if ($phrase !~ /^ \s* " [^"]+ " \s* \z/xms) {
691             # $phrase is not already quoted
692              
693 13         19 $phrase =~ s/ (["\\]) /\\$1/xmsg;
694              
695 13 100       161 if ($phrase !~ m/^ \s* $rfc2822_atom (?: \s+ $rfc2822_atom)* \s* \z/xms) {
696 3         8 $phrase = qq{"$phrase"};
697             }
698             }
699              
700 13 50       48 return $email ? "$phrase <$email>" : $phrase;
701             } else {
702 3   50     9 return $email || '';
703             }
704             }
705              
706             1;
707              
708             __END__