File Coverage

lib/Mail/Make/Headers.pm
Criterion Covered Total %
statement 397 513 77.3
branch 101 206 49.0
condition 49 114 42.9
subroutine 58 66 87.8
pod 26 26 100.0
total 631 925 68.2


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## MIME Email Builder - ~/lib/Mail/Make/Headers.pm
3             ## Version v0.9.0
4             ## Copyright(c) 2026 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2026/03/02
7             ## Modified 2026/03/05
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package Mail::Make::Headers;
14             BEGIN
15             {
16 9     9   206585 use strict;
  9         13  
  9         323  
17 9     9   39 use warnings;
  9         23  
  9         527  
18 9     9   81 warnings::register_categories( 'Mail::Make' );
19 9     9   824 use parent qw( Module::Generic );
  9         656  
  9         48  
20 9     9   459079 use vars qw( $VERSION $EXCEPTION_CLASS $SUPPORTED $CRLF );
  9         17  
  9         585  
21 9     9   903 use Mail::Make::Exception;
  9         25  
  9         75  
22 9     9   7150 use Mail::Make::Headers::ContentDisposition;
  9         26  
  9         129  
23 9     9   6637 use Mail::Make::Headers::ContentTransferEncoding;
  9         28  
  9         68  
24 9     9   8050 use Mail::Make::Headers::ContentType;
  9         22  
  9         125  
25 9     9   2539 use Mail::Make::Headers::Generic;
  9         16  
  9         64  
26 9     9   6030 use Mail::Make::Headers::MessageID;
  9         22  
  9         86  
27 9     9   6334 use Mail::Make::Headers::Subject;
  9         25  
  9         93  
28 9     9   6603 use MM::Table ();
  9         23  
  9         281  
29 9     9   3371 use MM::Const qw( :table );
  9         26  
  9         1976  
30             # use Wanted;
31 9         16 our $CRLF = "\015\012";
32 9         75 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
33             # Maps normalised header name (lowercase, no hyphens) to typed class
34 9         98 our $SUPPORTED =
35             {
36             contentdisposition => 'Mail::Make::Headers::ContentDisposition',
37             contenttransferencoding => 'Mail::Make::Headers::ContentTransferEncoding',
38             contenttype => 'Mail::Make::Headers::ContentType',
39             messageid => 'Mail::Make::Headers::MessageID',
40             subject => 'Mail::Make::Headers::Subject',
41             };
42 9         211 our $VERSION = 'v0.9.0';
43             };
44              
45 9     9   52 use strict;
  9         15  
  9         205  
46 9     9   37 use warnings;
  9         15  
  9         19080  
