File Coverage

blib/lib/Mail/Server/IMAP4/Fetch.pm
Criterion Covered Total %
statement 106 136 77.9
branch 50 72 69.4
condition 17 41 41.4
subroutine 10 15 66.6
pod 9 9 100.0
total 192 273 70.3


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Mail-Box-IMAP4 version 4.01.
2             # The POD got stripped from this file by OODoc version 3.05.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2001-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package Mail::Server::IMAP4::Fetch;{
13             our $VERSION = '4.01';
14             }
15              
16              
17 2     2   763078 use strict;
  2         5  
  2         72  
18 2     2   11 use warnings;
  2         4  
  2         129  
19              
20 2     2   655 use Log::Report 'mail-box-imap4', import => [];
  2         118062  
  2         11  
21              
22 2     2   366 use Date::Parse ();
  2         5  
  2         67  
23 2     2   15 use Digest::MD5 qw/md5_base64/;
  2         5  
  2         4284  
24              
25             #--------------------
26              
27             sub new($)
28 7     7 1 333683 { my ($class, $part, %args) = @_;
29              
30 7         26 my $head = $part->head;
31 7         56 my $body = $part->body;
32 7         48 my $type = $body->type->study;
33              
34 7         1672 my $self = bless +{
35             type => $type->body,
36             typeattr => [ $type->attrPairs ],
37             charset => $body->charset,
38             bodylines => $body->nrLines,
39             bodysize => $body->size
40             }, $class;
41              
42 7         1571 $self->{headbegin} = ($head->fileLocation)[0];
43 7         68 @{$self}{qw/bodybegin bodyend/} = $body->fileLocation;
  7         82  
44              
45             # The fields use the defined() check, to avoid accidental expensive
46             # stringification by the field objects.
47              
48 7         13 my ($field, $value);
49 7 100       21 $self->{date} = $field->unfoldedBody
50             if defined($field = $head->get('Date'));
51              
52 7 100       200 $self->{subject} = $field->unfoldedBody
53             if defined($field = $head->get('Subject'));
54              
55 7 100       155 $self->{description} = $field->unfoldedBody
56             if defined($field = $head->get('Content-Description'));
57              
58 7 100       81 $self->{language} = $field->unfoldedBody
59             if defined($field = $head->get('Content-Language'));
60              
61 7 100       94 $self->{filename} = $value
62             if defined($value = $body->dispositionFilename);
63              
64             $self->{bodyMD5} = md5_base64($body->string)
65 7 50       3180 if $args{md5checksums};
66              
67 7 50       23 if(defined($field = $body->transferEncoding))
68 7         56 { my $tf = $field->unfoldedBody;
69 7 100       248 $self->{transferenc} = $tf unless $tf eq 'none';
70             }
71              
72             # Should become:
73             # $self->{disposition} = [ $field->body, $field->study->attributes ]
74 7 50       21 if(defined($field = $body->disposition))
75 7         71 { my $how = $field->body;
76 7 100       304 $how = $body->isText ? 'inline' : 'attachment' if $how eq 'none';
    100          
77 7         499 $self->{disposition} = [ $how, $field->attributes ];
78             }
79             else
80 0 0       0 { $self->{disposition} = [ ($body->isText ? 'inline' : 'attachment') ];
81             }
82              
83 7   66     218 my $id = $head->get('Content-Message-ID') || $head->get("Message-ID");
84 7 100       158 if(defined $id)
85 5         22 { my $msgid = $id->unfoldedBody =~ s/^\<*/*$/>/r;
86 5 50       132 $self->{messageid} = $msgid if length $msgid;
87             }
88              
89 7         13 foreach my $addr ( qw/to from sender reply-to cc bcc/ )
90 42 100       575 { my $addrs = $head->study($addr) or next;
91 12         6500 foreach my $group ($addrs->groups)
92 11         60 { my @addrs = map +[ $_->phrase, $_->username, $_->domain ], $group->addresses;
93 11         494 push @{$self->{$addr}}, [ $group->name, @addrs ];
  11         37  
94             }
95             }
96              
97 7 100       144 if($body->isMultipart)
    100          
98 1         5 { $self->{parts} = +[ map $class->new($_), $body->parts ];
99             }
100             elsif($body->isNested)
101 1         16 { $self->{nest} = $class->new($body->nested);
102             }
103              
104 7         43 $self;
105             }
106              
107             #--------------------
108              
109 0     0 1 0 sub headLocation() { @{$_[0]}{ qw/headbegin bodybegin/ } }
  0         0  
110 0     0 1 0 sub bodyLocation() { @{$_[0]}{ qw/bodybegin bodyend/ } }
  0         0  
111 0     0 1 0 sub partLocation() { @{$_[0]}{ qw/headbegin bodyend/ } }
  0         0  
112              
113             #--------------------
114              
115             sub fetchBody($)
116 18     18 1 1627 { my ($self, $extended) = @_;
117              
118 18         38 my $type = uc $self->{type};
119 18         51 my ($mediatype, $subtype) = split m[/], $type;
120              
121 18 100       44 if($self->{parts})
122             { # Multipart message
123             # WARNING: no blanks between part descriptions
124 2         3 my $parts = join '', map $_->fetchBody($extended), @{$self->{parts}};
  2         10  
125 2   50     5 my @fields = (\$parts, $subtype || 'MIXED');
126              
127 2 100       6 if($extended) # only included when any valid info
128 1         2 { my @attr; # don't know what to include here
129             my @disp; # don't know about this either
130              
131             push @fields, \@attr, \@disp, $self->{language}
132 1 50 33     7 if @attr || @disp || defined $self->{language};
      33        
133             }
134              
135 2         3 return $self->_imapList(@fields);
136             }
137              
138             #
139             # Simple message
140             #
141              
142             my @fields = (
143             ($mediatype || 'TEXT'),
144             ($subtype || 'PLAIN'),
145             $self->{typeattr},
146             $self->{messageid},
147             $self->{description},
148             uc($self->{transferenc} || '8BIT'),
149 16   50     85 \($self->{bodysize}),
      50        
      100        
150             );
151              
152 16 100       34 if(my $nest = $self->{nest})
153             { # type MESSAGE (message/rfc822 encapsulated)
154 2         6 push @fields,
155             \$nest->fetchEnvelope,,
156             \$nest->fetchBody($extended);
157             }
158 16         26 push @fields, \$self->{bodylines};
159              
160 8         20 push @fields, @{$self}{ qw/bodyMD5 disposition language/ }
161 16 50 33     89 if $extended && ($self->{bodyMD5} || $self->{disposition} || $self->{language});
      66        
162              
163 16         36 $self->_imapList(@fields);
164             }
165              
166              
167             sub fetchEnvelope()
168 8     8 1 17 { my $self = shift;
169 8         21 my @fields = ($self->{date}, $self->{subject});
170              
171 8         23 foreach my $addr ( qw/from sender reply-to to cc bcc/ )
172 48 100       84 { unless($self->{$addr})
173 33         38 { push @fields, undef; # NIL
174 33         36 next;
175             }
176              
177             # For now, group information is ignored... RFC2060 is very
178             # unclear about it... and seems incompatible with RFC2822
179 15         20 my $addresses = '';
180 15         18 foreach my $group (@{$self->{$addr}})
  15         24  
181 15         23 { my ($name, @addr) = @$group;
182              
183             # addr_adl is obsoleted by rfc2822
184             $addresses .= $self->_imapList($_->[0], undef, $_->[1], $_->[2])
185 15         36 for @addr;
186             }
187              
188 15         26 push @fields, \$addresses;
189             }
190              
191 8         17 push @fields, $self->{'in-reply-to'}, $self->{messageid};
192              
193 8         15 $self->_imapList(@fields);
194             }
195              
196              
197 0     0 1 0 sub fetchSize() { $_[0]->{bodysize} }
198              
199              
200             sub part(;$)
201 9     9 1 1097 { my $self = shift;
202 9 100       48 my $nr = shift or return $self;
203              
204 7         15 my @nrs = split /\./, $nr;
205 7         15 while(@nrs)
206 8         12 { my $take = shift @nrs;
207 8 100 66     33 if(exists $self->{nest} && $take==1)
    100 100        
208 1         3 { $self = $self->{nest} }
209 5         16 elsif(exists $self->{parts} && @{$self->{parts}} >= $take)
210 4         9 { $self = $self->{parts}[$take-1] }
211 3         15 else { return undef }
212             }
213              
214 4         13 $self;
215             }
216              
217              
218             sub printStructure(;$$)
219 0     0 1 0 { my $self = shift;
220 0 0       0 my $fh = @_ ? shift : select;
221 0 0       0 my $number = @_ ? shift : '';
222              
223 0         0 my $buffer; # only filled if filehandle==undef
224 0 0       0 defined $fh or open $fh, '>:raw', \$buffer;
225              
226 0         0 my $type = $self->{type};
227 0   0     0 my $subject = $self->{subject} || '';
228 0         0 my $text = "$number $type: $subject\n";
229              
230 0   0     0 my $hbegin = $self->{headbegin} || 0;
231 0   0     0 my $bbegin = $self->{bodybegin} || '?';
232 0   0     0 my $bodyend = $self->{bodyend} || '?';
233 0   0     0 my $size = $self->{bodysize} // '?';
234 0   0     0 my $lines = $self->{bodylines} // '?';
235              
236 0         0 $text .= ' ' x (length($number) + 1);
237 0         0 $text .= "@ $hbegin-$bbegin-$bodyend, $size bytes, $lines lines\n";
238              
239 0         0 $fh->print($text);
240              
241 0 0       0 if(my $nest = $self->{nest})
    0          
242 0 0       0 { $nest->printStructure($fh, length($number) ? $number.'.1' :'1');
243             }
244             elsif(my $parts = $self->{parts})
245 0         0 { my $count = 1;
246 0 0       0 $number .= '.' if length $number;
247 0         0 $_->printStructure($fh, $number.$count++) for @$parts;
248             }
249              
250 0         0 $buffer;
251             }
252              
253             #--------------------
254              
255             # Concatenate the elements of a list, as the IMAP protocol does.
256             # ARRAYS are included a sublist, and normal strings get quoted.
257             # Pass a ref-scalar if something needs to be included without
258             # quoting.
259              
260             sub _imapList(@)
261 65     65   78 { my $self = shift;
262 65         69 my @f;
263              
264 65         91 foreach (@_)
265 328 100       589 { if(ref $_ eq 'ARRAY') { push @f, $self->_imapList(@$_) }
  24 100       47  
    100          
266 53         46 elsif(ref $_ eq 'SCALAR') { push @f, ${$_} }
  53         78  
267 100         125 elsif(!defined $_) { push @f, 'NIL' }
268             else
269 151         228 { my $copy = $_ =~ s/\\/\\\\/gr =~ s/\"/\\"/gr;
270 151         295 push @f, qq#"$_"#;
271             }
272             }
273              
274 65         80 local $" = ' ';
275 65         339 "(@f)";
276             }
277              
278             #--------------------
279              
280             1;