File Coverage

blib/lib/Email/MIME/Kit/Assembler/Standard.pm
Criterion Covered Total %
statement 150 158 94.9
branch 53 76 69.7
condition 20 26 76.9
subroutine 24 24 100.0
pod 0 4 0.0
total 247 288 85.7


line stmt bran cond sub pod time code
1             package Email::MIME::Kit::Assembler::Standard 3.000008;
2             # ABSTRACT: the standard kit assembler
3              
4 4     4   2344 use v5.20.0;
  4         16  
5 4     4   23 use Moose;
  4         11  
  4         28  
6 4     4   26321 use Moose::Util::TypeConstraints;
  4         10  
  4         33  
7              
8             #pod =head1 WARNING
9             #pod
10             #pod Email::MIME::Kit::Assembler::Standard works well, but is poorly decomposed,
11             #pod internally. Its methods may change substantially in the future, so relying on
12             #pod it as a base class is a bad idea.
13             #pod
14             #pod Because I<being able to> rely on it would be so useful, its behaviors will in
15             #pod the future be more reliable or factored out into roles. Until then, be
16             #pod careful.
17             #pod
18             #pod =cut
19              
20             with 'Email::MIME::Kit::Role::Assembler';
21              
22 4     4   9207 use Email::MIME::Creator;
  4         9  
  4         148  
23 4     4   24 use Encode ();
  4         10  
  4         90  
24 4     4   26 use File::Basename;
  4         9  
  4         7812  
