File Coverage

blib/lib/Courriel/HeaderAttribute.pm
Criterion Covered Total %
statement 75 75 100.0
branch 18 18 100.0
condition 7 8 87.5
subroutine 14 14 100.0
pod n/a
total 114 115 99.1


line stmt bran cond sub pod time code
1             package Courriel::HeaderAttribute;
2              
3 10     10   12780 use strict;
  10         12  
  10         247  
4 10     10   29 use warnings;
  10         11  
  10         197  
5 10     10   415 use namespace::autoclean;
  10         13934  
  10         54  
6              
7             our $VERSION = '0.42';
8              
9 10     10   632 use Courriel::HeaderAttribute;
  10         12  
  10         251  
10 10     10   373 use Courriel::Helpers qw( quote_and_escape_attribute_value );
  10         12  
  10         466  
11 10     10   2173 use Courriel::Types qw( Maybe NonEmptyStr Str );
  10         17  
  10         67  
12 10     10   53799 use Encode qw( encode );
  10         15  
  10         474  
13              
14 10     10   57 use Moose;
  10         13  
  10         62  
15 10     10   45939 use MooseX::StrictConstructor;
  10         98320  
  10         46  
16              
17             with 'Courriel::Role::Streams';
18              
19             has name => (
20             is => 'ro',
21             isa => NonEmptyStr,
22             required => 1,
23             );
24              
25             has value => (
26             is => 'ro',
27             isa => Str,
28             required => 1,
29             );
30              
31             has charset => (
32             is => 'ro',
33             isa => NonEmptyStr,
34             default => 'us-ascii',
35             );
36              
37             has language => (
38             is => 'ro',
39             isa => Maybe [NonEmptyStr],
40             default => undef,
41             );
42              
43             override BUILDARGS => sub {
44             my $class = shift;
45              
46             my $p = super();
47              
48             return $p unless defined $p->{value};
49              
50             $p->{charset} = 'UTF-8' if $p->{value} =~ /[^\p{ASCII}]/;
51              
52             return $p;
53             };
54              
55             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
56             sub _stream_to {
57 10     10   14 my $self = shift;
58 10         11 my $output = shift;
59              
60 10         18 $output->( $self->_as_string );
61             }
62             ## use critic
63              
64             {
65             my $non_attribute_char = qr{
66             $Courriel::Helpers::TSPECIALS
67             |
68             [ \*\%] # space, *, %
69             |
70             [^\p{ASCII}] # anything that's not ascii
71             |
72             [\x00-\x1f\x7f] # ctrl chars
73             }x;
74              
75             sub _as_string {
76 10     10   8 my $self = shift;
77              
78 10         241 my $value = $self->value;
79              
80 10         12 my $transport_method = '_simple_parameter';
81              
82 10 100 100     227 if ( $value =~ /[\x00-\x1f]|\x7f|[^\p{ASCII}]/
    100 66        
83             || defined $self->language
84             || $self->charset ne 'us-ascii' ) {
85              
86 4         13 $value = encode( 'utf-8', $value );
87 4         287 $value
88 110         312 =~ s/($non_attribute_char)/'%' . uc sprintf( '%02x', ord($1) )/eg;
89              
90 4         7 $transport_method = '_encoded_parameter';
91             }
92             elsif ( $value =~ /$non_attribute_char/ ) {
93 3         3 $transport_method = '_quoted_parameter';
94             }
95              
96             # XXX - hard code 78 as the max line length may not be right. Should
97             # this account for the length that the parameter name takes up (as
98             # well as encoding information, etc.)?
99              
100 10         11 my @pieces;
101 10         19 while ( length $value ) {
102 15         19 my $last_percent = rindex( $value, '%', 78 );
103              
104 15 100       28 my $size
    100          
105             = $last_percent >= 76 ? $last_percent
106             : length $value > 78 ? 78
107             : length $value;
108              
109 15         45 push @pieces, substr( $value, 0, $size, q{} );
110             }
111              
112 10 100       17 if ( @pieces == 1 ) {
113 7         21 return $self->$transport_method( undef, $pieces[0] );
114             }
115             else {
116             return join q{ },
117 3         7 map { $self->$transport_method( $_, $pieces[$_] ) }
  8         15  
118             0 .. $#pieces;
119             }
120             }
121             }
122              
123             ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
124             sub _simple_parameter {
125 4     4   5 my $self = shift;
126 4         4 my $order = shift;
127 4         5 my $value = shift;
128              
129 4         93 my $param = $self->name;
130 4 100       15 $param .= q{*} . $order if defined $order;
131 4         4 $param .= q{=};
132 4         5 $param .= $value;
133              
134 4         59 return $param;
135             }
136              
137             sub _quoted_parameter {
138 4     4   3 my $self = shift;
139 4         5 my $order = shift;
140 4         3 my $value = shift;
141              
142 4         91 my $param = $self->name;
143 4 100       9 $param .= q{*} . $order if defined $order;
144 4         4 $param .= q{=};
145              
146 4         5 $value =~ s/\"/\\\"/g;
147              
148 4         7 $param .= q{"} . $value . q{"};
149              
150 4         12 return $param;
151             }
152              
153             sub _encoded_parameter {
154 7     7   5 my $self = shift;
155 7         5 my $order = shift;
156 7         6 my $value = shift;
157              
158 7         170 my $param = $self->name;
159 7 100       15 $param .= q{*} . $order if defined $order;
160 7         10 $param .= q{*=};
161              
162             # XXX (1) - does it makes sense to just say everything is utf-8? in theory
163             # someone could pass through binary data in another encoding.
164 7 100       10 unless ($order) {
165 4   100     90 $param .= 'UTF-8' . q{'} . ( $self->language // q{} ) . q{'};
166             }
167              
168 7         8 $param .= $value;
169              
170 7         20 return $param;
171             }
172             ## use critic;
173              
174             __PACKAGE__->meta->make_immutable;
175              
176             1;
177              
178             # ABSTRACT: A single attribute belonging to a header
179              
180             __END__
181              
182             =pod
183              
184             =encoding utf-8
185              
186             =head1 NAME
187              
188             Courriel::HeaderAttribute - A single attribute belonging to a header
189              
190             =head1 VERSION
191              
192             version 0.42
193              
194             =head1 SYNOPSIS
195              
196             my $ct = $headers->get('Content-Type');
197             print $ct->get_attribute('charset')->value;
198              
199             =head1 DESCRIPTION
200              
201             This class represents a single attribute belonging to a header. An attribute
202             consists of a name and value, with optional charset and language information.
203              
204             =head1 API
205              
206             This class supports the following methods:
207              
208             =head1 Courriel::HeaderAttribute->new( ... )
209              
210             This method creates a new object. It accepts the following parameters:
211              
212             =over 4
213              
214             =item * name
215              
216             The name of the attribute. This should be a non-empty string.
217              
218             =item * value
219              
220             The value of the attribute. This can be empty.
221              
222             =item * charset
223              
224             The charset for the value. If the value contains any non-ASCII data, this will
225             always be "UTF-8", otherwise the default is "us-ascii".
226              
227             =item * language
228              
229             The language for the attribute's value. It should be a valid ISO language code
230             like "en-us" or "zh". This is optional.
231              
232             =back
233              
234             =head2 $attribute->name()
235              
236             The attribute name as passed to the constructor.
237              
238             =head2 $attribute->value()
239              
240             The attribute value as passed to the constructor.
241              
242             =head2 $attribute->charset()
243              
244             The attribute's charset.
245              
246             =head2 $attribute->language()
247              
248             The attribute's language.
249              
250             =head2 $attribute->as_string()
251              
252             This returns the attribute in a form suitable for putting in an email. This
253             may involve escaping, quoting, splitting up, and otherwise messing with the
254             value.
255              
256             If the value needs to be split across continuations, each name/value pair is
257             returned separate by a space, but not folded across multiple lines.
258              
259             =head2 $attribute->stream_to( output => $output )
260              
261             This method will send the stringified attribute to the specified output. The
262             output can be a subroutine reference, a filehandle, or an object with a
263             C<print()> method. The output may be sent as a single string, as a list of
264             strings, or via multiple calls to the output.
265              
266             =head1 ROLES
267              
268             This class does the C<Courriel::Role::Streams> role.
269              
270             =head1 SUPPORT
271              
272             Bugs may be submitted through L<the RT bug tracker|http://rt.cpan.org/Public/Dist/Display.html?Name=Courriel>
273             (or L<bug-courriel@rt.cpan.org|mailto:bug-courriel@rt.cpan.org>).
274              
275             I am also usually active on IRC as 'drolsky' on C<irc://irc.perl.org>.
276              
277             =head1 AUTHOR
278              
279             Dave Rolsky <autarch@urth.org>
280              
281             =head1 COPYRIGHT AND LICENCE
282              
283             This software is Copyright (c) 2016 by Dave Rolsky.
284              
285             This is free software, licensed under:
286              
287             The Artistic License 2.0 (GPL Compatible)
288              
289             =cut