File Coverage

blib/lib/Email/MIME/Kit/Bulk.pm
Criterion Covered Total %
statement 78 89 87.6
branch 10 28 35.7
condition 4 9 44.4
subroutine 21 22 95.4
pod 1 5 20.0
total 114 153 74.5


line stmt bran cond sub pod time code
1             package Email::MIME::Kit::Bulk;
2             our $AUTHORITY = 'cpan:YANICK';
3             # ABSTRACT: Email::MIME::Kit-based bulk mailer
4             $Email::MIME::Kit::Bulk::VERSION = '0.0.3';
5              
6 10     10   41 use Moose;
  10         19  
  10         69  
7 10     10   41603 use namespace::autoclean;
  10         11  
  10         81  
8              
9 10     10   5745 use Email::MIME;
  10         197567  
  10         299  
10 10     10   4205 use Email::MIME::Kit;
  10         506688  
  10         450  
11 10     10   5613 use Email::Sender::Simple 'sendmail';
  10         930022  
  10         91  
12 10     10   7767 use MooseX::Types::Email;
  10         1031527  
  10         322  
13 10     10   16933 use MooseX::Types::Path::Tiny qw/ Path /;
  10         1070612  
  10         71  
14 10     10   18918 use Try::Tiny;
  10         10  
  10         831  
15 10     10   5958 use PerlX::Maybe;
  10         15278  
  10         632  
16 10     10   4718 use List::AllUtils qw/ sum0 /;
  10         73680  
  10         696  
17 10     10   4522 use MCE::Map;
  10         323665  
  10         69  
18              
19 10     10   6790 use Email::MIME::Kit::Bulk::Kit;
  10         199  
  10         326  
20 10     10   5280 use Email::MIME::Kit::Bulk::Target;
  10         31  
  10         1044  
