File Coverage

lib/Mail/Make/Headers/Generic.pm
Criterion Covered Total %
statement 85 115 73.9
branch 16 38 42.1
condition 11 25 44.0
subroutine 23 31 74.1
pod 6 8 75.0
total 141 217 64.9


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## MIME Email Builder - ~/lib/Mail/Make/Headers/Generic.pm
3             ## Version v0.1.0
4             ## Copyright(c) 2026 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2026/03/02
7             ## Modified 2026/03/02
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::Generic;
14             BEGIN
15             {
16 11     11   5622 use strict;
  11         24  
  11         439  
17 11     11   51 use warnings;
  11         18  
  11         734  
18 11     11   1479 warnings::register_categories( 'Mail::Make' );
19 11     11   78 use parent qw( Module::Generic );
  11         19  
  11         63  
20 11     11   483802 use vars qw( $VERSION $EXCEPTION_CLASS );
  11         75  
  11         694  
21 11     11   58 use Encode ();
  11         42  
  11         252  
22 11     11   912 use Mail::Make::Exception;
  11         24  
  11         87  
23 11     11   8566 use URI::Escape::XS ();
  11         31508  
  11         758  
24             use overload (
25             '""' => 'as_string',
26 946     946   16980 'bool' => sub{1},
27 11     11   90 );
  11         17  
  11         192  
28 11         53 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
29 11         308 our $VERSION = 'v0.1.0';
30             };
31              
32 11     11   60 use strict;
  11         40  
  11         231  
33 11     11   42 use warnings;
  11         24  
  11         15690  
