File Coverage

lib/Mail/Make/Headers/ContentType.pm
Criterion Covered Total %
statement 69 72 95.8
branch 22 28 78.5
condition 17 26 65.3
subroutine 14 16 87.5
pod 8 8 100.0
total 130 150 86.6


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## MIME Email Builder - ~/lib/Mail/Make/Headers/ContentType.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::ContentType;
14             BEGIN
15             {
16 10     10   128930 use strict;
  10         20  
  10         411  
17 10     10   45 use warnings;
  10         23  
  10         647  
18 10     10   4273 warnings::register_categories( 'Mail::Make' );
19 10     10   533 use parent qw( Mail::Make::Headers::Generic );
  10         404  
  10         66  
20 10     10   667 use vars qw( $VERSION $EXCEPTION_CLASS $VALID_CHARSETS );
  10         18  
  10         617  
21 10     10   63 use Mail::Make::Exception;
  10         32  
  10         67  
22 10         18 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
23             # Common charsets accepted as valid; not exhaustive but covers typical usage
24 10         47 our $VALID_CHARSETS = qr/^(?:
25             us-ascii | ascii |
26             utf-8 | utf8 |
27             iso-8859-[\d]+ |
28             windows-\d+ |
29             shift[_-]jis |
30             euc-jp |
31             iso-2022-jp
32             )$/xi;
33 10         263 our $VERSION = 'v0.1.0';
34             };
35              
36 10     10   59 use strict;
  10         13  
  10         203  
37 10     10   36 use warnings;
  10         16  
  10         9347  
