File Coverage

blib/lib/Mail/Message/Body/Multipart.pm
Criterion Covered Total %
statement 195 215 90.7
branch 88 118 74.5
condition 23 38 60.5
subroutine 28 35 80.0
pod 22 24 91.6
total 356 430 82.7


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Mail-Message version 4.04.
2             # The POD got stripped from this file by OODoc version 3.06.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2001-2026 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::Message::Body::Multipart;{
13             our $VERSION = '4.04';
14             }
15              
16 37     37   968 use parent 'Mail::Message::Body';
  37         176  
  37         317  
17              
18 37     37   3511 use strict;
  37         83  
  37         1749  
19 37     37   203 use warnings;
  37         87  
  37         2968  
20              
21 37     37   265 use Log::Report 'mail-message', import => [ qw/__x error panic warning/ ];
  37         76  
  37         410  
22              
23 37     37   8857 use Scalar::Util qw/blessed/;
  37         139  
  37         2518  
24              
25 37     37   355 use Mail::Message::Body::Lines ();
  37         86  
  37         1071  
26 37     37   201 use Mail::Message::Part ();
  37         75  
  37         1322  
27 37     37   24792 use Mail::Box::FastScalar ();
  37         118  
  37         136504  
28              
29             #--------------------
30              
31             sub init($)
32 49     49 0 137 { my ($self, $args) = @_;
33 49         172 my $based = $args->{based_on};
34 49 100 66     453 $args->{mime_type} ||= defined $based ? $based->type : 'multipart/mixed';
35              
36 49         288 $self->SUPER::init($args);
37              
38 49         97 my @parts;
39 49 100       184 if($args->{parts})
40 31         59 { foreach my $raw (@{$args->{parts}})
  31         105  
41 54 50       152 { defined $raw or next;
42 54         217 my $cooked = Mail::Message::Part->coerce($raw, $self);
43              
44 54 50       149 defined $cooked
45             or error __x"data not convertible to a message (type is {class})", class => ref $raw;
46              
47 54         171 push @parts, $cooked;
48             }
49             }
50              
51 49         138 my $preamble = $args->{preamble};
52 49 50 66     209 $preamble = Mail::Message::Body->new(data => $preamble)
53             if defined $preamble && ! blessed $preamble;
54              
55 49         128 my $epilogue = $args->{epilogue};
56 49 50 66     188 $epilogue = Mail::Message::Body->new(data => $epilogue)
57             if defined $epilogue && ! blessed $epilogue;
58              
59 49 100       201 if($based)
60 23   33     138 { $self->boundary($args->{boundary} || $based->boundary);
61 23   66     145 $self->{MMBM_preamble} = $preamble // $based->preamble;
62              
63             $self->{MMBM_parts}
64             = @parts ? \@parts
65 23 100 66     134 : !$args->{parts} && $based->isMultipart ? [ $based->parts('ACTIVE') ]
    100          
66             : [];
67              
68 23   66     117 $self->{MMBM_epilogue} = $epilogue // $based->epilogue;
69             }
70             else
71 26   66     221 { $self->boundary($args->{boundary} ||$self->type->attribute('boundary'));
72 26         170 $self->{MMBM_preamble} = $preamble;
73 26         103 $self->{MMBM_parts} = \@parts;
74 26         89 $self->{MMBM_epilogue} = $epilogue;
75             }
76              
77 49         347 $self;
78             }
79              
80             sub isMultipart() { 1 }
81             sub isBinary() { 0 } # A multipart body is never binary itself. The parts may be.
82              
83             sub clone()
84 3     3 1 8 { my $self = shift;
85 3         10 my $preamble = $self->preamble;
86 3         9 my $epilogue = $self->epilogue;
87              
88 3 100       30 my $body = (ref $self)->new(
    100          
89             based_on => $self,
90             preamble => ($preamble ? $preamble->clone : undef),
91             epilogue => ($epilogue ? $epilogue->clone : undef),
92             parts => [ map $_->clone, $self->parts('ACTIVE') ],
93             );
94              
95             }
96              
97             sub nrLines()
98 22     22 1 65 { my $self = shift;
99 22         45 my $nr = 1; # trailing part-sep
100              
101 22 100       67 if(my $preamble = $self->preamble)
102 2         11 { $nr += $preamble->nrLines;
103 2 50       9 $nr++ if $preamble->endsOnNewline;
104             }
105              
106 22         81 foreach my $part ($self->parts('ACTIVE'))
107 33         123 { $nr += 1 + $part->nrLines;
108 33 100       138 $nr++ if $part->body->endsOnNewline;
109             }
110              
111 22 100       69 if(my $epilogue = $self->epilogue)
112             { # nrLines should match mbox counts, which is a bit
113             # unclear w.r.t. the \n after a multipart separator
114             # line.
115 2         7 $nr += $epilogue->nrLines -1;
116             }
117              
118 22         69 $nr;
119             }
120              
121             sub size()
122 26     26 1 59 { my $self = shift;
123 26         93 my $bbytes = length($self->boundary) +4; # \n--$b\n
124              
125 26         67 my $bytes = $bbytes +2; # last boundary, \n--$b--\n
126 26 100       84 if(my $preamble = $self->preamble)
127 5         15 { $bytes += $preamble->size }
128 21         53 else { $bytes -= 1 } # no leading \n
129              
130 26         126 $bytes += $bbytes + $_->size foreach $self->parts('ACTIVE');
131 26 100       88 if(my $epilogue = $self->epilogue)
132 6         18 { $bytes += $epilogue->size;
133             }
134 26         91 $bytes;
135             }
136              
137 4     4 1 24 sub string() { join '', $_[0]->lines }
138              
139             sub lines()
140 4     4 1 11 { my $self = shift;
141 4         21 my $boundary = $self->boundary;
142 4         20 my $preamble = $self->preamble;
143              
144 4         11 my @lines;
145 4 50       42 push @lines, $preamble->lines if $preamble;
146              
147 4         16 foreach my $part ($self->parts('ACTIVE'))
148             { # boundaries start with \n
149 8 100       119 if(!@lines) { ; }
    50          
150 4         11 elsif($lines[-1] =~ m/\n$/) { push @lines, "\n" }
151 0         0 else { $lines[-1] .= "\n" }
152 8         40 push @lines, "--$boundary\n", $part->lines;
153             }
154              
155 4 50       99 if(!@lines) { ; }
    50          
156 4         11 elsif($lines[-1] =~ m/\n$/) { push @lines, "\n" }
157 0         0 else { $lines[-1] .= "\n" }
158 4         16 push @lines, "--$boundary--";
159              
160 4 50       20 if(my $epilogue = $self->epilogue)
161 0         0 { $lines[-1] .= "\n";
162 0         0 push @lines, $epilogue->lines;
163             }
164              
165 4 50       117 wantarray ? @lines : \@lines;
166             }
167              
168             sub file() # It may be possible to speed-improve the next code, which first
169 0     0 1 0 { my $self = shift; # produces a full print of the message in memory...
170 0         0 my $dump = Mail::Box::FastScalar->new;
171 0         0 $self->print($dump);
172 0         0 $dump->seek(0,0);
173 0         0 $dump;
174             }
175              
176             sub print(;$)
177 9     9 1 20 { my $self = shift;
178 9   33     38 my $out = shift || select;
179              
180 9         94 my $boundary = $self->boundary;
181 9         24 my $count = 0;
182 9 100       32 if(my $preamble = $self->preamble)
183 3         18 { $preamble->print($out);
184 3         6 $count++;
185             }
186              
187 9         28 foreach my $part ($self->parts('ACTIVE'))
188 15 100       55 { $out->print("\n") if $count++;
189 15         155 $out->print("--$boundary\n");
190 15         201 $part->print($out);
191             }
192 9 100       46 $out->print("\n") if $count++;
193 9         94 $out->print("--$boundary--");
194              
195 9 100       109 if(my $epilogue = $self->epilogue)
196 3         17 { $out->print("\n");
197 3         47 $epilogue->print($out);
198             }
199              
200 9         26 $self;
201             }
202              
203             sub endsOnNewline()
204 5     5 1 9 { my $self = shift;
205 5 100       16 my $epilogue = $self->epilogue or return 1;
206 2         10 $epilogue =~ m/[\r\n]$/;
207             }
208              
209              
210             sub foreachLine($)
211 0     0 1 0 { my ($self, $code) = @_;
212 0         0 error __x"you cannot use foreachLine on a multipart.";
213             }
214              
215             sub check()
216 0     0 1 0 { my $self = shift;
217 0     0   0 $self->foreachComponent( sub { $_[1]->check } );
  0         0  
218             }
219              
220             sub encode(@)
221 0     0 1 0 { my ($self, %args) = @_;
222 0     0   0 $self->foreachComponent( sub { $_[1]->encode(%args) } );
  0         0  
223             }
224              
225             sub encoded()
226 20     20 1 66 { my $self = shift;
227 20     36   156 $self->foreachComponent( sub { $_[1]->encoded } );
  36         195  
228             }
229              
230             sub read($$$$)
231 16     16 1 57 { my ($self, $parser, $head, $bodytype) = @_;
232 16         108 my $boundary = $self->boundary;
233              
234 16         148 $parser->pushSeparator("--$boundary");
235              
236 16         90 my $te;
237 16 100 100     65 $te = lc $1 if +($head->get('Content-Transfer-Encoding') || '') =~ m/(\w+)/;
238              
239 16         68 my @sloppyopts = (mime_type => 'text/plain', transfer_encoding => $te);
240              
241             # Get preamble.
242 16         40 my $headtype = ref $head;
243 16         134 my $begin = $parser->filePosition;
244 16         178 my $preamble = Mail::Message::Body::Lines->new(@sloppyopts)->read($parser, $head);
245              
246 16 100       66 $preamble->nrLines or undef $preamble;
247 16 100       78 $self->{MMBM_preamble} = $preamble if defined $preamble;
248              
249             # Get the parts.
250              
251 16         33 my ($has_epilogue, @parts);
252 16         61 while(my $sep = $parser->readSeparator)
253 44 100       836 { if($sep =~ m/^--\Q$boundary\E--[ \t]*\n?/)
254             { # Per RFC 2046, a CRLF after the close-delimiter marks the presence
255             # of an epilogue. Preserve the epilogue, even if empty, so that the
256             # printed multipart body will also have the CRLF.
257             # This, however, is complicated w.r.t. mbox folders.
258 16         68 $has_epilogue = $sep =~ /\n/;
259 16         39 last;
260             }
261              
262 28         202 my $part = Mail::Message::Part->new(container => $self);
263 28 50       141 $part->readFromParser($parser, $bodytype)
264             or last;
265              
266 28 50 33     95 push @parts, $part if $part->head->names || $part->body->size;
267             }
268 16         83 $self->{MMBM_parts} = \@parts;
269              
270             # Get epilogue
271              
272 16         159 $parser->popSeparator;
273 16         109 my $epilogue = Mail::Message::Body::Lines->new(@sloppyopts)
274             ->read($parser, $head);
275              
276 16 0       105 my $end
    0          
    50          
277             = defined $epilogue ? ($epilogue->fileLocation)[1]
278             : @parts ? ($parts[-1]->body->fileLocation)[1]
279             : defined $preamble ? ($preamble->fileLocation)[1]
280             : $begin;
281              
282 16         129 $self->fileLocation($begin, $end);
283              
284 16 100 66     64 $has_epilogue || $epilogue->nrLines
285             or undef $epilogue;
286              
287 16 100       75 $self->{MMBM_epilogue} = $epilogue
288             if defined $epilogue;
289              
290 16         221 $self;
291             }
292              
293             #--------------------
294              
295             sub foreachComponent($)
296 20     20 1 56 { my ($self, $code) = @_;
297 20         38 my $changes = 0;
298              
299 20         43 my $new_preamble;
300 20 100       96 if(my $preamble = $self->preamble)
301 2         8 { $new_preamble = $code->($self, $preamble);
302 2 50       37 $changes++ unless $preamble == $new_preamble;
303             }
304              
305 20         53 my $new_epilogue;
306 20 100       72 if(my $epilogue = $self->epilogue)
307 2         7 { $new_epilogue = $code->($self, $epilogue);
308 2 100       9 $changes++ unless $epilogue == $new_epilogue;
309             }
310              
311 20         43 my @new_bodies;
312 20         73 foreach my $part ($self->parts('ACTIVE'))
313 32         136 { my $part_body = $part->body;
314 32         96 my $new_body = $code->($self, $part_body);
315              
316 32 100       123 $changes++ if $new_body != $part_body;
317 32         127 push @new_bodies, [$part, $new_body];
318             }
319              
320 20 100       151 $changes or return $self;
321              
322 9         21 my @new_parts;
323 9         23 foreach (@new_bodies)
324 19         51 { my ($part, $body) = @$_;
325 19         81 my $new_part = Mail::Message::Part->new(head => $part->head->clone, container => undef);
326 19         96 $new_part->body($body);
327 19         59 push @new_parts, $new_part;
328             }
329              
330 9         98 my $constructed = (ref $self)->new(
331             preamble => $new_preamble,
332             parts => \@new_parts,
333             epilogue => $new_epilogue,
334             based_on => $self,
335             );
336              
337             $_->container($constructed)
338 9         50 for @new_parts;
339              
340 9         62 $constructed;
341             }
342              
343              
344             sub attach(@)
345 2     2 1 5 { my $self = shift;
346 2         10 (ref $self)->new(based_on => $self, parts => [ $self->parts, @_ ]);
347             }
348              
349              
350             sub stripSignature(@)
351 1     1 1 3 { my $self = shift;
352              
353 1         6 my @allparts = $self->parts;
354 1         5 my @parts = grep ! $_->body->mimeType->isSignature, @allparts;
355              
356 1 50       38 @allparts == @parts ? $self : (ref $self)->new(based_on => $self, parts => \@parts);
357             }
358              
359             #--------------------
360              
361 108     108 1 1362 sub preamble() { $_[0]->{MMBM_preamble} }
362              
363              
364 115     115 1 503 sub epilogue() { $_[0]->{MMBM_epilogue} }
365              
366              
367             sub parts(;$)
368 161     161 1 477 { my $self = shift;
369 161 100       474 return @{$self->{MMBM_parts}} unless @_;
  68         514  
370              
371 93         184 my $what = shift;
372 93         163 my @parts = @{$self->{MMBM_parts}};
  93         322  
373              
374 93 0       744 $what eq 'RECURSE' ? (map $_->parts('RECURSE'), @parts)
    50          
    50          
    50          
    50          
375             : $what eq 'ALL' ? @parts
376             : $what eq 'DELETED' ? (grep $_->isDeleted, @parts)
377             : $what eq 'ACTIVE' ? (grep !$_->isDeleted, @parts)
378             : ref $what eq 'CODE'? (grep $what->($_), @parts)
379             : error __x"unknown criterium {what} to select parts.", what => $what;
380             }
381              
382              
383 13     13 1 171 sub part($) { $_[0]->{MMBM_parts}[$_[1]] }
384              
385             sub partNumberOf($)
386 8     8 1 17 { my ($self, $part) = @_;
387 8 50       22 my $msg = $self->message or panic "multipart is not connected.";
388              
389 8 100       52 my $base = $msg->isa('Mail::Message::Part') ? $msg->partNumber.'.' : '';
390              
391 8         22 my @parts = $self->parts('ACTIVE');
392 8         31 foreach my $partnr (0..@parts)
393 15 100       108 { return $base.($partnr+1)
394             if $parts[$partnr] == $part;
395             }
396 0         0 panic "multipart is not found or not active";
397             }
398              
399              
400             sub boundary(;$)
401 130     130 1 533 { my $self = shift;
402 130         419 my $mime = $self->type;
403              
404 130 100       388 unless(@_)
405 80         416 { my $boundary = $mime->attribute('boundary');
406 80 50       1119 return $boundary if defined $boundary;
407             }
408              
409 50   66     1995 my $boundary = $_[0] // "boundary-".int rand(1000000);
410 50         397 $self->type->attribute(boundary => $boundary);
411             }
412              
413 0 0   0 0   sub toplevel() { my $msg = $_[0]->message; $msg ? $msg->toplevel : undef}
  0            
414              
415             1;