25              
26             sub BUILD {
27 18     18 0 42 my ($self) = @_;
28 18         70 $self->_setup_content_ids;
29 18         69 $self->_pick_and_set_renderer;
30 18         82 $self->_build_subassemblies;
31             }
32              
33             has parent => (
34             is => 'ro',
35             isa => maybe_type(role_type('Email::MIME::Kit::Role::Assembler')),
36             weak_ref => 1,
37             );
38              
39             has renderer => (
40             reader => 'renderer',
41             writer => '_set_renderer',
42             clearer => '_unset_renderer',
43             isa => maybe_type(role_type('Email::MIME::Kit::Role::Renderer')),
44             init_arg => undef,
45             );
46              
47             sub assemble {
48 20     20 0 57 my ($self, $stash) = @_;
49              
50 20         699 my $manifest = $self->manifest;
51              
52 20         50 my $has_body = defined $manifest->{body};
53 20         49 my $has_path = defined $manifest->{path};
54 20 50       30 my $has_alts = @{ $manifest->{alternatives} || [] };
  20         66  
55 20 50       38 my $has_att = @{ $manifest->{attachments} || [] };
  20         77  
56              
57 20 50 100     112 Carp::croak("neither body, path, nor alternatives provided")
      66        
58             unless $has_body or $has_path or $has_alts;
59              
60             Carp::croak("you must provide only one of body, path, or alternatives")
61 20 50       53 unless (grep {$_} $has_body, $has_path, $has_alts) == 1;
  60         160  
62              
63 20 50       85 my $assembly_method = $has_body ? '_assemble_from_manifest_body'
    100          
    100          
64             : $has_path ? '_assemble_from_kit'
65             : $has_alts ? '_assemble_mp_alt'
66             : confess "unreachable code is a mistake";
67              
68 20         135 $self->$assembly_method($stash);
69             }
70              
71             sub _assemble_from_string {
72 17     17   61 my ($self, $body, $stash) = @_;
73              
74 17 100       31 my %attr = %{ $self->manifest->{attributes} || {} };
  17         594  
75 17   100     73 $attr{content_type} ||= 'text/plain';
76              
77 17 100       72 if ($attr{content_type} =~ m{^text/}) {
78             # I really shouldn't have to do this, but I'm not going to go screw around
79             # with @#$@#$ Email::Simple/MIME just to deal with it right now. -- rjbs,
80             # 2009-01-19
81 13 100       83 $body .= "\x0d\x0a" unless $body =~ /[\x0d|\x0a]\z/;
82             }
83              
84 17         64 my $body_ref = $self->render(\$body, $stash);
85              
86             my $email = $self->_contain_attachments({
87             attributes => \%attr,
88             header => $self->manifest->{header},
89             stash => $stash,
90             body => $$body_ref,
91             container_type => $self->manifest->{container_type},
92 17         638 });
93             }
94              
95             sub _assemble_from_manifest_body {
96 7     7   20 my ($self, $stash) = @_;
97              
98             $self->_assemble_from_string(
99             $self->manifest->{body},
100 7         233 $stash,
101             );
102             }
103              
104             sub _assemble_from_kit {
105 10     10   27 my ($self, $stash) = @_;
106              
107 10   100     353 my $type = $self->manifest->{attributes}{content_type} || 'text/plain';
108 10 100       42 my $method = $type =~ m{^text/} ? 'get_decoded_kit_entry' : 'get_kit_entry';
109              
110 10         307 my $body_ref = $self->kit->$method($self->manifest->{path});
111              
112 10         49 $self->_assemble_from_string($$body_ref, $stash);
113             }
114              
115             sub _assemble_mp_alt {
116 3     3   15 my ($self, $stash) = @_;
117              
118 3 50       6 my %attr = %{ $self->manifest->{attributes} || {} };
  3         109  
119 3   50     21 $attr{content_type} = $attr{content_type} || 'multipart/alternative';
120              
121 3 50       45 if ($attr{content_type} !~ qr{\Amultipart/alternative\b}) {
122 0         0 confess "illegal content_type for mail with alts: $attr{content_type}";
123             }
124              
125 3         14 my $parts = [ map { $_->assemble($stash) } @{ $self->_alternatives } ];
  8         18284  
  3         108  
126              
127             my $email = $self->_contain_attachments({
128             attributes => \%attr,
129             header => $self->manifest->{header},
130 3         1819 stash => $stash,
131             parts => $parts,
132             });
133             }
134              
135             sub _renderer_from_override {
136 8     8   18 my ($self, $override) = @_;
137              
138             # Allow an explicit undef to mean "no rendering is to be done." -- rjbs,
139             # 2009-01-19
140 8 50       26 return undef unless defined $override;
141              
142 0         0 return $self->kit->_build_component(
143             'Email::MIME::Kit::Renderer',
144             $override,
145             );
146             }
147              
148             sub _pick_and_set_renderer {
149 18     18   62 my ($self) = @_;
150              
151             # "renderer" entry at top-level sets the kit default_renderer, so trying to
152             # look at the "renderer" entry at top-level for an override is nonsensical
153             # -- rjbs, 2009-01-22
154 18 100       569 unless ($self->parent) {
155 6         189 $self->_set_renderer($self->kit->default_renderer);
156 6         16 return;
157             }
158              
159             # If there's no override, we just use the parent. We don't need to worry
160             # about the "there is no parent" case, because that was handled above. --
161             # rjbs, 2009-01-22
162 12 100       362 unless (exists $self->manifest->{renderer}) {
163 10         342 $self->_set_renderer($self->parent->renderer);
164 10         22 return;
165             }
166              
167 2         76 my $renderer = $self->_renderer_from_override($self->manifest->{renderer});
168 2         76 $self->_set_renderer($renderer);
169             }
170              
171             has manifest => (
172             is => 'ro',
173             required => 1,
174             );
175              
176             has [ qw(_attachments _alternatives) ] => (
177             is => 'ro',
178             isa => 'ArrayRef',
179             init_arg => undef,
180             default => sub { [] },
181             );
182              
183             has _body => (
184             reader => 'body',
185             writer => '_set_body',
186             );
187              
188             sub _build_subassemblies {
189 18     18   43 my ($self) = @_;
190              
191 18 100       583 if (my $body = $self->manifest->{body}) {
192 6         210 $self->_set_body($body);
193             }
194              
195 18 50       33 for my $attach (@{ $self->manifest->{attachments} || [] }) {
  18         549  
196 4         118 my $assembler = $self->kit->_assembler_from_manifest($attach, $self);
197 4 50       26 $assembler->_set_attachment_info($attach)
198             if $assembler->can('_set_attachment_info');
199 4         7 push @{ $self->_attachments }, $assembler;
  4         138  
200             }
201              
202 18 50       39 for my $alt (@{ $self->manifest->{alternatives} || [] }) {
  18         561  
203 8         17 push @{ $self->_alternatives },
  8         259  
204             $self->kit->_assembler_from_manifest($alt, $self);
205             }
206             }
207              
208             sub _set_attachment_info {
209 4     4   10 my ($self, $manifest) = @_;
210              
211 4   50     15 my $attr = $manifest->{attributes} ||= {};
212              
213 4 50       14 $attr->{encoding} = 'base64' unless exists $attr->{encoding};
214 4 50       13 $attr->{disposition} = 'attachment' unless exists $attr->{disposition};
215              
216 4 100       14 unless (exists $attr->{filename}) {
217 2         3 my $filename;
218             ($filename) = File::Basename::fileparse($manifest->{path})
219 2 50       74 if $manifest->{path};
220              
221             # XXX: Steal the attachment-name-generator from Email::MIME::Modifier, or
222             # something. -- rjbs, 2009-01-20
223 2   50     18 $filename ||= "unknown-attachment";
224              
225 2         6 $attr->{filename} = $filename;
226             }
227             }
228              
229             sub render {
230 17     17 0 39 my ($self, $input_ref, $stash) = @_;
231 17     2   89 local $stash->{cid_for} = sub { $self->cid_for_path($_[0]) };
  2         9  
232 17 100       614 return $input_ref unless my $renderer = $self->renderer;
233 12         54 return $renderer->render($input_ref, $stash);
234             }
235              
236             sub _prep_header {
237 20     20   78 my ($self, $header, $stash) = @_;
238              
239 20         30 my @done_header;
240 20         73 for my $entry (@$header) {
241             confess "no field name candidates"
242 24 50       79 unless my (@hval) = grep { /^[^:]/ } keys %$entry;
  30         155  
243 24 50       71 confess "multiple field name candidates: @hval" if @hval > 1;
244 24         58 my $value = $entry->{ $hval[ 0 ] };
245              
246 24 50       56 if (ref $value) {
247 0         0 my ($v, $p) = @$value;
248 0         0 $value = join q{; }, $v, map { "$_=$p->{$_}" } keys %$p;
  0         0  
249             } else {
250             # I don't think I need to bother with $self->render, which will set up
251             # the cid_for callback. Honestly, who is going to be referencing a
252             # content-id from a header? Let's hope I never find out... -- rjbs,
253             # 2009-01-22
254             my $renderer = exists $entry->{':renderer'}
255 24 100       703 ? $self->_renderer_from_override($entry->{':renderer'})
256             : $self->renderer;
257              
258 24 100       71 $value = ${ $renderer->render(\$value, $stash) } if defined $renderer;
  12         57  
259             }
260              
261 24         81 push @done_header, $hval[0] => $value;
262             }
263              
264 20         59 return \@done_header;
265             }
266              
267             sub _contain_attachments {
268 20     20   50 my ($self, $arg) = @_;
269              
270 20         40 my @attachments = @{ $self->_attachments };
  20         665  
271 20         72 my $header = $self->_prep_header($arg->{header}, $arg->{stash});
272              
273 20         42 my $ct = $arg->{container_type};
274              
275 20         36 my %attr = %{ $arg->{attributes} };
  20         86  
276 20         51 my $body_type = 'body';
277              
278 20 100 100     160 if ($attr{content_type} =~ m{^text/}) {
    100          
279 13         26 $body_type = 'body_str';
280              
281 13   50     74 $attr{encoding} ||= 'quoted-printable';
282 13   100     51 $attr{charset} ||= 'UTF-8'
283             } elsif (($arg->{body} || '') =~ /\P{ASCII}/) {
284 4   50     10 $attr{encoding} ||= 'base64';
285             }
286              
287 20 100       57 unless (@attachments) {
288 16 50       41 confess "container_type given for single-part assembly" if $ct;
289              
290             return Email::MIME->create(
291             attributes => \%attr,
292             header_str => $header,
293             $body_type => $arg->{body},
294             parts => $arg->{parts},
295 16         136 );
296             }
297              
298             my $email = Email::MIME->create(
299             attributes => \%attr,
300             $body_type => $arg->{body},
301             parts => $arg->{parts},
302 4         39 );
303              
304 4         13859 my @att_parts = map { $_->assemble($arg->{stash}) } @attachments;
  4         16  
305              
306 4   100     7883 my $container = Email::MIME->create(
307             attributes => { content_type => ($ct || 'multipart/mixed') },
308             header_str => $header,
309             parts => [ $email, @att_parts ],
310             );
311              
312 4         32084 return $container;
313             }
314              
315             has _cid_registry => (
316             is => 'ro',
317             init_arg => undef,
318             default => sub { { } },
319             );
320              
321             sub cid_for_path {
322 2     2 0 7 my ($self, $path) = @_;
323 2         75 my $cid = $self->_cid_registry->{ $path };
324              
325 2 50       8 confess "no content-id for path $path" unless $cid;
326              
327 2         17 return $cid;
328             }
329              
330             sub _setup_content_ids {
331 18     18   74 my ($self) = @_;
332              
333 18 50       32 for my $att (@{ $self->manifest->{attachments} || [] }) {
  18         619  
334 4 50       15 next unless $att->{path};
335              
336 4         10 for my $header (@{ $att->{header} }) {
  4         11  
337 0         0 my ($header) = grep { /^[^:]/ } keys %$header;
  0         0  
338 0 0       0 Carp::croak("attachments must not supply content-id")
339             if lc $header eq 'content-id';
340             }
341              
342 4         126 my $cid = $self->kit->_generate_content_id;
343 4         2078 push @{ $att->{header} }, {
  4         19  
344             'Content-Id' => $cid->in_brackets,
345             ':renderer' => undef,
346             };
347              
348 4         81 $self->_cid_registry->{ $att->{path} } = $cid->as_string;
349             }
350             }
351              
352 4     4   2922 no Moose::Util::TypeConstraints;
  4         11  
  4         24  