47              
48             my $header_fields_canonical =
49             {
50             'from' => 'From',
51             'to' => 'To',
52             'cc' => 'Cc',
53             'bcc' => 'Bcc',
54             'reply-to' => 'Reply-To',
55             'sender' => 'Sender',
56             'subject' => 'Subject',
57             'date' => 'Date',
58             'message-id' => 'Message-ID',
59             'in-reply-to' => 'In-Reply-To',
60             'references' => 'References',
61             'return-path' => 'Return-Path',
62             'received' => 'Received',
63             'mime-version' => 'MIME-Version',
64             'content-type' => 'Content-Type',
65             'content-transfer-encoding' => 'Content-Transfer-Encoding',
66             'content-disposition' => 'Content-Disposition',
67             'content-id' => 'Content-ID',
68             'content-description' => 'Content-Description',
69             };
70              
71             my $header_fields_order =
72             {
73             'return-path' => 10,
74             'received' => 20,
75             'date' => 30,
76             'from' => 40,
77             'sender' => 50,
78             'reply-to' => 60,
79             'to' => 70,
80             'cc' => 80,
81             'bcc' => 90,
82             'message-id' => 100,
83             'in-reply-to' => 110,
84             'references' => 120,
85             'subject' => 130,
86             'mime-version' => 200,
87             'content-type' => 210,
88             'content-transfer-encoding' => 220,
89             'content-disposition' => 230,
90             'content-id' => 240,
91             'content-description' => 250,
92             };
93             my $fqdn_re = qr/\A[A-Za-z0-9](?:[A-Za-z0-9\-\.]*[A-Za-z0-9])?\z/;
94             my %valid_encoding = map{ $_ => 1 } qw( 7bit 8bit binary base64 quoted-printable );
95              
96             sub init
97             {
98 187     187 1 748818 my $self = shift( @_ );
99             # Internal ordered list of [ $name, $value ] pairs to preserve order
100 187         1327 $self->{_headers} = [];
101 187         817 $self->{_exception_class} = $EXCEPTION_CLASS;
102 187         690 $self->{_init_strict_use_sub} = 1;
103 187         2537 $self->{_t} = MM::Table->make;
104 187 100       766 if( @_ )
105             {
106 1 50       4 if( @_ % 2 )
107             {
108 0         0 return( $self->error( "new needs an even number of arguments" ) );
109             }
110             # We need to preserve the order in which the header field names were provided.
111 1         5 for( my $i = 0; $i < scalar( @_ ); $i += 2 )
112             {
113 3 50 33     15 if( defined( $_[$i] ) && $_[$i] eq 'debug' )
114             {
115 0         0 $self->debug( $_[$i + 1] );
116 0         0 next;
117             }
118 3 50       9 $self->push_header( $_[$i] => $_[$i + 1] ) ||
119             return( $self->pass_error );
120             }
121             }
122 187 50       1414 $self->SUPER::init() || return( $self->pass_error );
123 187         16264 return( $self );
124             }
125              
126             # instead of aliasing it, we redirect so it shows up in a stack trace.
127 0     0 1 0 sub add { return( shift->push_header( @_ ) ); }
128              
129             # Returns the string representation of all headers, CRLF-terminated,
130             # ready to be written to a message (without the trailing blank line).
131             sub as_string
132             {
133 68     68 1 185 my $self = shift( @_ );
134 68 100       281 my $eol = @_ ? shift( @_ ) : $CRLF;
135 68 50       231 my $max = @_ ? shift( @_ ) : 78;
136 68 50 66     349 if( $self->{_cache_value} &&
      66        
      33        
      33        
137             $self->{_cache_value}->[0] eq $eol &&
138             $self->{_cache_value}->[1] == $max &&
139             $self->{_cache_value}->[2] &&
140             !CORE::length( $self->{_reset} ) )
141             {
142 2         8 return( $self->{_cache_value}->[2] );
143             }
144              
145             # WARNING: sorting email headers can be semantically risky (e.g. Received:).
146 66         224 my @pairs;
147             $self->{_t}->do(sub
148             {
149 339     339   702 my( $k, $v ) = @_;
150 339         1010 push( @pairs, [ $k, $v ] );
151 339         604 return(1);
152 66         1007 });
153              
154             @pairs = sort
155             {
156 66         656 $self->_mail_header_order( $a->[0] ) <=> $self->_mail_header_order( $b->[0] )
  525         1145  
157             } @pairs;
158              
159 66         128 my $out = '';
160 66         250 for( my $i = 0; $i < @pairs; $i++ )
161             {
162 339         749 my $line = $self->_display_name( $pairs[ $i ]->[0] ) . ': ' . $pairs[ $i ]->[1];
163              
164 339         892 $out .= $self->_fold_header_line( $line, $eol, $max ) . $eol;
165             }
166              
167 66         442 $self->{_cache_value} = [$eol, $max, $out];
168 66         303 CORE::delete( $self->{_reset} );
169 66         602 return( $out );
170             }
171              
172             sub as_string_without_sort
173             {
174 1     1 1 8 my $self = shift( @_ );
175 1 50       5 my $eol = @_ ? shift( @_ ) : $CRLF;
176              
177 1         2 my $out = '';
178             $self->{_t}->do(sub
179             {
180 3     3   7 my( $k, $v ) = @_;
181 3         9 $out .= $k . ': ' . $v . $eol;
182 3         6 return(1);
183 1         11 });
184              
185 1         8 return( $out );
186             }
187              
188             sub clear
189             {
190 1     1 1 3 my $self = shift( @_ );
191 1         4 $self->{_t}->clear();
192 1         1 return( $self );
193             }
194              
195             sub clone
196             {
197 1     1 1 5 my $self = shift( @_ );
198              
199 1         4 my $c = ref( $self )->new();
200 1         8 $c->{_t} = $self->{_t}->copy( undef );
201              
202 1         3 return( $c );
203             }
204              
205             # content_disposition - convenience typed accessor
206             sub content_disposition
207             {
208 0     0 1 0 my $self = shift( @_ );
209 0 0       0 if( @_ )
210             {
211 0         0 my $val = shift( @_ );
212 0 0       0 $val = "$val" if( ref( $val ) );
213 0         0 $self->reset(1);
214 0         0 return( $self->set( 'Content-Disposition', $val ) );
215             }
216 0         0 return( $self->new_field( 'Content-Disposition' ) );
217             }
218              
219             # content_id - convenience accessor for Content-ID
220             sub content_id
221             {
222 3     3 1 15 my $self = shift( @_ );
223 3 50       5 if( @_ )
224             {
225 3         5 my $cid = shift( @_ );
226             # Normalise: strip surrounding angle brackets if present, then re-add
227 3         8 $cid =~ s/^<//;
228 3         11 $cid =~ s/>$//;
229 3 100       10 if( $cid =~ /[\x00-\x1F\x7F]/ )
230             {
231 1         5 return( $self->error( "Invalid Content-ID value '$cid': contains illegal characters." ) );
232             }
233 2         8 $self->reset(1);
234 2         8 return( $self->set( 'Content-ID', "<${cid}>" ) );
235             }
236 0         0 return( $self->get( 'Content-ID' ) );
237             }
238              
239             # content_transfer_encoding - convenience accessor
240             sub content_transfer_encoding
241             {
242 57     57 1 3490 my $self = shift( @_ );
243 57 100       161 if( @_ )
244             {
245 7         47 my $enc = lc( shift( @_ ) );
246 7 100       22 unless( exists( $valid_encoding{ $enc } ) )
247             {
248 1         7 return( $self->error( "Unknown Content-Transfer-Encoding '$enc'." ) );
249             }
250 6         15 $self->reset(1);
251 6         15 return( $self->set( 'Content-Transfer-Encoding', $enc ) );
252             }
253 50         186 return( $self->get( 'Content-Transfer-Encoding' ) );
254             }
255              
256             # content_type - convenience typed accessor
257             sub content_type
258             {
259 2     2 1 10 my $self = shift( @_ );
260 2 100       6 if( @_ )
261             {
262 1         2 my $val = shift( @_ );
263 1 50       2 $val = "$val" if( ref( $val ) );
264 1         4 $self->reset(1);
265 1         3 return( $self->set( 'Content-Type', $val ) );
266             }
267 1         5 return( $self->new_field( 'Content-Type' ) );
268             }
269              
270             # $hdr->date( $my_date );
271             # $hdr->date( $my_date, { strict => 1 } );
272             sub date
273             {
274 2     2 1 801 my $self = shift( @_ );
275 2 50       5 my $has_args = scalar( @_ ) ? 1 : 0;
276 2         3 my $opts = {};
277 2 50 33     10 $opts = pop( @_ ) if( scalar( @_ ) && ref( $_[-1] ) eq 'HASH' );
278              
279             # Accessor mode
280 2 50       5 if( @_ == 0 )
281             {
282 0 0       0 if( $has_args )
283             {
284 0         0 return( $self->error( "No date was provided." ) );
285             }
286 0         0 return( $self->header( 'Date' ) );
287             }
288              
289             # Mutator mode
290 2         2 my $v = shift( @_ );
291              
292 2 50 33     7 if( defined( $v ) && !ref( $v ) )
293             {
294             # epoch seconds -> RFC 5322 date-time
295 2 100       15 if( $v =~ /^[[:blank:]]*-?\d+[[:blank:]]*$/ )
    50          
296             {
297 1         2 my $epoch = $v;
298 1         3 $epoch =~ s/^[[:blank:]]+//;
299 1         2 $epoch =~ s/[[:blank:]]+$//;
300            
301 1   50     4 my $date = $self->_format_rfc5322_date( $epoch ) ||
302             return( $self->pass_error );
303            
304 1         35 $self->header( 'Date' => $date );
305 1         2 $self->reset(1);
306 1         3 return( $self );
307             }
308             # Non-numeric: accept as-is, but optionally validate
309             elsif( $opts->{strict} )
310             {
311 0 0       0 $self->_validate_date_value( $v ) ||
312             return( $self->pass_error );
313             }
314             # else, anything goes...
315             }
316              
317 1         4 $self->reset(1);
318 1         2 $self->header( 'Date' => $v );
319             # We return the current instance for chaining.
320 1         2 return( $self );
321             }
322              
323             sub exists
324             {
325 57     57 1 164 my $self = shift( @_ );
326              
327 57 50       221 if( @_ != 1 )
328             {
329 0         0 return( $self->error( "exists expects exactly one argument" ) );
330             }
331              
332 57   50     185 my $cname = $self->_canon_name( $_[0] ) || return( $self->pass_error );
333              
334 57         224 my $v = $self->{_t}->get( $cname );
335              
336 57 100       299 return( defined( $v ) ? 1 : 0 );
337             }
338              
339             # get( $name ) - returns the raw string value of the first matching header
340             sub get
341             {
342 172     172 1 2285 my $self = shift( @_ );
343              
344 172 50       625 if( @_ != 1 )
345             {
346 0         0 return( $self->error( "get expects exactly one argument" ) );
347             }
348              
349 172         800 return( $self->header( $_[0] ) );
350             }
351              
352             # has( $name ) - boolean existence check
353             {
354 9     9   71 no warnings 'once';
  9         16  
  9         16188  
355             # NOTE: sub has aliased to 'has'
356             *has = \&exists;
357             }
358              
359             sub header
360             {
361 333     333 1 3708 my $self = shift( @_ );
362              
363 333 50       905 if( @_ == 0 )
364             {
365 0         0 return( $self->error( "header called with no arguments" ) );
366             }
367              
368             # Getter
369 333 100       829 if( @_ == 1 )
370             {
371 285         1144 my $name = $_[0];
372 285   50     745 my $cname = $self->_canon_name( $name ) || return( $self->pass_error );
373              
374 285         1732 my @vals = $self->{_t}->get( $cname );
375              
376 285 100       648 if( !@vals )
377             {
378 127         1667 return;
379             }
380              
381 158 100       1223 return( wantarray() ? @vals : join( ', ', @vals ) );
382             }
383              
384             # Setter
385 48 50       159 if( @_ % 2 )
386             {
387 0         0 return( $self->error( "header needs one or an even number of arguments" ) );
388             }
389              
390 48         75 my %cleared;
391 48         294 for( my $i = 0; $i < @_; $i += 2 )
392             {
393 48         288 my $name = $_[$i];
394 48         128 my $val = $_[$i + 1];
395              
396 48   50     126 my $cname = $self->_canon_name( $name ) || return( $self->pass_error );
397              
398 48         265 my $lkey = lc( $cname );
399 48         97 $lkey =~ tr/_/-/;
400              
401 48 50       317 if( !$cleared{ $lkey }++ )
402             {
403 48         313 $self->{_t}->unset( $cname );
404             }
405              
406             # We get back an array reference
407 48   50     151 my $vals = $self->_coerce_values( $val ) || return( $self->pass_error );
408              
409 48         166 for( my $j = 0; $j < @$vals; $j++ )
410             {
411 48         190 $self->{_t}->add( $cname => $vals->[ $j ] );
412             }
413             }
414 48         193 $self->reset(1);
415              
416             # We return the current instance for chaining
417 48         108 return( $self );
418             }
419              
420             sub header_field_names
421             {
422 0     0 1 0 my $self = shift( @_ );
423              
424 0         0 my %seen;
425             my @out;
426              
427             $self->{_t}->do(sub
428             {
429 0     0   0 my( $k, $v ) = @_;
430              
431 0         0 my $lkey = lc( $k );
432 0         0 $lkey =~ tr/_/-/;
433              
434 0 0       0 if( !$seen{ $lkey }++ )
435             {
436 0   0     0 my $name = $self->_display_name( $k ) || return( $self->pass_error );
437 0         0 push( @out, $name );
438             }
439 0         0 return(1);
440 0         0 });
441              
442 0 0       0 return( wantarray() ? @out : \@out );
443             }
444              
445             sub init_header
446             {
447 376     376 1 619 my $self = shift( @_ );
448              
449 376 50       923 if( @_ % 2 )
450             {
451 0         0 return( $self->error( "init_header needs an even number of arguments" ) );
452             }
453              
454 376         899 for( my $i = 0; $i < @_; $i += 2 )
455             {
456 376         891 my $name = $_[$i];
457 376         670 my $val = $_[$i + 1];
458              
459 376   50     773 my $cname = $self->_canon_name( $name ) ||
460             return( $self->pass_error );
461              
462 376         1359 my @existing = $self->{_t}->get( $cname );
463 376 100       947 next if( @existing );
464              
465 316 50       988 $self->push_header( $cname => $val ) ||
466             return( $self->pass_error );
467             }
468 376         831 $self->reset(1);
469 376         827 return( $self );
470             }
471              
472             # length()
473             # Returns the total number of header field entries currently stored,
474             # counting each value separately for multi-valued fields.
475             # Returns 0 when no headers have been set.
476             sub length
477             {
478 0     0 1 0 my $self = shift( @_ );
479 0         0 my $st = $self->{_t}->_state();
480 0         0 return( scalar( @{$st->{_entries}} ) );
  0         0  
481             }
482              
483             # $hdr->message_id( undef ); # remove
484             # $hdr->message_id( undef, { strict => 1 } ); # same: remove
485             sub message_id
486             {
487 45     45 1 100 my $self = shift( @_ );
488 45 50       171 my $has_args = scalar( @_ ) ? 1 : 0;
489 45         82 my $opts = {};
490 45 50 33     581 $opts = pop( @_ ) if( scalar( @_ ) && ref( $_[-1] ) eq 'HASH' );
491              
492             # Generate mode
493             # Allow: $hdr->message_id( { generate => 1, domain => 'example.com' } )
494 45 50       207 if( $opts->{generate} )
495             {
496 45         105 my $domain = $opts->{domain};
497 45 50 33     452 if( !defined( $domain ) || $domain eq '' )
498             {
499             # Sys::Hostname is a core module, so it is guaranteed to be available.
500 0 0       0 $self->_load_class( 'Sys::Hostname' ) ||
501             return( $self->pass_error );
502 0         0 local $@;
503             # Sys::Hostname::hostname() croaks, so we need to catch that.
504             eval
505 0         0 {
506 0         0 $domain = Sys::Hostname::hostname();
507             };
508             }
509              
510             # Need to check the user-provided value against rfc1123 and rfc952 using regular expression
511             # Validate domain: require dot + RFC-ish hostname
512 45 50 33     1318 if( !defined( $domain ) ||
      33        
513             # Better than using a regular expression, and we check it first, before executing the following complexe regular expression.
514             index( $domain, '.' ) == -1 ||
515             # We use a pre-compiled regular expression
516             $domain !~ $fqdn_re )
517             {
518             # Let's be clear, and tell the user what is wrong so he does not need to go looking in the code to understand.
519 0         0 return( $self->error( "Invalid Message-ID domain '$domain'. You need to specify a domain with the option 'domain'." ) );
520             }
521              
522 45   50     367 my $mid = $self->_generate_message_id( $domain ) ||
523             return( $self->pass_error );
524              
525 45         198 $self->reset(1);
526 45         187 $self->header( 'Message-ID' => $mid );
527 45         230 return( $mid );
528             }
529              
530             # Accessor mode
531 0 0       0 if( @_ == 0 )
532             {
533 0 0       0 if( $has_args )
534             {
535 0         0 return( $self->error( "No Message-ID value was provided." ) );
536             }
537 0         0 return( $self->header( 'Message-ID' ) );
538             }
539              
540 0         0 my $v = shift( @_ );
541              
542             # undef => remove
543 0 0       0 if( !defined( $v ) )
544             {
545 0         0 $self->reset(1);
546 0         0 $self->remove_header( 'Message-ID' );
547 0         0 return( $self );
548             }
549              
550 0 0       0 if( ref( $v ) )
551             {
552 0 0       0 if( !$self->_can_overload( $v => '""' ) )
553             {
554 0         0 return( $self->error( "Invalid Message-ID value (", $self->_str_val( $v ), ")." ) );
555             }
556 0         0 $v = "$v";
557             }
558              
559 0 0       0 if( $opts->{strict} )
560             {
561 0 0       0 $self->_validate_message_id_value( $v ) ||
562             return( $self->pass_error );
563             }
564              
565 0         0 $self->reset(1);
566 0         0 $self->header( 'Message-ID' => $v );
567             # We return the current instance for chaining
568 0         0 return( $self );
569             }
570              
571             # new_field( $name, $value ) - factory returning a typed object or Generic
572             # e.g. $headers->new_field( 'Content-Type' )
573             sub new_field
574             {
575 3     3 1 27 my $self = shift( @_ );
576 3   50     10 my $name = shift( @_ ) || return( $self->error( "No field name provided." ) );
577 3         7 my $value = shift( @_ );
578 3   50     12 my $key = $self->_normalise_name( $name ) || return( $self->pass_error );
579 3   50     13 my $class = $SUPPORTED->{ $key } || 'Mail::Make::Headers::Generic';
580 3 50       19 $self->_load_class( $class ) || return( $self->pass_error );
581 3 100 66     2740 if( defined( $value ) && CORE::length( $value ) )
582             {
583 2   33     27 return( $class->new( $value ) || $self->pass_error( $class->error ) );
584             }
585 1   33     9 return( $class->new || $self->pass_error( $class->error ) );
586             }
587              
588             # print( $fh ) - writes all headers + blank line to filehandle
589             sub print
590             {
591 0     0 1 0 my $self = shift( @_ );
592 0         0 my $fh = shift( @_ );
593 0 0       0 my $eol = @_ ? $_[0] : $CRLF;
594 0 0       0 $fh->print( $self->as_string( @_ ) ) ||
595             return( $self->error( "Unable to print headers: $!" ) );
596 0 0       0 $fh->print( $eol ) ||
597             return( $self->error( "Unable to print blank line after headers: $!" ) );
598 0         0 return( $self );
599             }
600              
601             sub push_header
602             {
603 338     338 1 646 my $self = shift( @_ );
604              
605 338 50       834 if( @_ % 2 )
606             {
607 0         0 return( $self->error( "push_header needs an even number of arguments" ) );
608             }
609              
610 338         1031 for( my $i = 0; $i < @_; $i += 2 )
611             {
612 338         692 my $name = $_[$i];
613 338         702 my $val = $_[$i + 1];
614              
615 338   50     681 my $cname = $self->_canon_name( $name ) ||
616             return( $self->pass_error );
617              
618             # We get back an array reference
619 338   50     834 my $vals = $self->_coerce_values( $val ) ||
620             return( $self->pass_error );
621              
622 338         797 for( my $j = 0; $j < @$vals; $j++ )
623             {
624 340         1251 $self->{_t}->add( $cname => $vals->[ $j ] );
625             }
626             }
627 338         1035 $self->reset(1);
628              
629 338         1464 return( $self );
630             }
631              
632             # remove( $name ) - removes all headers with the given name
633             {
634 9     9   86 no warnings 'once';
  9         18  
  9         2623  
635             # NOTE: sub remove is aliased to remove_header()
636             *remove = \&remove_header;
637             }
638              
639             sub remove_header
640             {
641 13     13 1 48 my $self = shift( @_ );
642 13 50       52 if( @_ == 0 )
643             {
644 0         0 return( $self->error( "remove_header called with no arguments" ) );
645             }
646              
647 13         27 my @removed_all;
648              
649 13         94 for( my $i = 0; $i < @_; $i++ )
650             {
651 14         259 my $name = $_[$i];
652 14   50     56 my $cname = $self->_canon_name( $name ) ||
653             return( $self->pass_error );
654              
655 14         78 my @vals = $self->{_t}->get( $cname );
656 14 100       52 push( @removed_all, @vals ) if( @vals );
657              
658 14         65 $self->{_t}->unset( $cname );
659             }
660 13 100       79 $self->reset(1) if( scalar( @removed_all ) );
661              
662 13 100       92 return( wantarray() ? @removed_all : ( scalar( @removed_all ) ? $removed_all[-1] : 0 ) );
    100          
663             }
664              
665             {
666 9     9   70 no warnings 'once';
  9         26  
  9         13797  
667             # NOTE: sub replace is aliased to replace_header()
668             *replace = \&replace_header;
669             }
670              
671             sub replace_header
672             {
673 381     381 1 35953664 my $self = shift( @_ );
674              
675 381 50       1290 if( @_ == 0 )
676             {
677 0         0 return( $self->error( "replace_header called with no arguments" ) );
678             }
679              
680 381 50       1349 if( @_ % 2 )
681             {
682 0         0 return( $self->error( "replace_header needs an even number of arguments" ) );
683             }
684              
685 381         755 my %cleared;
686 381         1131 for( my $i = 0; $i < @_; $i += 2 )
687             {
688 381         1265 my $name = $_[$i];
689 381         1021 my $val = $_[$i + 1];
690              
691 381   100     1310 my $cname = $self->_canon_name( $name ) ||
692             return( $self->pass_error );
693              
694 379         791 my $lkey = lc( $cname );
695 379         678 $lkey =~ tr/_/-/;
696              
697 379 50       2546 if( !$cleared{ $lkey }++ )
698             {
699 379         2571 $self->{_t}->unset( $cname );
700             }
701              
702             # undef => remove header (no re-add)
703 379 50       974 next if( !defined( $val ) );
704              
705             # We get back an array reference
706 379   50     1197 my $vals = $self->_coerce_values( $val ) || return( $self->pass_error );
707 379         1049 for( my $j = 0; $j < @$vals; $j++ )
708             {
709 379         1793 $self->{_t}->add( $cname => $vals->[ $j ] );
710             }
711             }
712 379         1301 $self->reset(1);
713              
714             # We return the current instance for chaining.
715 379         1568 return( $self );
716             }
717              
718             sub reset
719             {
720 1201     1201 1 1711 my $self = shift( @_ );
721 1201 50 100     4155 $self->{_reset} = scalar( @_ ) if( !CORE::length( $self->{_reset} ) && scalar( @_ ) );
722 1201         1642 return( $self );
723             }
724              
725             sub scan
726             {
727 53     53 1 109 my $self = shift( @_ );
728 53   50     186 my $cb = shift( @_ ) ||
729             return( $self->error( "No callback was provided." ) );
730              
731 53 50       299 if( ref( $cb ) ne 'CODE' )
732             {
733 0         0 return( $self->error( "scan expects a CODE reference" ) );
734             }
735              
736             $self->{_t}->do(sub
737             {
738 269     269   721 my( $k, $v ) = @_;
739 269         677 $cb->( $k, $v );
740 269         719 return(1);
741 53         873 });
742              
743 53         334 return( $self );
744             }
745              
746             # instead of aliasing it, we redirect so it shows up in a stack trace.
747 161     161 1 228999 sub set { return( shift->replace_header( @_ ) ); }
748              
749             # replace( $name, $value ) - alias for set(). Provided for API compatibility.
750             # sub replace { return( shift->set( @_ ) ); }
751              
752             sub _canon_name
753             {
754 1499     1499   2249 my $self = shift( @_ );
755 1499   50     4006 my $name = shift( @_ ) ||
756             return( $self->error( "No header name was provided." ) );
757              
758 1499 100       4104 $self->_validate_field_name( $name ) || return( $self->pass_error );
759              
760 1497         3145 $name =~ tr/_/-/;
761 1497         3496 return( $self->_display_name( $name ) );
762             }
763              
764             sub _coerce_values
765             {
766 765     765   1270 my $self = shift( @_ );
767 765         1373 my $val = shift( @_ );
768 765 50 33     3154 unless( defined( $val ) && CORE::length( $val ) )
769             {
770 0         0 return( $self->error( "No header value was provided to coerce." ) );
771             }
772              
773 765         1203 my @vals;
774 765 100       1910 if( ref( $val ) eq 'ARRAY' )
775             {
776 2         10 for( my $i = 0; $i < @$val; $i++ )
777             {
778 4   50     13 my $clean = $self->_sanitize_value( $val->[$i] ) ||
779             return( $self->pass_error );
780 4         16 push( @vals, $clean );
781             }
782             }
783             else
784             {
785 763   50     2244 my $clean = $self->_sanitize_value( $val ) ||
786             return( $self->pass_error );
787 763         1926 push( @vals, $clean );
788             }
789              
790 765         2320 return( \@vals );
791             }
792              
793             sub _display_name
794             {
795 1836     1836   2809 my $self = shift( @_ );
796 1836   50     4144 my $name = shift( @_ ) ||
797             return( $self->error( "No header name was provided." ) );
798              
799 1836         3298 my $k = lc( $name );
800 1836         2483 $k =~ tr/_/-/;
801              
802 1836         4052 my $canon = $self->_mail_canonical_name( $k );
803 1836 50       3734 return( $self->pass_error ) if( !defined( $canon ) );
804 1836 100       6199 return( $canon ) if( $canon );
805              
806 54         187 return( join( '-', map{ ucfirst( $_ ) } split( /-/, $k ) ) );
  106         447  
807             }
808              
809             sub _fold_header_line
810             {
811 339     339   651 my $self = shift( @_ );
812 339         623 my( $line, $eol, $max ) = @_;
813              
814 339   50     706 $max ||= 78;
815             # Unsigned integer
816 339 50       1522 if( $max !~ /^\d+$/ )
817             {
818 0         0 return( $self->error( "The maximum line length value provided (", $self->_str_val( $max ), ") is not an unsigned integer." ) );
819             }
820              
821             # If already short enough, return unchanged
822 339 100       678 if( CORE::length( $line ) <= $max )
823             {
824 333         1404 return( $line );
825             }
826              
827 6         44 my $out = '';
828 6         36 while( CORE::length( $line ) > $max )
829             {
830 9         24 my $cut = $max;
831              
832             # Find last WSP within first $max chars
833 9         40 my $chunk = substr( $line, 0, $max + 1 );
834 9         28 my $pos_sp = rindex( $chunk, ' ' );
835 9         23 my $pos_tab = rindex( $chunk, "\t" );
836 9 50       37 my $pos = $pos_sp > $pos_tab ? $pos_sp : $pos_tab;
837              
838 9 50       40 if( $pos > 0 )
839             {
840 9         18 $cut = $pos;
841             }
842              
843 9         40 $out .= substr( $line, 0, $cut ) . $eol . ' ';
844              
845             # Drop leading WSP on the remainder
846 9         146 $line = substr( $line, $cut );
847 9         76 $line =~ s/^[ \t]+//;
848             }
849 6         17 $out .= $line;
850 6         35 return( $out );
851             }
852              
853             sub _format_rfc5322_date
854             {
855 1     1   1 my $self = shift( @_ );
856 1         2 my $epoch = shift( @_ );
857 1 50       2 if( !defined( $epoch ) )
858             {
859 0         0 return( $self->error( "No timestamp was provided to get the mail formatted date." ) );
860             }
861              
862             # Good until 2286
863 1 50       5 unless( $epoch =~ /^\d{1,10}$/ )
864             {
865 0   0     0 return( $self->error( "The timestamp provided is incorrect (", $self->_str_val( $epoch // 'undef' ), "). It should be a 10-digits integer." ) );
866             }
867              
868 1         3 my @wd = qw( Sun Mon Tue Wed Thu Fri Sat );
869 1         3 my @mo = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
870              
871             # localtime gives local TZ; we also need numeric offset +HHMM
872 1         9 my( $sec, $min, $hour, $mday, $mon, $year, $wday ) = localtime( $epoch );
873              
874 1         2 $year += 1900;
875              
876 1         4 my $off = $self->_tz_offset_hhmm( $epoch );
877              
878             # Because providing an $epoch value of 0 would trigger warnings of uninitialized values.
879 9     9   76 no warnings 'uninitialized';
  9         25  
  9         22632  
880             # Maybe use POSIX::strftime instead ?
881 1         7 return( sprintf(
882             "%s, %02d %s %04d %02d:%02d:%02d %s",
883             $wd[$wday],
884             $mday,
885             $mo[$mon],
886             $year,
887             $hour,
888             $min,
889             $sec,
890             $off
891             ) );
892             }
893              
894             sub _generate_message_id
895             {
896 45     45   130 my( $self, $domain ) = @_;
897              
898 45 50 33     790 if( !defined( $domain ) || $domain !~ /\A[A-Za-z0-9](?:[A-Za-z0-9\-\.]*[A-Za-z0-9])?\z/ || $domain !~ /\./ )
      33        
899             {
900 0         0 return( $self->error( "Invalid domain for Message-ID generation." ) );
901             }
902              
903 45   50     203 my $left = $self->_message_id_left_part() ||
904             return( $self->pass_error );
905              
906 45         359 return( '<' . $left . '@' . $domain . '>' );
907             }
908              
909             sub _mail_canonical_name
910             {
911 1836     1836   2585 my $self = shift( @_ );
912 1836   50     3621 my $name = shift( @_ ) ||
913             return( $self->error( "No header name was provided." ) );
914 1836         3134 my $name_lc = lc( $name );
915 1836 100       6923 return( $header_fields_canonical->{ $name_lc } ) if( exists( $header_fields_canonical->{ $name_lc } ) );
916 54         105 return( '' );
917             }
918              
919             sub _mail_header_order
920             {
921 1050     1050   1277 my $self = shift( @_ );
922 1050   50     1821 my $name_disp = shift( @_ ) ||
923             return( $self->error( "No header name was provided." ) );
924              
925 1050         1445 my $k = lc( $name_disp );
926 1050         1424 $k =~ tr/_/-/;
927             # Moved the header fields order definition table at the top of this module, as a private variable.
928 1050 100       2801 return( exists( $header_fields_order->{ $k } ) ? $header_fields_order->{ $k } : 1000 );
929             }
930              
931             sub _message_id_left_part
932             {
933 45     45   95 my( $self ) = @_;
934              
935             # Prefer Data::UUID if available
936 45 50       286 if( $self->_load_class( 'Data::UUID' ) )
937             {
938 45         45188 my $ug = Data::UUID->new;
939 45         726 my $bin = $ug->create; # 16 bytes
940              
941             # Base64url, no padding, no newline
942 45 50       241 $self->_load_class( 'MIME::Base64' ) || return( $self->pass_error );
943 45         32763 my $b64 = MIME::Base64::encode_base64( $bin, '' );
944 45         134 $b64 =~ tr!+/!-_!;
945 45         448 $b64 =~ s/=+\z//;
946              
947 45         592 return( $b64 );
948             }
949              
950             # Fallback: time+pid+seq+rand
951 0         0 my $t = time();
952 0         0 my $pid = $$;
953              
954 0         0 our $MSGID_SEQ;
955 0 0       0 $MSGID_SEQ = 0 if( !defined( $MSGID_SEQ ) );
956 0         0 $MSGID_SEQ++;
957              
958 0         0 my $r = int( rand( 0xFFFFFFFF ) );
959              
960 0         0 return( sprintf( "%x.%x.%x.%x", $t, $pid, $MSGID_SEQ, $r ) );
961             }
962              
963             sub _normalise_name
964             {
965 3     3   4 my $self = shift( @_ );
966 3   50     10 my $name = shift( @_ ) ||
967             return( $self->error( "No header name was provided to normalise." ) );
968 3         7 $name = lc( $name );
969 3         11 $name =~ tr/-//d; # More efficient than $name =~ s/-//g;
970 3         10 return( $name );
971             }
972              
973             sub _sanitize_value
974             {
975 767     767   1354 my $self = shift( @_ );
976 767         1176 my $v = shift( @_ );
977              
978 767 50       1562 $v = '' if( !defined( $v ) );
979              
980             # Freeze stringification NOW (important for objects like Module::Generic::HeaderValue)
981 767 50       1566 if( ref( $v ) )
982             {
983 0 0       0 if( !$self->_can_overload( $v => '""' ) )
984             {
985 0         0 $self->error( "Header value is a reference but is not stringifiable (", $self->_str_val( $v ), ")." );
986 0         0 return;
987             }
988 0         0 $v = "$v";
989             }
990              
991             # Allow RFC-style folding: CRLF or LF followed immediately by SP/HTAB.
992             # All other CR/LF sequences (i.e. attempted header injection) are replaced
993             # by a single space instead of being passed through.
994             # First, normalise CRLF -> LF
995 767         2013 $v =~ s/\r\n/\n/g;
996             # Bare CR -> LF
997 767         1428 $v =~ s/\r/\n/g;
998             # Unwrap legal folding: LF + WSP -> single space
999 767         1117 $v =~ s/\n[ \t]+/ /g;
1000             # Any remaining LF is injection - replace with space
1001 767         1125 $v =~ s/\n/ /g;
1002              
1003             # Remove ASCII control chars except tab
1004 767         1752 $v =~ s/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]//g;
1005              
1006 767         2048 return( $v );
1007             }
1008              
1009             # This is normally called by _tz_offset_hhmm()
1010             sub _timegm_approx
1011             {
1012 2     2   3 my $self = shift( @_ );
1013             # localtime() and gmtime() return 9 elements in list context; we only need the first 7.
1014 2 50       5 unless( scalar( @_ ) >= 7 )
1015             {
1016 0         0 return( $self->error(
1017             q{_timegm_approx() called improperly. You need to call it as $self->_timegm_approx( $sec, $min, $hour, $mday, $mon, $year, $wday )}
1018             ) );
1019             }
1020             # Discard $yday and $isdst if present
1021 2         5 my @args = @_[0..6];
1022 2         4 my @params = qw( seconds minutes hours day month year weekday );
1023 2         5 for( my $i = 0; $i < scalar( @args ); $i++ )
1024             {
1025 14 50       272 if( !$self->_is_number( $args[$i] ) )
1026             {
1027 0   0     0 return( $self->error(
1028             "The ", $params[$i],
1029             " parameter at offset $i needs to be an integer, but I got '",
1030             $self->_str_val( $args[$i] // 'undef' ), "'"
1031             ) );
1032             }
1033             }
1034 2         80 my( $sec, $min, $hour, $mday, $mon, $year, $wday ) = @args;
1035              
1036             # localtime/gmtime give: year since 1900, mon 0..11
1037 2         4 $year += 1900;
1038              
1039             # Days since epoch, using a civil->days algorithm (no modules).
1040 2         2 my $y = $year;
1041 2         4 my $m = $mon + 1;
1042              
1043 2 50       5 if( $m <= 2 )
1044             {
1045 2         3 $y -= 1;
1046 2         3 $m += 12;
1047             }
1048              
1049 2         5 my $era = int( $y / 400 );
1050 2         4 my $yoe = $y - $era * 400;
1051 2         4 my $doy = int( ( 153 * ( $m - 3 ) + 2 ) / 5 ) + $mday - 1;
1052 2         5 my $doe = $yoe * 365 + int( $yoe / 4 ) - int( $yoe / 100 ) + $doy;
1053              
1054             # 719468 is days from 0000-03-01 to 1970-01-01
1055 2         2 my $days = $era * 146097 + $doe - 719468;
1056              
1057 2         5 return( $days * 86400 + $hour * 3600 + $min * 60 + $sec );
1058             }
1059              
1060             sub _tz_offset_hhmm
1061             {
1062 1     1   2 my $self = shift( @_ );
1063 1         1 my $epoch = shift( @_ );
1064              
1065             # Compute offset by comparing localtime and gmtime representations.
1066             # This avoids non-core modules and works across DST changes.
1067 1         3 my @l = localtime( $epoch );
1068 1         3 my @g = gmtime( $epoch );
1069              
1070 1         2 my $lsec = $self->_timegm_approx( @l );
1071 1 50       3 return( $self->pass_error ) if( !defined( $lsec ) );
1072 1         3 my $gsec = $self->_timegm_approx( @g );
1073 1 50       4 return( $self->pass_error ) if( !defined( $gsec ) );
1074              
1075 1         2 my $delta = $lsec - $gsec; # seconds east of UTC
1076              
1077 1         1 my $sign = '+';
1078 1 50       3 if( $delta < 0 )
1079             {
1080 0         0 $sign = '-';
1081 0         0 $delta = -$delta;
1082             }
1083              
1084 1         3 my $hh = int( $delta / 3600 );
1085 1         2 my $mm = int( ( $delta % 3600 ) / 60 );
1086              
1087 1         6 return( sprintf( "%s%02d%02d", $sign, $hh, $mm ) );
1088             }
1089              
1090             sub _validate_date_value
1091             {
1092 0     0   0 my( $self, $v ) = @_;
1093              
1094             # ASCII printable only (SP .. ~), no CR/LF (already handled elsewhere, but we must be explicit)
1095             # if( $v !~ /\A[\x20-\x7E]*\z/ || $v =~ /[\r\n]/ )
1096 0 0       0 if( $v !~ /\A[\x20-\x7E]*\z/ )
1097             {
1098 0         0 return( $self->error( "Invalid Date header (non-ASCII or contains line breaks)." ) );
1099             }
1100              
1101             # Trim outer blanks for matching
1102 0         0 $v =~ s/\A[[:blank:]]+//;
1103 0         0 $v =~ s/[[:blank:]]+\z//;
1104              
1105             # Optional weekday, strict month names, strict numeric TZ
1106 0         0 my $wd = qr/(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)/;
1107 0         0 my $mo = qr/(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/;
1108              
1109 0         0 my $re = qr/
1110             \A
1111             (?:
1112             $wd , [[:blank:]]+
1113             )?
1114             (?:0?[1-9]|[12][0-9]|3[01])
1115             [[:blank:]]+
1116             $mo
1117             [[:blank:]]+
1118             [12][0-9]{3}
1119             [[:blank:]]+
1120             (?:[01][0-9]|2[0-3])
1121             :
1122             [0-5][0-9]
1123             :
1124             [0-5][0-9]
1125             [[:blank:]]+
1126             [+-][0-9]{4}
1127             \z
1128             /x;
1129              
1130 0 0       0 if( $v !~ $re )
1131             {
1132 0         0 return( $self->error( "Invalid Date header syntax." ) );
1133             }
1134 0         0 return(1);
1135             }
1136              
1137             sub _validate_field_name
1138             {
1139 1499     1499   2228 my $self = shift( @_ );
1140 1499   50     3382 my $name = shift( @_ ) ||
1141             return( $self->error( "No field name was provided to check." ) );
1142              
1143 1499 100       7814 unless( $name =~ /^[\x21-\x39\x3B-\x7E]+$/ )
1144             {
1145 2         16 return( $self->error( "Invalid header field name '${name}': must be printable ASCII with no colon or whitespace." ) );
1146             }
1147 1497         3594 return(1);
1148             }
1149              
1150             sub _validate_message_id_value
1151             {
1152 0     0     my( $self, $v ) = @_;
1153              
1154             # ASCII visible only, no spaces, no control chars
1155 0 0         if( $v !~ /\A[\x21-\x7E]+\z/ )
1156             {
1157 0           return( $self->error( "Invalid Message-ID (non-ASCII or contains spaces/control characters)." ) );
1158             }
1159              
1160             # Must be wrapped in angle brackets
1161 0 0         if( $v !~ /\A<([^<>]+)>\z/ )
1162             {
1163 0           return( $self->error( "Invalid Message-ID (missing angle brackets)." ) );
1164             }
1165              
1166 0           my $inner = $1;
1167              
1168             # Exactly one '@'
1169 0 0         if( $inner !~ /\A([^@]+)\@([^@]+)\z/ )
1170             {
1171 0           return( $self->error( "Invalid Message-ID (must contain exactly one '\@')." ) );
1172             }
1173              
1174 0           my $local = $1;
1175 0           my $domain = $2;
1176              
1177             # Local-part: pragmatic (not full RFC)
1178 0 0         if( $local !~ /\A[A-Za-z0-9.!#\$%&'\*\+\/=\?\^_`\{\|\}~\-]+\z/ )
1179             {
1180 0           return( $self->error( "Invalid Message-ID local-part." ) );
1181             }
1182              
1183             # Domain: pragmatic
1184 0 0 0       if( $domain !~ /\A[A-Za-z0-9](?:[A-Za-z0-9\-\.]*[A-Za-z0-9])?\z/ || $domain !~ /\./ )
1185             {
1186 0           return( $self->error( "Invalid Message-ID domain." ) );
1187             }
1188              
1189 0           return(1);
1190             }
1191              
1192             1;
1193             # NOTE: POD
1194             __END__
1195              
1196             =encoding utf-8
1197              
1198             =head1 NAME
1199              
1200             Mail::Make::Headers - Mail Header Collection for Mail::Make
1201              
1202             =head1 SYNOPSIS
1203              
1204             use Mail::Make::Headers;
1205              
1206             my $h = Mail::Make::Headers->new ||
1207             die( Mail::Make::Headers->error );
1208              
1209             $h->set( 'MIME-Version', '1.0' );
1210             $h->content_type( 'text/html; charset=utf-8' );
1211             $h->content_transfer_encoding( 'quoted-printable' );
1212             $h->content_id( 'part1.abc@example.com' );
1213              
1214             print $h->as_string;
1215              
1216             =head1 VERSION
1217              
1218             v0.9.0
1219              
1220             =head1 DESCRIPTION
1221              
1222             An ordered collection of mail header fields for L<Mail::Make::Entity>.
1223              
1224             Provides typed accessors for the headers most relevant to MIME construction (C<Content-Type>, C<Content-Disposition>, C<Content-Transfer-Encoding>, C<Content-ID>), with strict validation on every assignment, plus a generic L</set>/L</get> interface for arbitrary headers.
1225              
1226             Header injection is prevented: field names and values are validated for illegal characters on every L</set> call.
1227              
1228             =head2 Field names
1229              
1230             Field names are treated case-insensitively, and C<_> may be used instead of C<-> (for example C<Content_Type> is treated as C<Content-Type>). Names are canonicalised for storage (e.g. C<Message-ID>, C<MIME-Version>, C<Content-Type>).
1231              
1232             =head2 Values and security
1233              
1234             Values are sanitised to prevent header injection:
1235              
1236             =over 4
1237              
1238             =item *
1239              
1240             CR and LF are replaced with spaces.
1241              
1242             =item *
1243              
1244             ASCII control characters are removed (except tab).
1245              
1246             =back
1247              
1248             This keeps the header container safe even if values originate from external
1249             input.
1250              
1251             =head2 Ordering
1252              
1253             Email headers sometimes have meaningful ordering (e.g. C<Received:> lines).
1254             For this reason, C<as_string_without_sort> preserves insertion order and is recommended for general email usage.
1255              
1256             C<as_string> applies a conservative ordering suitable for display or certain use-cases, but may not be appropriate for all messages.
1257              
1258             =head1 METHODS
1259              
1260             =head2 new
1261              
1262             my $h = Mail::Make::Headers->new;
1263             my $h = Mail::Make::Headers->new( Field => Value, ... );
1264              
1265             Construct a new headers object. Constructor pairs are passed through C<push_header>.
1266              
1267             =head2 as_string( [ $eol ] )
1268              
1269             Returns all headers as a single string, each line terminated by C<$eol> (default CRLF). Does B<not> include the trailing blank line that separates headers from body.
1270              
1271             =head2 as_string_without_sort
1272              
1273             my $s = $h->as_string_without_sort;
1274             my $s = $h->as_string_without_sort( $eol );
1275              
1276             Return formatted header lines preserving insertion order. Recommended for general email usage.
1277              
1278             =head2 clear
1279              
1280             $h->clear;
1281              
1282             Remove all header fields.
1283              
1284             =head2 content_disposition( [ $value ] )
1285              
1286             Convenience typed accessor for the C<Content-Disposition> header. On retrieval returns a L<Mail::Make::Headers::ContentDisposition> object, or C<undef> if not set.
1287              
1288             =head2 content_id( [ $value ] )
1289              
1290             Sets or gets the C<Content-ID> header. Angle brackets are normalised automatically. Validates for control characters.
1291              
1292             =head2 content_transfer_encoding( [ $encoding ] )
1293              
1294             Sets or gets the C<Content-Transfer-Encoding> header. Validates that the value is one of C<7bit>, C<8bit>, C<binary>, C<base64>, or C<quoted-printable>. Value is normalised to lowercase.
1295              
1296             =head2 content_type( [ $value ] )
1297              
1298             Convenience typed accessor for the C<Content-Type> header. On retrieval returns a L<Mail::Make::Headers::ContentType> object, or C<undef> if not set.
1299              
1300             =head2 get
1301              
1302             my @values = $h->get( $field );
1303             my $value = $h->get( $field );
1304              
1305             Alias for:
1306              
1307             $h->header( $field );
1308              
1309             In list context returns all values for the field. In scalar context returns the values joined with C<", ">.
1310              
1311             =head2 has( $name )
1312              
1313             Returns 1 if the named header is present, 0 otherwise.
1314              
1315             =head2 header
1316              
1317             $h->header( $field )
1318             $h->header( $field => $value )
1319             $h->header( $f1 => $v1, $f2 => $v2, ... )
1320              
1321             Get or set header fields.
1322              
1323             In list context, a multi-valued field is returned as a list of values.
1324              
1325             In scalar context, values are returned joined with C<", ">.
1326              
1327             When setting multiple fields, the old value(s) of the last field is returned (C<undef> if the field did not exist).
1328              
1329             The C<$value> may be a string (or something that stringifies) or an arrayref of strings. Values are sanitised as described above.
1330              
1331             =head2 header_field_names
1332              
1333             my @names = $h->header_field_names;
1334             my $names = $h->header_field_names;
1335              
1336             Return the list of distinct field names (canonical spelling). In scalar context returns an arrayref.
1337              
1338             =head2 length
1339              
1340             my $count = $h->length;
1341              
1342             Returns the total number of header field B<entries> currently stored.
1343              
1344             Each value is counted separately, so a multi-valued field (set via repeated calls to L</push_header>) contributes one to the count per value added.
1345              
1346             Returns C<0> when no headers have been set.
1347              
1348             =head2 init_header
1349              
1350             $h->init_header( $field => $value )
1351              
1352             Set the header only if it is not already present.
1353              
1354             =head2 new_field( $name [, $value ] )
1355              
1356             Factory method: returns a new typed header object (L<Mail::Make::Headers::ContentType>, L<Mail::Make::Headers::ContentDisposition>, etc.) or a L<Mail::Make::Headers::Generic> object for unknown field names.
1357              
1358             =head2 print( $fh )
1359              
1360             Writes all headers followed by a blank line to the given filehandle.
1361              
1362             =head2 add( $field => $value )
1363              
1364             $h->add( 'X-Custom' => 'hello' );
1365             $h->add( $f1 => $v1, $f2 => $v2, ... );
1366              
1367             Alias for L</push_header>. Adds value(s) for the specified field(s) without removing any pre-existing values.
1368              
1369             =head2 message_id( [$value | %opts] )
1370              
1371             # Read current Message-ID
1372             my $mid = $h->message_id;
1373              
1374             # Set an explicit value
1375             $h->message_id( '<unique@example.com>' );
1376              
1377             # Generate a new Message-ID automatically
1378             $h->message_id( { generate => 1, domain => 'example.com' } );
1379              
1380             # Remove the Message-ID header
1381             $h->message_id( undef );
1382              
1383             Accessor and generator for the C<Message-ID> header field.
1384              
1385             Called with no arguments, returns the current C<Message-ID> value.
1386              
1387             Called with a plain string, sets the C<Message-ID> to that value after clearing any existing one. If C<< { strict => 1 } >> is passed in the options hash, the value is validated against the RFC 2822 msg-id grammar.
1388              
1389             Called with C<< { generate => 1 } >>, a new unique Message-ID is generated using the supplied C<domain> option (or the system hostname if none is given).
1390             The domain must be a valid FQDN containing at least one dot.
1391              
1392             Called with C<undef>, removes the C<Message-ID> header.
1393              
1394             Returns C<$self> in setter mode, the Message-ID string in getter mode, and C<undef> on error.
1395              
1396             =head2 push_header
1397              
1398             $h->push_header( $field => $value )
1399             $h->push_header( $f1 => $v1, $f2 => $v2, ... )
1400              
1401             Add new value(s) for the specified field(s). Previous values are retained.
1402              
1403             C<$value> may be a scalar or an arrayref.
1404              
1405             =head2 remove( $field, ... )
1406              
1407             $h->remove( 'X-Custom' );
1408             $h->remove( 'Cc', 'Bcc' );
1409              
1410             Alias for L</remove_header>.
1411              
1412             =head2 remove_header( $field, ... )
1413              
1414             $h->remove_header( $field, ... )
1415              
1416             Remove the specified fields and return the removed values.
1417              
1418             In list context, returns the values removed.
1419              
1420             In scalar context, returns the last removed value or C<0> if nothing was removed.
1421              
1422             =head2 replace( $field => $value )
1423              
1424             $h->replace( 'Subject' => 'New subject' );
1425              
1426             Alias for L</replace_header>.
1427              
1428             =head2 replace_header
1429              
1430             $h->replace_header( $field => $value )
1431             $h->replace_header( $f1 => $v1, $f2 => $v2, ... )
1432              
1433             Replace the value(s) of one or more header fields.
1434              
1435             All existing occurrences of the specified field are removed before the new value(s) are added.
1436              
1437             If C<$value> is C<undef>, the field is removed.
1438              
1439             The old value(s) of the last field processed are returned. In list context, all previous values are returned. In scalar context, values are returned joined with C<", ">.
1440              
1441             This method is similar to C<header()> in setter mode, but explicitly treats C<undef> as a request to delete the field.
1442              
1443             =head2 reset( [$flag] )
1444              
1445             $h->reset(1);
1446              
1447             Internal cache-invalidation method. When called with a true value, signals that the serialised header string cached internally is stale and must be regenerated on the next call to L</as_string>.
1448              
1449             User code rarely needs to call this directly; it is invoked automatically by any method that modifies the header set (L</add>, L</remove_header>, L</replace_header>, L</message_id>, etc.).
1450              
1451             =head2 scan
1452              
1453             $h->scan( sub { my( $k, $v ) = @_; ... } );
1454              
1455             Call the callback for each stored header field/value pair (one call per value).
1456              
1457             =head2 set
1458              
1459             $h->set( $field => $value );
1460             $h->set( $f1 => $v1, $f2 => $v2, ... );
1461              
1462             Alias for C<replace_header()>.
1463              
1464             If C<$value> is C<undef>, the field is removed.
1465              
1466             =head2 exists
1467              
1468             if( $h->exists( $field ) ) { ... }
1469              
1470             Return true if at least one value is present for the given field.
1471              
1472             Field names are case-insensitive and C<_> is treated as C<->.
1473              
1474             =head2 set( $name, $value )
1475              
1476             Sets (replaces or appends) the named header. C<$value> may be a plain string or any object that stringifies.
1477              
1478             Validates the field name (printable ASCII, no colon or whitespace) and the value (no bare CR/LF header injection).
1479              
1480             =head1 AUTHOR
1481              
1482             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
1483              
1484             =head1 SEE ALSO
1485              
1486             L<Mail::Make>, L<Mail::Make::Entity>, L<Mail::Make::Headers::Generic>, L<Mail::Make::Headers::ContentType>, L<Mail::Make::Headers::ContentDisposition>
1487              
1488             =head1 COPYRIGHT & LICENSE
1489              
1490             Copyright(c) 2026 DEGUEST Pte. Ltd.
1491              
1492             All rights reserved.
1493              
1494             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
1495              
1496             =cut