34              
35             sub init
36             {
37 167     167 1 538 my $self = shift( @_ );
38 167         779 $self->{_exception_class} = $EXCEPTION_CLASS;
39 167 50       860 $self->SUPER::init( @_ ) || return( $self->pass_error );
40 167         570 return( $self );
41             }
42              
43 0     0 1 0 sub as_string { return( shift->_hv_as_string( @_ ) ); }
44              
45 2     2 1 68 sub field_name { return( shift->_set_get_scalar( '_field_name', @_ ) ); }
46              
47 112     112 1 545 sub param { return( shift->_set_get_param( @_ ) ); }
48              
49 0     0 1 0 sub params { return( shift->_set_get_params( @_ ) ); }
50              
51 0     0 1 0 sub value { return( shift->_set_get_scalar( '_value', @_ ) ); }
52              
53 167     167   1053 sub _field_name { return( shift->_set_get_scalar( '_field_name', @_ ) ); }
54              
55             # rfc2231 filename decoding
56             # Accepts UTF-8'lang'encoded or plain value
57             # Returns decoded string
58             sub _filename_decode
59             {
60 0     0   0 my $self = shift( @_ );
61 0         0 my $fname = shift( @_ );
62 0 0 0     0 return( $fname ) if( !defined( $fname ) || !length( $fname ) );
63             # rfc2231 extended notation: charset'language'encoded
64 0 0       0 if( $fname =~ /^([A-Za-z0-9\-]+)'([^']*)'(.+)$/ )
65             {
66 0         0 my( $charset, $lang, $encoded ) = ( $1, $2, $3 );
67 0         0 my $decoded = URI::Escape::XS::uri_unescape( $encoded );
68             # try-catch
69 0         0 local $@;
70             eval
71 0         0 {
72 0         0 $decoded = Encode::decode( $charset, $decoded );
73             };
74 0 0       0 if( $@ )
75             {
76 0         0 return( $fname );
77             }
78 0 0       0 return( wantarray() ? ( $decoded, $charset, $lang ) : $decoded );
79             }
80 0 0       0 return( wantarray() ? ( $fname, undef, undef ) : $fname );
81             }
82              
83             # rfc2231 / rfc5987 filename encoding
84             # Returns encoded string such as UTF-8''Yamato%2CInc-Logo.png
85             # Returns undef if no encoding needed (pure ASCII, no special chars)
86             sub _filename_encode
87             {
88 32     32   93 my $self = shift( @_ );
89 32         77 my $fname = shift( @_ );
90 32         118 my $lang = shift( @_ );
91             # Contains non-ASCII or RFC 2045 special chars that require quoting/encoding
92 32 100 100     593 if( $fname =~ /[^\x20-\x7E]/ || $fname =~ /[()<>@,;:\\"\/\[\]?=]/ )
93             {
94 7 100       53 $lang = '' if( !defined( $lang ) );
95 7         103 my $encoded = URI::Escape::XS::uri_escape( Encode::encode( 'UTF-8', $fname ) );
96 7         539 return( sprintf( "UTF-8'%s'%s", $lang, $encoded ) );
97             }
98             # Pure safe ASCII - caller should use simple quoting if needed
99 25         129 return( undef );
100             }
101              
102 527     527   3228 sub _hv { return( shift->_set_get_object_without_init( '_hv', 'Module::Generic::HeaderValue', @_ ) ); }
103              
104             sub _hv_as_string
105             {
106 141     141   448 my $self = shift( @_ );
107 141         476 my $hv = $self->_hv;
108 141 50       13561 return( '' ) if( !$hv );
109 141         1679 return( $hv->as_string( @_ ) );
110             }
111              
112             sub _new_hv
113             {
114 1     1   3 my $self = shift( @_ );
115 1 50       7 $self->_load_class( 'Module::Generic::HeaderValue' ) || return( $self->pass_error );
116 1         896 return( Module::Generic::HeaderValue->new( @_ ) );
117             }
118              
119             sub _parse_header_value
120             {
121 165     165   483 my $self = shift( @_ );
122 165         359 my $this = shift( @_ );
123 165 50 33     1462 return( $self->error( "No header value was provided to parse." ) )
124             if( !defined( $this ) || !length( "$this" ) );
125 165 50       852 $self->_load_class( 'Module::Generic::HeaderValue' ) || return( $self->pass_error );
126 165   50     193291 my $hv = Module::Generic::HeaderValue->new_from_header( $this, @_ ) ||
127             return( $self->pass_error( Module::Generic::HeaderValue->error ) );
128 165         9755875 return( $hv );
129             }
130              
131             sub _set_get_param
132             {
133 136     136   321 my $self = shift( @_ );
134 136   50     773 my $name = shift( @_ ) || return( $self->error( "No parameter name was provided." ) );
135 136         385 my $hv = $self->_hv;
136 136 50 66     12232 return( '' ) if( !scalar( @_ ) && !$hv );
137 136 50       615 return( $self->error( "Header value object (Module::Generic::HeaderValue) could not be found!" ) ) if( !$hv );
138 136 100       1209 if( @_ )
139             {
140 128         754 $hv->param( $name => shift( @_ ) );
141             }
142 136         1402076 return( $hv->param( $name ) );
143             }
144              
145             sub _set_get_params
146             {
147 0     0   0 my $self = shift( @_ );
148 0   0     0 my $hv = $self->_hv || return( $self->error( "Header value object could not be found!" ) );
149 0         0 my $params = $hv->params;
150 0 0       0 if( @_ )
151             {
152 0         0 while( my( $n, $v ) = splice( @_, 0, 2 ) )
153             {
154 0         0 $params->set( $n => $v );
155             }
156             }
157             else
158             {
159 0         0 return( $params );
160             }
161             }
162              
163             # Same as _set_get_param but wraps value in double quotes on store,
164             # strips them on retrieve
165             sub _set_get_qparam
166             {
167 26     26   59 my $self = shift( @_ );
168 26   50     168 my $name = shift( @_ ) || return( $self->error( "No parameter name was provided." ) );
169 26   50     79 my $hv = $self->_hv || return( $self->error( "Header value object could not be found!" ) );
170 26         2194 my $v;
171 26 100       79 if( @_ )
172             {
173 25         52 $v = shift( @_ );
174 25         129 $v =~ s/^\"//;
175 25         94 $v =~ s/(?<!\\)\"$//;
176 25         149 $hv->param( $name => qq{"${v}"} );
177             }
178             else
179             {
180 1         3 $v = $hv->param( $name );
181 1 50 33     475 return( '' ) if( !defined( $v ) || !length( "$v" ) );
182 1         4 $v =~ s/^\"//;
183 1         5 $v =~ s/(?<!\\)\"$//;
184             }
185 26         231369 return( $v );
186             }
187              
188             sub _set_get_value
189             {
190 0     0     my $self = shift( @_ );
191 0           my $hv = $self->_hv;
192 0 0         if( @_ )
193             {
194 0           $hv->value( shift( @_ ) );
195             }
196 0           return( $hv->value_data );
197             }
198              
199             # NOTE: STORABLE support
200 0     0 0   sub STORABLE_freeze { CORE::return( CORE::shift->FREEZE( @_ ) ); }
201              
202 0     0 0   sub STORABLE_thaw { CORE::return( CORE::shift->THAW( @_ ) ); }
203              
204             1;
205             # NOTE: POD
206             __END__
207              
208             =encoding utf-8
209              
210             =head1 NAME
211              
212             Mail::Make::Headers::Generic - Base Class for Mail::Make Header Objects
213              
214             =head1 SYNOPSIS
215              
216             package Mail::Make::Headers::MyHeader;
217             use parent qw( Mail::Make::Headers::Generic );
218              
219             =head1 VERSION
220              
221             v0.1.0
222              
223             =head1 DESCRIPTION
224              
225             This is the base class for all typed header objects in L<Mail::Make>. It provides shared infrastructure for header value parsing, parameter handling, RFC 2231 filename encoding/decoding, and stringification.
226              
227             =head1 METHODS
228              
229             =head2 as_string
230              
231             Returns the header field value as a string, suitable for inclusion in a mail message.
232              
233             =head2 field_name
234              
235             Sets or gets the header field name (e.g. C<Content-Type>).
236              
237             =head2 param( $name [, $value ] )
238              
239             Sets or gets a named parameter on the header value. For example the C<charset> parameter of a C<Content-Type> field.
240              
241             =head2 params( $name => $value, ... )
242              
243             Sets multiple parameters at once. With no arguments, returns the parameter hash object.
244              
245             =head2 value( [ $value ] )
246              
247             Sets or gets the main header value (the part before the first C<;>).
248              
249             =head1 PRIVATE METHODS
250              
251             =head2 _filename_encode( $filename [, $language ] )
252              
253             Encodes C<$filename> using RFC 2231 / RFC 5987 if it contains non-ASCII characters or RFC 2045 special characters (such as commas). Returns the encoded string C<UTF-8'lang'percent-encoded>, or C<undef> if no encoding was necessary.
254              
255             =head2 _filename_decode( $value )
256              
257             Decodes an RFC 2231 encoded filename value. In list context returns C<( $decoded, $charset, $language )>. In scalar context returns just the decoded filename. If decoding is not required the original value is returned unchanged.
258              
259             =head2 _hv
260              
261             Sets or gets the underlying L<Module::Generic::HeaderValue> object.
262              
263             =head2 _hv_as_string
264              
265             Returns the stringified form of the L<Module::Generic::HeaderValue> object, or an empty string if none is set.
266              
267             =head2 _new_hv( $string )
268              
269             Instantiates a new L<Module::Generic::HeaderValue> from C<$string>.
270              
271             =head2 _parse_header_value( $string )
272              
273             Parses C<$string> as a structured header value using L<Module::Generic::HeaderValue/new_from_header> and returns the resulting object.
274              
275             =head2 _set_get_param( $name [, $value ] )
276              
277             Low-level parameter accessor, delegates to the internal L<Module::Generic::HeaderValue> object.
278              
279             =head2 _set_get_params( $name => $value, ... )
280              
281             Low-level multi-parameter accessor.
282              
283             =head2 _set_get_qparam( $name [, $value ] )
284              
285             Like L</_set_get_param> but stores the value surrounded by double quotes and strips them on retrieval. Used for C<filename=> parameters.
286              
287             =head2 _set_get_value( [ $value ] )
288              
289             Low-level main-value accessor.
290              
291             =head1 AUTHOR
292              
293             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
294              
295             =head1 SEE ALSO
296              
297             L<Mail::Make>, L<Mail::Make::Headers>, L<Mail::Make::Headers::ContentType>, L<Mail::Make::Headers::ContentDisposition>
298              
299             =head1 COPYRIGHT & LICENSE
300              
301             Copyright(c) 2026 DEGUEST Pte. Ltd.
302              
303             All rights reserved.
304              
305             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
306              
307             =cut