353 4     4   1207 no Moose;
  4         10  
  4         26  
354             __PACKAGE__->meta->make_immutable;
355             1;
356              
357             __END__
358              
359             =pod
360              
361             =encoding UTF-8
362              
363             =head1 NAME
364              
365             Email::MIME::Kit::Assembler::Standard - the standard kit assembler
366              
367             =head1 VERSION
368              
369             version 3.000008
370              
371             =head1 PERL VERSION
372              
373             This library should run on perls released even a long time ago. It should
374             work on any version of perl released in the last five years.
375              
376             Although it may work on older versions of perl, no guarantee is made that the
377             minimum required version will not be increased. The version may be increased
378             for any reason, and there is no promise that patches will be accepted to
379             lower the minimum required perl.
380              
381             =head1 WARNING
382              
383             Email::MIME::Kit::Assembler::Standard works well, but is poorly decomposed,
384             internally. Its methods may change substantially in the future, so relying on
385             it as a base class is a bad idea.
386              
387             Because I<being able to> rely on it would be so useful, its behaviors will in
388             the future be more reliable or factored out into roles. Until then, be
389             careful.
390              
391             =head1 AUTHOR
392              
393             Ricardo Signes <cpan@semiotic.systems>
394              
395             =head1 COPYRIGHT AND LICENSE
396              
397             This software is copyright (c) 2023 by Ricardo Signes.
398              
399             This is free software; you can redistribute it and/or modify it under
400             the same terms as the Perl 5 programming language system itself.
401              
402             =cut