File Coverage

lib/Mail/Make/Headers/ContentDisposition.pm
Criterion Covered Total %
statement 84 89 94.3
branch 26 38 68.4
condition 11 21 52.3
subroutine 15 15 100.0
pod 7 7 100.0
total 143 170 84.1


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## MIME Email Builder - ~/lib/Mail/Make/Headers/ContentDisposition.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::ContentDisposition;
14             BEGIN
15             {
16 10     10   132024 use strict;
  10         17  
  10         358  
17 10     10   41 use warnings;
  10         22  
  10         613  
18 10     10   3631 warnings::register_categories( 'Mail::Make' );
19 10     10   683 use parent qw( Mail::Make::Headers::Generic );
  10         405  
  10         54  
20 10     10   731 use vars qw( $VERSION $EXCEPTION_CLASS $VALID_DISPOSITIONS );
  10         18  
  10         680  
21 10     10   69 use Mail::Make::Exception;
  10         21  
  10         66  
22 10         19 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
23 10         56 our $VALID_DISPOSITIONS = qr/^(?:inline|attachment|form-data)$/i;
24 10         234 our $VERSION = 'v0.1.0';
25             };
26              
27 10     10   69 use strict;
  10         17  
  10         213  
28 10     10   35 use warnings;
  10         21  
  10         8809  