38              
39             sub init
40             {
41 127     127 1 899325 my $self = shift( @_ );
42 127         1231 $self->{_exception_class} = $EXCEPTION_CLASS;
43 127 50 66     1738 @_ = () if( @_ == 1 && $self->_is_a( $_[0] => 'Module::Generic::Null' ) );
44 127 100       4099 if( @_ )
45             {
46 125         436 my $str = shift( @_ );
47 125 50 33     1841 if( !defined( $str ) || !length( "$str" ) )
48             {
49 0         0 return( $self->error( "No value was provided for Content-Type field." ) );
50             }
51 125         657 my $params = $self->_get_args_as_hash( @_ );
52 125   50     2019 my $hv = $self->_parse_header_value( $str ) || return( $self->pass_error );
53 125         1399 $hv->param( $_ => $params->{ $_ } ) for( keys( %$params ) );
54 125         1286 $self->_hv( $hv );
55             }
56 127 50       54909 $self->SUPER::init( @_ ) || return( $self->pass_error );
57 127         998 $self->_field_name( 'Content-Type' );
58 127         122897 return( $self );
59             }
60              
61 108     108 1 89717 sub as_string { return( shift->_hv_as_string( @_ ) ); }
62              
63             sub boundary
64             {
65 8     8 1 2352 my $self = shift( @_ );
66 8 100       26 if( @_ )
67             {
68 6         14 my $b = shift( @_ );
69 6 50 33     83 if( defined( $b ) && length( $b ) )
70             {
71             # RFC 2046 boundary: 1-70 chars from a limited set, no trailing space
72 6 100 100     71 return( $self->error(
      100        
73             "Invalid boundary '$b': only 0-9 a-z A-Z and '()+_,-./:=? ' are allowed, max 70 chars, no trailing space"
74             ) ) if( length( $b ) > 70 || $b =~ m{[^0-9a-zA-Z'()+_,\-./:=? ]} || $b =~ / $/ );
75             }
76 3         17 return( $self->_set_get_param( boundary => $b ) );
77             }
78 2         5 return( $self->_set_get_param( 'boundary' ) );
79             }
80              
81             sub charset
82             {
83 10     10 1 6334 my $self = shift( @_ );
84 10 100       34 if( @_ )
85             {
86 6         12 my $cs = shift( @_ );
87 6 50 33     41 if( defined( $cs ) && length( $cs ) )
88             {
89 6 100       74 unless( $cs =~ $VALID_CHARSETS )
90             {
91 1         10 return( $self->error( "Unknown or unsupported charset '$cs'." ) );
92             }
93             # Normalise to lowercase
94 5         16 $cs = lc( $cs );
95             # Normalise utf8 -> utf-8
96 5 100       14 $cs = 'utf-8' if( $cs eq 'utf8' );
97             }
98 5         25 return( $self->_set_get_param( charset => $cs ) );
99             }
100 4         14 return( $self->_set_get_param( 'charset' ) );
101             }
102              
103             # Returns a fresh RFC 2046 compliant boundary using Data::UUID
104             sub make_boundary
105             {
106 1     1 1 10 my $self = shift( @_ );
107 1 50       3 $self->_load_class( 'Data::UUID' ) || return( $self->pass_error );
108 1         3198 return( Data::UUID->new->create_str );
109             }
110              
111 0     0 1 0 sub name { return( shift->_set_get_param( name => @_ ) ); }
112              
113             sub type
114             {
115 8     8 1 50321 my $self = shift( @_ );
116 8 100       33 if( @_ )
117             {
118 3   100     71 my $mime = shift( @_ ) || return( $self->error( "No MIME type was provided." ) );
119             # Basic structural validation: type/subtype
120 2 100       17 unless( $mime =~ m{^[A-Za-z0-9][A-Za-z0-9!\#\$&\-^_]*
121             /[A-Za-z0-9][A-Za-z0-9!\#\$&\-^_.+]*$}x )
122             {
123 1         14 return( $self->error( "Invalid MIME type '$mime': expected 'type/subtype' format." ) );
124             }
125 1   50     13 my $hv = $self->_new_hv( $mime ) || return( $self->pass_error );
126 1         321 $self->_hv( $hv );
127 1         1360 return( $mime );
128             }
129             else
130             {
131 5   100     20 my $hv = $self->_hv || return( '' );
132 4         424 return( $hv->value_data );
133             }
134             }
135              
136             # Alias
137 0     0 1   sub value { return( shift->type( @_ ) ); }
138              
139             1;
140             # NOTE: POD
141             __END__
142              
143             =encoding utf-8
144              
145             =head1 NAME
146              
147             Mail::Make::Headers::ContentType - Content-Type Header Field Object
148              
149             =head1 SYNOPSIS
150              
151             use Mail::Make::Headers::ContentType;
152              
153             my $ct = Mail::Make::Headers::ContentType->new( 'text/plain' ) ||
154             die( Mail::Make::Headers::ContentType->error );
155             $ct->charset( 'utf-8' );
156             print "$ct";
157             # text/plain; charset=utf-8
158              
159             my $ct = Mail::Make::Headers::ContentType->new( 'multipart/mixed' ) ||
160             die( Mail::Make::Headers::ContentType->error );
161             $ct->boundary( $ct->make_boundary );
162             print "$ct";
163             # multipart/mixed; boundary=550E8400-E29B-41D4-A716-446655440000
164              
165             =head1 VERSION
166              
167             v0.1.0
168              
169             =head1 DESCRIPTION
170              
171             Typed object for the C<Content-Type> mail header field. Provides strict validation of the MIME type, charset, and boundary parameter, refusing silently corrupt values that would produce a broken message.
172              
173             =head1 METHODS
174              
175             =head2 new( $mime_type [, %params ] )
176              
177             Instantiates a new object. C<$mime_type> must be in C<type/subtype> format.
178             Optional C<%params> are set as additional parameters on the header value.
179              
180             =head2 as_string
181              
182             Returns the complete header field value as a string, including all parameters.
183              
184             =head2 boundary( [ $boundary ] )
185              
186             Sets or gets the C<boundary> parameter. On setting, validates that the value conforms to RFC 2046: up to 70 characters from the set C<[0-9A-Za-z'()+_,-./:=? ]>, with no trailing space.
187              
188             Returns an error if the boundary is invalid.
189              
190             =head2 charset( [ $charset ] )
191              
192             Sets or gets the C<charset> parameter. On setting, validates the value against a list of known charsets and normalises C<utf8> to C<utf-8>.
193              
194             =head2 make_boundary
195              
196             Returns a freshly generated, RFC 2046 compliant boundary string based on a UUID.
197              
198             =head2 name( [ $name ] )
199              
200             Sets or gets the C<name> parameter (used for inline parts).
201              
202             =head2 type( [ $mime_type ] )
203              
204             Sets or gets the MIME type (e.g. C<text/html>). Validates the C<type/subtype> format on assignment.
205              
206             =head2 value( [ $mime_type ] )
207              
208             Alias for L</type>.
209              
210             =head1 AUTHOR
211              
212             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
213              
214             =head1 SEE ALSO
215              
216             L<Mail::Make>, L<Mail::Make::Headers>, L<Mail::Make::Headers::Generic>, L<Mail::Make::Headers::ContentDisposition>
217              
218             RFC 2045, RFC 2046
219              
220             =head1 COPYRIGHT & LICENSE
221              
222             Copyright(c) 2026 DEGUEST Pte. Ltd.
223              
224             All rights reserved.
225              
226             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
227              
228             =cut