File Coverage

blib/lib/Mail/Message/Head/Complete.pm
Criterion Covered Total %
statement 180 240 75.0
branch 61 104 58.6
condition 28 60 46.6
subroutine 36 49 73.4
pod 36 38 94.7
total 341 491 69.4


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::Head::Complete;{
13             our $VERSION = '4.04';
14             }
15              
16 40     40   3675 use parent 'Mail::Message::Head';
  40         79  
  40         277  
17              
18 40     40   2913 use strict;
  40         80  
  40         1064  
19 40     40   215 use warnings;
  40         98  
  40         2710  
20              
21 40     40   251 use Log::Report 'mail-message', import => [ qw/__x info trace warning/ ];
  40         86  
  40         255  
22              
23 40     40   28933 use Mail::Box::Parser ();
  40         125  
  40         1057  
24 40     40   23118 use Mail::Message::Head::Partial ();
  40         139  
  40         3121  
25              
26 40     40   303 use Scalar::Util qw/weaken blessed/;
  40         90  
  40         2719  
27 40     40   270 use List::Util qw/sum/;
  40         90  
  40         3020  
28 40     40   22950 use Sys::Hostname qw/hostname/;
  40         64797  
  40         161325  
29              
30             #--------------------
31              
32             sub clone(;@)
33 53     53 1 715 { my $self = shift;
34 53         296 my $copy = (ref $self)->new;
35              
36 53         271 $copy->addNoRealize($_->clone) for $self->grepNames(@_);
37 53         215 $copy->modified(1);
38 53         243 $copy;
39             }
40              
41              
42             sub build(@)
43 4     4 1 265211 { my $class = shift;
44 4         43 my $self = $class->new;
45 4         16 while(@_)
46 25         67 { my $name = shift;
47 25 50       56 defined $name or next;
48              
49 25 50       190 if($name->isa('Mail::Message::Field'))
50 0         0 { $self->add($name);
51 0         0 next;
52             }
53              
54 25         45 my $content = shift;
55 25 50       60 defined $content or next;
56              
57 25 50 33     64 if(ref $content && $content->isa('Mail::Message::Field'))
58 0         0 { warning __x"field objects have an implied name ({name})", name => $name;
59 0         0 $self->add($content);
60 0         0 next;
61             }
62              
63 25         63 $self->add($name, $content);
64             }
65              
66 4         16 $self;
67             }
68              
69             #--------------------
70              
71             sub isDelayed() {0}
72              
73              
74 47     47 1 2392 sub nrLines() { sum 1, map $_->nrLines, shift->orderedFields }
75 60     60 1 189 sub size() { sum 1, map $_->size, shift->orderedFields }
76              
77              
78             sub wrap($)
79 0     0 1 0 { my ($self, $length) = @_;
80 0         0 $_->setWrapLength($length) for $self->orderedFields;
81             }
82              
83             #--------------------
84              
85             sub add(@)
86 181     181 1 3413 { my $self = shift;
87              
88             # Create object for this field.
89              
90             my $field
91             = @_==1 && blessed $_[0] ? shift # A fully qualified field is added.
92 181 50 33     1455 : ($self->{MMH_field_type} // 'Mail::Message::Field::Fast')->new(@_);
      50        
93              
94 181 50       426 defined $field or return;
95              
96             # Put it in place.
97              
98 181         617 $field->setWrapLength;
99 181         365 my $known = $self->{MMH_fields};
100 181         476 my $name = $field->name; # is already lower-cased
101              
102 181         706 $self->addOrderedFields($field);
103              
104 181 100       464 if(! defined $known->{$name}) { $known->{$name} = $field }
  178 100       515  
105 1         3 elsif(ref $known->{$name} eq 'ARRAY') { push @{$known->{$name}}, $field }
  1         3  
106 2         7 else { $known->{$name} = [ $known->{$name}, $field ] }
107              
108 181         336 $self->{MMH_modified}++;
109 181         479 $field;
110             }
111              
112              
113             sub count($)
114 0     0 1 0 { my $known = shift->{MMH_fields};
115 0         0 my $value = $known->{lc shift};
116 0 0       0 ! defined $value ? 0 : ref $value ? @$value : 1;
    0          
117             }
118              
119              
120 34     34 1 1436 sub names() { $_[0]->knownNames }
121              
122              
123             sub grepNames(@)
124 172     172 1 295 { my $self = shift;
125              
126 172         300 my @take;
127 172 50       737 push @take, (ref $_ eq 'ARRAY' ? @$_ : $_) for @_;
128 172 100       550 @take or return $self->orderedFields;
129              
130 119         191 my $take;
131 119 50 33     646 if(@take==1 && ref $take[0] eq 'Regexp')
132 119         216 { $take = $take[0]; # one regexp prepared already
133             }
134             else
135             { # I love this trick:
136 0         0 local $" = ')|(?:';
137 0         0 $take = qr/^(?:(?:@take))/i;
138             }
139              
140 119         402 grep $_->name =~ $take, $self->orderedFields;
141             }
142              
143              
144             my @skip_none = qw/content-transfer-encoding content-disposition content-description content-id/;
145             my %skip_none = map +($_ => 1), @skip_none;
146              
147             sub set(@)
148 570     570 1 2957 { my $self = shift;
149 570 100 100     1991 @_!=1 || defined $_[0] or return;
150              
151 483   50     1614 my $type = $self->{MMH_field_type} // 'Mail::Message::Field::Fast';
152 483         837 $self->{MMH_modified}++;
153              
154             # Create object for this field.
155 483 100 66     2323 my $field = @_==1 && blessed $_[0] ? shift->clone : $type->new(@_);
156              
157 483         1301 my $name = $field->name; # is already lower-cased
158 483         890 my $known = $self->{MMH_fields};
159              
160             # Internally, non-existing content-info are in the body stored as 'none'
161             # The header will not contain these lines.
162              
163 483 100 100     1874 if($skip_none{$name} && $field->body eq 'none')
164 305         603 { delete $known->{$name};
165 305         1233 return $field;
166             }
167              
168 178         650 $field->setWrapLength;
169 178         569 $known->{$name} = $field;
170              
171 178         644 $self->addOrderedFields($field);
172 178         429 $field;
173             }
174              
175              
176             sub reset($@)
177 14     14 1 2422 { my ($self, $name) = (shift, lc shift);
178              
179 14         29 my $known = $self->{MMH_fields};
180              
181 14 100       51 if(@_==0)
182 13 100       40 { $self->{MMH_modified}++ if delete $known->{$name};
183 13         32 return ();
184             }
185              
186 1         2 $self->{MMH_modified}++;
187              
188             # Cloning required, otherwise double registrations will not be
189             # removed from the ordered list: that's controled by 'weaken'
190              
191 1         4 my @fields = map $_->clone, @_;
192 1 50       3 $known->{$name} = @_==1 ? $fields[0] : \@fields;
193              
194 1         3 $self->addOrderedFields(@fields);
195 1         24 $self;
196             }
197              
198              
199 12     12 1 43 sub delete($) { $_[0]->reset($_[1]) }
200              
201              
202             sub removeField($)
203 89     89 1 195 { my ($self, $field) = @_;
204 89         238 my $name = $field->name;
205 89         218 my $known = $self->{MMH_fields};
206              
207 89 50       415 if(!defined $known->{$name})
    100          
    50          
208             { ; } # complain
209             elsif(ref $known->{$name} eq 'ARRAY')
210 2         4 { for(my $i=0; $i < @{$known->{$name}}; $i++)
  2         7  
211             {
212 2         9 return splice @{$known->{$name}}, $i, 1
213 2 50       5 if $known->{$name}[$i] eq $field;
214             }
215             }
216             elsif($known->{$name} eq $field)
217 87         391 { return delete $known->{$name};
218             }
219              
220 0         0 warning __x"cannot remove field {name} from header: not found.";
221 0         0 undef;;
222             }
223              
224              
225             sub removeFields(@)
226 49     49 1 86 { my $self = shift;
227 49         368 (bless $self, 'Mail::Message::Head::Partial')->removeFields(@_);
228             }
229              
230              
231             sub removeFieldsExcept(@)
232 0     0 1 0 { my $self = shift;
233 0         0 (bless $self, 'Mail::Message::Head::Partial')->removeFieldsExcept(@_);
234             }
235              
236              
237 87     87 1 652 sub removeContentInfo() { $_[0]->removeFields(qr/^Content-/, 'Lines') }
238              
239              
240             sub removeResentGroups(@)
241 0     0 1 0 { my $self = shift;
242 0         0 (bless $self, 'Mail::Message::Head::Partial')->removeResentGroups(@_);
243             }
244              
245              
246             sub removeListGroup(@)
247 0     0 1 0 { my $self = shift;
248 0         0 (bless $self, 'Mail::Message::Head::Partial')->removeListGroup(@_);
249             }
250              
251              
252             sub removeSpamGroups(@)
253 0     0 1 0 { my $self = shift;
254 0         0 (bless $self, 'Mail::Message::Head::Partial')->removeSpamGroups(@_);
255             }
256              
257              
258             sub spamDetected()
259 0     0 1 0 { my $self = shift;
260 0 0       0 my @sgs = $self->spamGroups or return undef;
261 0         0 grep $_->spamDetected, @sgs;
262             }
263              
264              
265             sub print(;$)
266 39     39 1 1737 { my $self = shift;
267 39   33     90 my $fh = shift || select;
268 39         232 $_->print($fh) for $self->orderedFields;
269 39         115 $fh->print("\n");
270 39         377 $self;
271             }
272              
273              
274             sub printUndisclosed($)
275 0     0 1 0 { my ($self, $fh) = @_;
276 0         0 $_->print($fh) for grep $_->toDisclose, $self->orderedFields;
277 0         0 $fh->print("\n");
278 0         0 $self;
279             }
280              
281              
282             sub printSelected($@)
283 0     0 1 0 { my ($self, $fh) = (shift, shift);
284              
285 0         0 foreach my $field ($self->orderedFields)
286 0         0 { my $Name = $field->Name;
287 0         0 my $name = $field->name;
288              
289 0         0 my $found;
290 0         0 foreach my $pattern (@_)
291 0 0       0 { $found = ref $pattern ? ($Name =~ $pattern) : ($name eq lc $pattern);
292 0 0       0 last if $found;
293             }
294              
295 0 0       0 if(!$found) { ; }
296 0         0 else { $fh->print("\n") }
297             }
298              
299 0         0 $self;
300             }
301              
302              
303 1     1 0 839 sub toString() { $_[0]->string }
304             sub string()
305 6     6 1 13 { my $self = shift;
306              
307 6         30 my @lines = map $_->string, $self->orderedFields;
308 6         24 push @lines, "\n";
309              
310 6 50       74 wantarray ? @lines : join('', @lines);
311             }
312              
313              
314             sub resentGroups()
315 3     3 1 512 { my $self = shift;
316 3         552 require Mail::Message::Head::ResentGroup;
317 3         19 Mail::Message::Head::ResentGroup->from($self);
318             }
319              
320              
321             sub addResentGroup(@)
322 3     3 1 638 { my $self = shift;
323              
324 3         23 require Mail::Message::Head::ResentGroup;
325 3 100       15 my $rg = @_==1 ? (shift) : Mail::Message::Head::ResentGroup->new(@_);
326              
327 3         12 my @fields = $rg->orderedFields;
328 3         10 my $order = $self->{MMH_order};
329              
330             # Look for the first line which relates to resent groups
331 3         5 my $i;
332 3         12 for($i=0; $i < @$order; $i++)
333 13 50       23 { defined $order->[$i] or next;
334 13 100       26 last if $rg->isResentGroupFieldName($order->[$i]->name);
335             }
336              
337 3         6 my $known = $self->{MMH_fields};
338 3         18 while(@fields)
339 15         17 { my $f = pop @fields;
340              
341             # Add to the order of fields
342 15         24 splice @$order, $i, 0, $f;
343 15         17 weaken( $order->[$i] );
344 15         24 my $name = $f->name;
345              
346             # Adds *before* in the list for get().
347 15 100       24 if(!defined $known->{$name}) { $known->{$name} = $f }
  13 50       27  
348 0         0 elsif(ref $known->{$name} eq 'ARRAY'){ unshift @{$known->{$name}}, $f }
  0         0  
349 2         4 else { $known->{$name} = [$f, $known->{$name}] }
350             }
351              
352 3         9 $rg->messageHead($self);
353              
354             # Oh, the header has changed!
355 3         12 $self->modified(1);
356              
357 3         6 $rg;
358             }
359              
360              
361             sub listGroup()
362 18     18 1 39 { my $self = shift;
363 18         1626 eval "require 'Mail::Message::Head::ListGroup'";
364 18         183 Mail::Message::Head::ListGroup->from($self);
365             }
366              
367              
368             sub addListGroup($)
369 0     0 1 0 { my ($self, $lg) = @_;
370 0         0 $lg->attach($self);
371             }
372              
373              
374             sub spamGroups(@)
375 43     43 1 1347 { my $self = shift;
376 43         331 require Mail::Message::Head::SpamGroup;
377 43 100       160 my @types = @_ ? (types => \@_) : ();
378 43         266 my @sgs = Mail::Message::Head::SpamGroup->from($self, @types);
379 43 100 100     321 wantarray || @_ != 1 ? @sgs : $sgs[0];
380             }
381              
382              
383             sub addSpamGroup($)
384 0     0 1 0 { my ($self, $sg) = @_;
385 0         0 $sg->attach($self);
386             }
387              
388             #--------------------
389              
390 2 50   2 1 11 sub timestamp() { $_[0]->guessTimestamp || time }
391              
392              
393             sub recvstamp()
394 2     2 1 4 { my $self = shift;
395 2 100       11 return $self->{MMH_recvstamp} if exists $self->{MMH_recvstamp};
396              
397             my $recvd = $self->get('received', 0)
398 1 50       5 or return $self->{MMH_recvstamp} = undef;
399              
400 0         0 my $stamp = Mail::Message::Field->dateToTimestamp($recvd->comment);
401              
402 0 0 0     0 $self->{MMH_recvstamp} = defined $stamp && $stamp > 0 ? $stamp : undef;
403             }
404              
405              
406             sub guessTimestamp()
407 2     2 0 4 { my $self = shift;
408 2 100       16 return $self->{MMH_timestamp} if exists $self->{MMH_timestamp};
409              
410 1         2 my $stamp;
411 1 50       5 if(my $date = $self->get('date'))
412 1         5 { $stamp = Mail::Message::Field->dateToTimestamp($date);
413             }
414              
415 1 50       919 unless($stamp)
416 0         0 { foreach (reverse $self->get('received'))
417 0         0 { $stamp = Mail::Message::Field->dateToTimestamp($_->comment);
418 0 0       0 last if $stamp;
419             }
420             }
421              
422 1 50 33     22 $self->{MMH_timestamp} = defined $stamp && $stamp > 0 ? $stamp : undef;
423             }
424              
425             sub guessBodySize()
426 112     112 1 1074 { my $self = shift;
427              
428 112         315 my $cl = $self->get('Content-Length');
429 112 100 66     536 return $1 if defined $cl && $cl =~ m/(\d+)/;
430              
431 80         221 my $lines = $self->get('Lines'); # 40 chars per lines
432 80 100 66     334 return $1 * 40 if defined $lines && $lines =~ m/(\d+)/;
433              
434 78         180 undef;
435             }
436              
437             #--------------------
438              
439             sub createFromLine()
440 0     0 1 0 { my $self = shift;
441 0         0 my $sender = $self->message->sender;
442 0   0     0 my $stamp = $self->recvstamp || $self->timestamp || time;
443 0 0       0 my $addr = defined $sender ? $sender->address : 'unknown';
444 0         0 "From $addr ".(gmtime $stamp)."\n"
445             }
446              
447              
448             my $msgid_creator;
449              
450             sub createMessageId()
451 152   66 152 1 451 { $msgid_creator ||= $_[0]->messageIdPrefix;
452 152         464 $msgid_creator->(@_);
453             }
454              
455              
456             sub messageIdPrefix(;$$)
457 16     16 1 49 { my $thing = shift;
458 16 50 33     87 return $msgid_creator
459             if defined $msgid_creator && !@_;
460              
461 16 50 33     92 return $msgid_creator = shift
462             if @_==1 && ref $_[0] eq 'CODE';
463              
464 16   33     241 my $prefix = shift || "mailbox-$$";
465              
466 16         209 my $hostname = shift;
467 16 50       94 if(!defined $hostname)
468 16         1405 { eval "require Net::Domain";
469 16 50       174853 $@ or $hostname = Net::Domain::hostfqdn();
470             }
471 16   0     6286 $hostname ||= hostname || 'localhost';
      33        
472              
473 16         1477 eval "require Time::HiRes";
474 16 50       296 if(Time::HiRes->can('gettimeofday'))
475             {
476             return $msgid_creator = sub {
477 152     152   829 my ($sec, $micro) = Time::HiRes::gettimeofday();
478 152         1043 "$prefix-$sec-$micro\@$hostname";
479 16         188 };
480             }
481              
482 0           my $unique_id = time;
483 0     0     $msgid_creator = sub { $unique_id++; "$prefix-$unique_id\@$hostname" };
  0            
  0            
484             }
485              
486             1;