21              
22              
23             has targets => (
24             traits => ['Array'],
25             isa => 'ArrayRef[Email::MIME::Kit::Bulk::Target]',
26             required => 1,
27             handles => {
28             targets => 'elements',
29             num_targets => 'count',
30             },
31             );
32              
33              
34             has kit => (
35             is => 'ro',
36             isa => Path,
37             required => 1,
38             coerce => 1,
39             );
40              
41              
42             has from => (
43             is => 'ro',
44             isa => 'MooseX::Types::Email::EmailAddress',
45             required => 1,
46             );
47              
48              
49             has processes => (
50             is => 'ro',
51             isa => 'Maybe[Int]',
52             predicate => 'has_processes',
53             );
54              
55             sub single_process {
56 10     10   65 no warnings;
  10         11  
  10         8409  
57 9     9 0 225 return $_[0]->processes == 1;
58             }
59              
60             has verbose => (
61             isa => 'Bool',
62             is => 'ro',
63             default => 0,
64             );
65              
66             has transport => (
67             is => 'ro',
68             );
69              
70             sub mime_kit {
71 5     5 0 14 my $self = shift;
72 5         176 Email::MIME::Kit::Bulk::Kit->new({
73             source => $self->kit->stringify,
74             @_,
75             });
76             }
77              
78             around BUILDARGS => sub {
79             my $orig = shift;
80             my $class = shift;
81              
82             my $params = $class->$orig(@_);
83              
84             if (!exists $params->{targets} && exists $params->{to}) {
85             $params->{targets} = [
86             Email::MIME::Kit::Bulk::Target->new(
87             to => delete $params->{to},
88             map { maybe $_ => delete $params->{$_} } qw/ cc bcc /
89             )
90             ];
91             }
92              
93             return $params;
94             };
95              
96              
97             sub send {
98 9     9 1 9 my $self = shift;
99              
100 9         117 my $af = STDOUT->autoflush;
101              
102 9 50       711 MCE::Map::init { max_workers => $self->processes }
103             if $self->has_processes;
104              
105             my $errors = sum0
106             $self->single_process
107 0         0 ? map { $self->send_target($_) } $self->targets
108 9 50   2   36 : mce_map { $self->send_target($_) } $self->targets;
  2         14531  
109              
110 1 0       263749 warn "\n" . ($self->num_targets - $errors) . ' email(s) sent successfully'
    50          
111             . ($errors ? " ($errors failure(s))" : '') . "\n" if $self->verbose;
112              
113 1         28 STDOUT->autoflush($af);
114              
115 1         133 return $self->num_targets - $errors;
116             }
117              
118             sub send_target {
119 2     2 0 12 my( $self, $target ) = @_;
120              
121 2         16 my $email = $self->assemble_mime_kit($target);
122              
123             # work around bugs in q-p encoding (it forces \r\n, but the sendmail
124             # executable expects \n, or something like that)
125 2         397 (my $text = $email->as_string) =~ s/\x0d\x0a/\n/g;
126              
127             return try {
128 2     2   112 sendmail(
129             $text,
130             {
131             from => $target->from,
132             to => [ $target->recipients ],
133             maybe transport => $self->transport,
134             }
135             );
136 2 50       79034 print '.' if $self->verbose;
137 2         5 0;
138             }
139             catch {
140 0 0 0 0   0 my @recipients = (blessed($_) && $_->isa('Email::Sender::Failure'))
141             ? ($_->recipients)
142             : ($target->recipients);
143              
144             # XXX better error handling here - logging?
145 0         0 warn 'Failed to send to ' . join(', ', @recipients) . ': '
146             . "$_";
147              
148 0 0       0 print 'x' if $self->verbose;
149 0         0 1;
150 2         228 };
151             }
152              
153             sub assemble_mime_kit {
154 5     5 0 24 my $self = shift;
155 5         12 my ($target) = @_;
156              
157 5   66     321 my $from = $target->from || $self->from;
158 5         146 my $to = $target->to;
159 5         185 my @cc = $target->cc;
160              
161 5         10 my %opts;
162 5 50       193 $opts{language} = $target->language
163             if $target->has_language;
164              
165 5         34 my $kit = $self->mime_kit(%opts);
166 5         151 my $email = $kit->assemble($target->template_params);
167              
168 5 50       223180 if (my @attachments = $target->extra_attachments) {
169 0         0 my $old_email = $email;
170              
171             my @parts = map {
172 0 0       0 my $attach = ref($_) ? $_ : [$_];
  0         0  
173             Email::MIME->create(
174             attributes => {
175             filename => $attach->[0],
176             name => $attach->[0],
177             encoding => 'base64',
178             disposition => 'attachment',
179             ($attach->[1]
180             ? (content_type => $attach->[1])
181             : ()),
182             },
183 0 0       0 body => ${ $kit->get_kit_entry($attach->[0]) },
  0         0  
184             );
185             } @attachments;
186              
187 0         0 $email = Email::MIME->create(
188             header => [
189             Subject => $old_email->header_obj->header_raw('Subject'),
190             ],
191             parts => [
192             $old_email,
193             @parts,
194             ],
195             );
196             }
197              
198             # XXX Email::MIME::Kit reads the manifest.json file as latin1
199             # fix this in a better way once that is fixed?
200 5         14 my $subject = $email->header('Subject');
201 5         137 utf8::decode($subject);
202 5         18 $email->header_str_set('Subject' => $subject);
203              
204 5 50       229 $email->header_str_set('From' => $from)
205             unless $email->header('From');
206 5 50       361 $email->header_str_set('To' => $to)
207             unless $email->header('To');
208 5 100 66     356 $email->header_str_set('Cc' => join(', ', @cc))
209             unless $email->header('Cc') || !@cc;
210              
211 5         332 $email->header_str_set(
212             'X-UserAgent'
213             => "Email::MIME::Kit::Bulk v$Email::MIME::Kit::Bulk::VERSION"
214             );
215              
216 5         406 return $email;
217             }
218              
219             __PACKAGE__->meta->make_immutable;
220              
221             1;
222              
223             __END__
224              
225             =pod
226              
227             =encoding UTF-8
228              
229             =head1 NAME
230              
231             Email::MIME::Kit::Bulk - Email::MIME::Kit-based bulk mailer
232              
233             =head1 VERSION
234              
235             version 0.0.3
236              
237             =head1 SYNOPSIS
238              
239             use Email::MIME::Kit::Bulk;
240             use Email::MIME::Kit::Bulk::Target;
241              
242             my @targets = (
243             Email::MIME::Kit::Bulk::Target->new(
244             to => 'someone@somewhere.com',
245             ),
246             Email::MIME::Kit::Bulk::Target->new(
247             to => 'someone.else@somewhere.com',
248             cc => 'copied@somewhere.com',
249             language => 'en',
250             ),
251             );
252              
253             my $bulk = Email::MIME::Kit::Bulk->new(
254             kit => '/path/to/mime/kit',
255             processes => 5,
256             targets => \@targets,
257             );
258              
259             $bulk->send;
260              
261             =head1 DESCRIPTION
262              
263             C<Email::MIME::Kit::Bulk> is an extension of L<Email::MIME::Kit> for sending
264             bulk emails. The module can be used directly, or via the
265             companion script C<emk_bulk>.
266              
267             If a language is specified for a target, C<Email::MIME::Kit> will use
268             C<manifest.I<language>.json> to generate its associated email. If no language
269             is given, the regular C<manifest.json> will be used instead.
270              
271             If C<emk_bulk> is used, it'll look in the I<kit> directory for a
272             C<targets.json> file, which it'll use to create the email targets.
273             The format of the C<targets.json> file is a simple serialization of
274             the L<Email::MIME::Kit::Bulk::Target> constructor arguments:
275              
276             [
277             {
278             "to" : "someone@somewhere.com"
279             "cc" : [
280             "someone+cc@somewhere.com"
281             ],
282             "language" : "en",
283             "template_params" : {
284             "superlative" : "Fantastic"
285             },
286             },
287             {
288             "to" : "someone+french@somewhere.com"
289             "cc" : [
290             "someone+frenchcc@somewhere.com"
291             ],
292             "language" : "fr",
293             "template_params" : {
294             "superlative" : "Extraordinaire"
295             },
296             }
297             ]
298              
299             C<Email::MIME::Kit::Bulk> uses L<MCE> to parallize the sending of the emails.
300             The number of processes used can be set via the C<processes> constructor
301             argument. By default L<MCE> will select the number of processes based on
302             the number of available
303             processors. If the number of processes is set to be C<1>, L<MCE> is bypassed
304             altogether.
305              
306             =head1 METHODS
307              
308             =head2 new( %args )
309              
310             Constructor.
311              
312             =head3 Arguments
313              
314             =over
315              
316             =item targets => \@targets
317              
318             Takes in an array of L<Email::MIME::Kit::Bulk::Target> objects,
319             which are the email would-be recipients.
320              
321             Either the argument C<targets> or C<to> must be passed to the constructor.
322              
323             =item to => $email_address
324              
325             Email address of the 'C<To:>' recipient. Ignored if C<targets> is given as well.
326              
327             =item cc => $email_address
328              
329             Email address of the 'C<Cc:>' recipient. Ignored if C<targets> is given as well.
330              
331             =item bcc => $email_address
332              
333             Email address of the 'C<Bcc:>' recipient. Ignored if C<targets> is given as well.
334              
335             =item kit => $path
336              
337             Path of the directory holding the files used by L<Email::MIME::Kit>.
338             Can be a string or a L<Path::Tiny> object.
339              
340             =item from => $email_address
341              
342             'C<From>' address for the email .
343              
344             =item processes => $nbr
345              
346             Maximal number of parallel processes used to send the emails.
347              
348             If not specified, will be chosen by L<MCE>.
349             If set to 1, the parallel processing will be skipped
350             altogether.
351              
352             Not specified by default.
353              
354             =back
355              
356             =head2 send()
357              
358             Send the emails.
359              
360             =head1 AUTHORS
361              
362             =over 4
363              
364             =item *
365              
366             Jesse Luehrs <doy@cpan.org>
367              
368             =item *
369              
370             Yanick Champoux <yanick.champoux@iinteractive.com>
371              
372             =back
373              
374             =head1 COPYRIGHT AND LICENSE
375              
376             This software is copyright (c) 2017 by Infinity Interactive <contact@iinteractive.com>.
377              
378             This is free software; you can redistribute it and/or modify it under
379             the same terms as the Perl 5 programming language system itself.
380              
381             =cut