File Coverage

blib/lib/Email/MIME/Kit/Bulk.pm
Criterion Covered Total %
statement 79 90 87.7
branch 10 28 35.7
condition 4 9 44.4
subroutine 22 23 95.6
pod 1 5 20.0
total 116 155 74.8


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