29              
30             sub init
31             {
32 42     42 1 2576633 my $self = shift( @_ );
33 42         645 $self->{filename_charset} = undef;
34 42         525 $self->{filename_lang} = undef;
35 42         252 $self->{_exception_class} = $EXCEPTION_CLASS;
36 42         140 my $disposition = shift( @_ );
37 42 100 66     505 return( $self->error( "No value was provided for Content-Disposition field." ) )
38             if( !defined( $disposition ) || !length( "$disposition" ) );
39 41         295 my $params = $self->_get_args_as_hash( @_ );
40 41         313 my $debug = delete( $params->{debug} );
41 41         278 $self->debug( $debug );
42 41 50       1854 $self->disposition( $disposition, ( scalar( %$params ) ? %$params : () ) ) || return( $self->pass_error );
    100          
43 40 50       480 $self->SUPER::init( @_ ) || return( $self->pass_error );
44 40         276 $self->_field_name( 'Content-Disposition' );
45 40         36992 return( $self );
46             }
47              
48 33     33 1 2518 sub as_string { return( shift->_hv_as_string( [qw( filename )] ) ); }
49              
50             sub disposition
51             {
52 54     54 1 179865 my $self = shift( @_ );
53 54 100       177 if( @_ )
54             {
55 47   50     203 my $dispo = shift( @_ ) || return( $self->error( "No content disposition was provided." ) );
56 47 100       838 unless( $dispo =~ $VALID_DISPOSITIONS )
57             {
58 2         34 return( $self->error( "Invalid disposition '$dispo': must be one of inline, attachment, form-data." ) );
59             }
60 45         208 my $params = $self->_get_args_as_hash( @_ );
61 45         356 $dispo = lc( $dispo );
62 45         270 my $hv = $self->_hv;
63 45 100       4173 if( $hv )
64             {
65 5         53 $hv->value( $dispo );
66             }
67             else
68             {
69 40   50     311 $hv = $self->_parse_header_value( $dispo ) || return( $self->pass_error );
70 40         478 $self->_hv( $hv );
71             }
72 45 50       30462 $hv->params( $params ) if( scalar( keys( %$params ) ) );
73 45         289 return( $dispo );
74             }
75             else
76             {
77 7   50     37 my $hv = $self->_hv || return( '' );
78 7         805 return( $hv->value_data );
79             }
80             }
81              
82             # Sets or gets the filename parameter.
83             # On assignment, applies RFC 2231 encoding automatically when the filename
84             # contains non-ASCII or RFC 2045 special characters (including commas).
85             # This is the fix for the MIME::Entity silent failure bug.
86             sub filename
87             {
88 34     34 1 295613 my $self = shift( @_ );
89 34 100       138 if( @_ )
90             {
91 33         130 my( $fname, $lang ) = @_;
92 33 100       147 if( !defined( $fname ) )
93             {
94             # Remove both the plain and extended forms
95 1         4 my $hv = $self->_hv;
96 1 50       89 if( $hv )
97             {
98 1         19 $hv->params->delete( 'filename' );
99 1         897 $hv->params->delete( 'filename*' );
100             }
101 1         890 return( $self );
102             }
103 32   66     389 $lang //= $self->filename_lang;
104 32         51571 my $encoded = $self->_filename_encode( $fname, $lang );
105 32 100       132 if( defined( $encoded ) )
106             {
107             # Non-trivial filename: use RFC 2231 extended notation (filename*)
108 7 50       68 $self->_set_get_param( 'filename*' => $encoded ) || return( $self->pass_error );
109             }
110             else
111             {
112             # Pure safe ASCII: use plain quoted filename
113 25         231 $self->_set_get_qparam( filename => $fname );
114             }
115 32         5935 return( $fname );
116             }
117             else
118             {
119             # Prefer filename* (RFC 2231) over plain filename
120 1         11 my $v = $self->_set_get_param( 'filename*' );
121 1 50 33     584 if( defined( $v ) && length( "$v" ) )
122             {
123 0         0 my( $decoded, $charset, $lang ) = $self->_filename_decode( $v );
124 0 0       0 $self->filename_charset( $charset ) if( defined( $charset ) );
125 0 0       0 $self->filename_lang( $lang ) if( defined( $lang ) );
126 0         0 return( $decoded );
127             }
128 1         6 $v = $self->_set_get_qparam( 'filename' );
129 1         5 return( $v );
130             }
131             }
132              
133             sub filename_charset
134             {
135 2     2 1 4304 my $self = shift( @_ );
136 2 50       12 if( @_ )
137             {
138 2         5 my $v = shift( @_ );
139 2 50 33     55 if( defined( $v ) && length( $v ) )
140             {
141 2 100 66     17 if( lc( $v ) ne 'utf-8' && lc( $v ) ne 'utf8' )
142             {
143 1         7 return( $self->error( "Only 'utf-8' is supported as filename charset." ) );
144             }
145 1         3 $v = 'UTF-8';
146             }
147 1         7 return( $self->_set_get_scalar_as_object( 'filename_charset', $v ) );
148             }
149 0         0 return( $self->_set_get_scalar_as_object( 'filename_charset' ) );
150             }
151              
152 33     33 1 271 sub filename_lang { return( shift->_set_get_scalar_as_object( 'filename_lang', @_ ) ); }
153              
154 2     2 1 1032 sub name { return( shift->_set_get_param( name => @_ ) ); }
155              
156             1;
157             # NOTE: POD
158             __END__
159              
160             =encoding utf-8
161              
162             =head1 NAME
163              
164             Mail::Make::Headers::ContentDisposition - Content-Disposition Header Field Object
165              
166             =head1 SYNOPSIS
167              
168             use Mail::Make::Headers::ContentDisposition;
169              
170             my $cd = Mail::Make::Headers::ContentDisposition->new( 'inline' ) ||
171             die( Mail::Make::Headers::ContentDisposition->error );
172             $cd->filename( 'Yamato,Inc-Logo.png' );
173             print "$cd";
174             # inline; filename*=UTF-8''Yamato%2CInc-Logo.png
175              
176             # Pure ASCII filename - plain quoting used instead
177             $cd->filename( 'logo.png' );
178             print "$cd";
179             # inline; filename="logo.png"
180              
181             # With language hint for RFC 2231
182             $cd->filename( 'ファイル.txt', 'ja-JP' );
183             print "$cd";
184             # inline; filename*=UTF-8'ja-JP'%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.txt
185              
186             =head1 VERSION
187              
188             v0.1.0
189              
190             =head1 DESCRIPTION
191              
192             Typed object for the C<Content-Disposition> mail header field.
193              
194             The key improvement over L<MIME::Entity> / L<Mail::Field> is the L</filename> method: when the filename contains special RFC 2045 characters (including commas) or non-ASCII characters, it automatically encodes the value using RFC 2231 extended notation (C<filename*>), instead of silently producing a malformed header that corrupts the entire message.
195              
196             =head1 METHODS
197              
198             =head2 new( $disposition [, %params ] )
199              
200             Instantiates a new object. C<$disposition> must be one of C<inline>, C<attachment>, or C<form-data>.
201              
202             =head2 as_string
203              
204             Returns the complete header field value as a string, including all parameters.
205              
206             =head2 disposition( [ $disposition ] )
207              
208             Sets or gets the disposition type. Validates that the value is one of C<inline>, C<attachment>, or C<form-data>. Returns an error otherwise.
209              
210             =head2 filename( [ $filename [, $language ] ] )
211              
212             Sets or gets the filename parameter. This method is RFC 2231 aware:
213              
214             =over 4
215              
216             =item *
217              
218             If C<$filename> contains non-ASCII characters or RFC 2045 special characters (such as C<,>, C<(>, C<)>, C<@>, etc.), it encodes the value as C<filename*=UTF-8'language'percent-encoded> and sets C<filename*> instead of C<filename>.
219              
220             =item *
221              
222             Otherwise, the filename is stored as a plain quoted C<filename=> parameter.
223              
224             =item *
225              
226             On retrieval, C<filename*> takes precedence over C<filename>. The value is decoded and returned as a plain Perl string.
227              
228             =back
229              
230             Setting to C<undef> removes both C<filename> and C<filename*>.
231              
232             =head2 filename_charset
233              
234             Returns the charset used during the last RFC 2231 decode operation. Set automatically by L</filename>. Read-only in normal usage; only C<utf-8> is accepted if set explicitly.
235              
236             =head2 filename_lang
237              
238             Sets or gets the default language tag (RFC 1766 / ISO 639) used when encoding filenames with RFC 2231. For example C<ja-JP>.
239              
240             =head2 name( [ $name ] )
241              
242             Sets or gets the C<name=> parameter, used for C<form-data> dispositions.
243              
244             =head2 param( $name [, $value ] )
245              
246             Low-level access to an arbitrary parameter.
247              
248             =head2 params( $name => $value, ... )
249              
250             Sets multiple parameters at once. With no arguments, returns the parameter hash object.
251              
252             =head1 AUTHOR
253              
254             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
255              
256             =head1 SEE ALSO
257              
258             L<Mail::Make>, L<Mail::Make::Headers>, L<Mail::Make::Headers::Generic>, L<Mail::Make::Headers::ContentType>
259              
260             RFC 2183, RFC 2231, RFC 5987
261              
262             =head1 COPYRIGHT & LICENSE
263              
264             Copyright(c) 2026 DEGUEST Pte. Ltd.
265              
266             All rights reserved.
267              
268             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
269              
270             =cut