File Coverage

blib/lib/Email/Mailer.pm
Criterion Covered Total %
statement 72 75 96.0
branch 22 28 78.5
condition 24 44 54.5
subroutine 12 12 100.0
pod 2 2 100.0
total 132 161 81.9


line stmt bran cond sub pod time code
1             package Email::Mailer;
2             # ABSTRACT: Multi-purpose emailer for HTML, auto-text, attachments, and templates
3              
4 1     1   195451 use 5.014;
  1         7  
5 1     1   405 use exact -noautoclean;
  1         28287  
  1         5  
6              
7 1     1   2579 use Email::MessageID;
  1         644  
  1         31  
8 1     1   509 use Email::MIME 1.940;
  1         21738  
  1         34  
9 1     1   491 use Email::MIME::CreateHTML;
  1         87648  
  1         50  
10 1     1   431 use Email::Sender::Simple 'sendmail';
  1         156760  
  1         6  
11 1     1   715 use HTML::FormatText;
  1         19113  
  1         30  
12 1     1   749 use HTML::TreeBuilder;
  1         6988  
  1         11  
13 1     1   645 use IO::All 'io';
  1         9263  
  1         7  
14 1     1   90 use MIME::Words 'encode_mimewords';
  1         2  
  1         1216  
15              
16             our $VERSION = '1.17'; # VERSION
17              
18             sub new {
19 14     14 1 33623 my $self = shift;
20              
21 14 100       30 unless ( ref $self ) {
22             # $self is not an object, is incoming pair values = make $self object
23 8         42 $self = bless( {@_}, $self );
24             }
25             else {
26             # $self is an object = make a new $self object incorporating any new values
27 6         42 $self = bless( { %$self, @_ }, ref $self );
28             }
29              
30             # for a certain set of keys, ensure they are all lower-case
31             $self->{ lc $_ } = delete $self->{$_}
32 14 100       60 for ( grep { /^(?:to|from|subject|html|text)$/i and /[A-Z]/ } keys %$self );
  62         256  
33              
34 14         74 return $self;
35             }
36              
37             sub send {
38 8     8 1 10495 my $self = shift;
39              
40             # if @_ is a set of hashrefs, map them into new mail objects; otherwise, just merge in new values;
41             # then iterate through the objects inside the map
42             my @mails = map {
43             # make a clean copy of the data so we can return the mail object unchanged at the end
44 9         34 my $mail = {%$_};
45              
46             # process any template functionality (look for values that are scalarrefs)
47 9 100       34 if ( ref $mail->{process} eq 'CODE' ) {
48 2         20 $mail->{$_} = $mail->{process}->( ${ $mail->{$_} }, $mail->{data} || {} )
49 1   50     4 for ( grep { ref $mail->{$_} eq 'SCALAR' } keys %$mail );
  6         10  
50             }
51              
52             # automatically create the text version from HTML if there is no text version and there is HTML
53 9 100 100     47 if ( $mail->{html} and not $mail->{text} ) {
54 6   100     20 my $width = $mail->{width} // 72;
55 6   100     22 $width ||= 1_000_000;
56              
57             $mail->{text} = HTML::FormatText
58             ->new( leftmargin => 0, rightmargin => $width )
59 6         55 ->format( HTML::TreeBuilder->new->parse( $mail->{html} ) );
60             }
61              
62 9   50     16865 $mail->{'Content-Transfer-Encoding'} //= 'quoted-printable';
63 9   50     48 $mail->{'Content-Type'} ||= 'text/plain; charset=us-ascii';
64              
65 9 50       70 my $charset = ( $mail->{'Content-Type'} =~ /\bcharset\s*=\s*([^;]+)/i ) ? $1 : 'ISO-8859-1';
66 9         39 my @keys = keys %$mail;
67 9         21 for my $name ( qw( to from subject ) ) {
68 27         37 my ($key) = grep { lc($_) eq $name } @keys;
  216         288  
69             $mail->{$key} = encode_mimewords( $mail->{$key}, Charset => $charset )
70 27 50 33     134 if ( $key and defined $mail->{$key} and $mail->{$key} =~ /[^[:ascii:]]/ );
      33        
71             }
72              
73 9   33     68 $mail->{'Message-Id'} //= Email::MessageID->new->in_brackets;
74              
75             # create a headers hashref (delete things from a data copy that known to not be headers)
76             my $headers = [
77             map {
78 54 50       105 $mail->{$_} = join( ',', @{ $mail->{$_} } ) if ( ref $mail->{$_} eq 'ARRAY' );
  0         0  
79 54 50       76 $mail->{$_} = join( ',', values %{ $mail->{$_} } ) if ( ref $mail->{$_} eq 'HASH' );
  0         0  
80 54         105 ucfirst($_) => $mail->{$_};
81             }
82 9         2019 grep { not /^(?:html|text|embed|attachments|process|data|transport|width)$/i }
  81         190  
83             sort keys %$mail
84             ];
85              
86             # build up an attachments arrayref of attachment MIME objects
87             my $attachments = ( not $mail->{attachments} or ref $mail->{attachments} ne 'ARRAY' ) ? [] : [
88             map {
89             Email::MIME->create(
90             attributes => {
91             disposition => 'attachment',
92             content_type => $_->{ctype} || 'application/octet-stream',
93             encoding => $_->{encoding} // 'base64',
94             filename => $_->{name} || $_->{filename} || $_->{source},
95             name => $_->{name} || $_->{filename} || $_->{source},
96             },
97 2 100 50     2071 body => ( ( $_->{content} ) ? $_->{content} : io( $_->{source} )->binary->all ),
      50        
      33        
      33        
98             ),
99 9 100 66     42 } @{ $mail->{attachments} }
  1         3  
100             ];
101              
102             # build a single MIME email object to send based on what data we have for the email
103 9         1435 my $email_mime;
104 9 100 66     66 if ( $mail->{text} and not $mail->{html} and @$attachments == 0 ) {
    50 66        
      33        
105             $email_mime = Email::MIME->create(
106             header => $headers,
107             body => $mail->{text},
108 1         8 );
109             }
110             elsif ( $mail->{text} and not $mail->{html} ) {
111             $email_mime = Email::MIME->create(
112             header => $headers,
113             attributes => { content_type => 'multipart/mixed' },
114             parts => [
115             Email::MIME->create(
116             header => [],
117             body => $mail->{text},
118 0         0 ),
119             @$attachments,
120             ],
121             );
122             }
123             else {
124             $email_mime = Email::MIME->create(
125             header => $headers,
126             attributes => { content_type => 'multipart/mixed' },
127             parts => [
128             Email::MIME->create_html(
129             header => [],
130             body => $mail->{html},
131             text_body => $mail->{text},
132             embed => $mail->{embed},
133 8         71 ),
134             @$attachments,
135             ],
136             );
137             }
138              
139             # send the email with Email::Sender::Simple
140 9         117280 sendmail( $email_mime, { transport => $mail->{transport} } );
141              
142 9         114 $_;
143 8 100       40 } ( ref $_[0] eq 'HASH' ) ? ( map { $self->new(%$_) } @_ ) : $self->new(@_);
  2         7  
144              
145             # return the mail objects as desired by the caller
146 8 50       42 return ( wantarray() ) ? (@mails) : \@mails;
147             }
148              
149             1;
150              
151             __END__
152              
153             =pod
154              
155             =encoding UTF-8
156              
157             =head1 NAME
158              
159             Email::Mailer - Multi-purpose emailer for HTML, auto-text, attachments, and templates
160              
161             =head1 VERSION
162              
163             version 1.17
164              
165             =for markdown [![test](https://github.com/gryphonshafer/Email-Mailer/workflows/test/badge.svg)](https://github.com/gryphonshafer/Email-Mailer/actions?query=workflow%3Atest)
166             [![codecov](https://codecov.io/gh/gryphonshafer/Email-Mailer/graph/badge.svg)](https://codecov.io/gh/gryphonshafer/Email-Mailer)
167              
168             =head1 SYNOPSIS
169              
170             use Email::Mailer;
171             my ( $to, $from, $subject, $text, $html );
172              
173             # send a simple text email
174             Email::Mailer->send(
175             to => $to,
176             from => $from,
177             subject => $subject,
178             text => $text,
179             );
180              
181             # send multi-part HTML/text email with the text auto-generated from the HTML
182             # and images and other resources embedded in the email
183             my $mail = Email::Mailer->new;
184             $mail->send(
185             to => $to,
186             from => $from,
187             subject => $subject,
188             html => $html,
189             );
190              
191             # send multi-part HTML/text email with the text auto-generated from the HTML
192             # but skip embedding images and other resources
193             Email::Mailer->new->send(
194             to => $to,
195             from => $from,
196             subject => $subject,
197             html => $html,
198             embed => 0,
199             );
200              
201             # send multi-part HTML/text email but supply the text explicitly
202             Email::Mailer->new(
203             to => $to,
204             from => $from,
205             subject => $subject,
206             html => $html,
207             text => $text,
208             )->send;
209              
210             # send multi-part HTML/text email with a couple of attached files
211             use IO::All 'io';
212             Email::Mailer->send(
213             to => $to,
214             from => $from,
215             subject => $subject,
216             html => $html,
217             text => $text,
218             attachments => [
219             {
220             ctype => 'application/pdf',
221             source => 'file.pdf',
222             },
223             {
224             ctype => 'application/pdf',
225             content => io('file.pdf')->binary->all,
226             encoding => 'base64',
227             name => 'file.pdf',
228             },
229             ],
230             );
231              
232             # build an email and iterate over a data set for sending
233             Email::Mailer->new(
234             from => $from,
235             subject => $subject,
236             html => $html,
237             )->send(
238             { to => 'person_0@example.com' },
239             { to => 'person_1@example.com' },
240             {
241             to => 'person_2@example.com',
242             subject => 'Override $subject with this',
243             },
244             );
245              
246             # setup a second mail object based on the first but changing the "from"
247             my $mail_0 = Email::Mailer->new(
248             from => $from,
249             subject => $subject,
250             html => $html,
251             );
252             my $mail_1 = $mail_0->new( from => 'different_address@example.com' );
253             $mail_0->send;
254             $mail_1->send;
255              
256             # use a templating system for the HTML and subject
257             use Template;
258             my $tt = Template->new;
259             my $tmail = Email::Mailer->new(
260             from => $from,
261             subject => \$subject,
262             html => \$html,
263             process => sub {
264             my ( $template, $data ) = @_;
265             my $content;
266             $tt->process( \$template, $data, \$content );
267             return $content;
268             },
269             );
270             $tmail->send($_) for (
271             { to => 'person_0@example.com', data => { name => 'Person 0' } },
272             { to => 'person_1@example.com', data => { name => 'Person 1' } },
273             );
274              
275             =head1 DESCRIPTION
276              
277             Following the charter and example of L<Email::Simple>, this module provides a
278             simple and flexible interface to sending various types of email including
279             plain text, HTML/text multi-part, attachment support, and template hooks.
280             The module depends on a series of great modules in the Email::* and HTML::*
281             namespaces.
282              
283             =head1 PRIMARY METHODS
284              
285             There are 2 primary methods.
286              
287             =head2 new
288              
289             This is an instantiator and a replicative instantiator. If passed nothing, it'll
290             return you a blank mail object. If you pass it anything, it'll use that data to
291             setup a more informed mail object for later sending.
292              
293             my $mail_blank = Email::Mailer->new;
294             my $mail_to = Email::Mailer->new( to => 'default_to@example.com');
295              
296             If you call C<new()> off an instantiated mail object, it'll make a copy of that
297             object, changing any internal data based on what you pass in to the C<new()>.
298              
299             # create a new object with both a default "To" and "From"
300             my $mail_to_from = $mail_to->new( from => 'default_from@example.com' );
301              
302             =head2 send
303              
304             This method will attempt to send mail. Any parameters you can pass to C<new()>
305             you can pass to C<send()>. Any incoming parameters will override any existing
306             parameters in an instantiated object.
307              
308             $mail_to_from->send(
309             subject => 'Example Subject Line',
310             text => 'Hello. This is example email content.',
311             );
312              
313             If C<send()> succeeds, it'll return an instantiated object based on the combined
314             parameters. If it fails, it'll throw an exception.
315              
316             use Try::Tiny;
317              
318             my $mail_with_all_the_parameters;
319             try {
320             $mail_with_all_the_parameters = $mail_to_from->send(
321             subject => 'Example Subject Line',
322             text => 'Hello. This is example email content.',
323             );
324             }
325             catch {
326             print "There was an error, but I'm going to ignore it and keep going.\n";
327             };
328              
329             You can also pass to C<send()> a list of hashrefs. If you do that, C<send()>
330             will assume you want each of the hashrefs to be like a set of data sent to an
331             independent call to C<send()>. The method will attempt to send multiple emails
332             based on your data, and it'll return an array or arrayref (based on context)
333             of the mail objects ultimately created.
334              
335             my @emails_sent = $mail_with_all_the_parameters->send(
336             { to => 'person_0@example.com' },
337             { to => 'person_1@example.com' },
338             );
339              
340             my $emails_sent = $mail_with_all_the_parameters->send(
341             { to => 'person_0@example.com' },
342             { to => 'person_1@example.com' },
343             );
344              
345             $mail_with_all_the_parameters->send($_) for (
346             { to => 'person_0@example.com' },
347             { to => 'person_1@example.com' },
348             );
349              
350             =head1 PARAMETERS
351              
352             There are a bunch of parameters you can pass to the primary methods. First off,
353             anything not explicitly mentioned in this section, the methods will assume is
354             a mail header.
355              
356             If any value of a key is a reference to scalar text, the value of that scalar
357             text will be assumed to be a template and processed through the subref defined
358             by the "process" parameter.
359              
360             =head2 html
361              
362             This parameter should contain HTML content (or a reference to scalar text that
363             is the template that'll be used to generate HTML content).
364              
365             =head2 text
366              
367             This parameter should contain plain text content (or a template reference). If
368             not provided then "text" will be automatically generated based on the "html"
369             content.
370              
371             By default, the text generated will be wrapped at 72 characters width. However,
372             you can override that by setting width explicitly:
373              
374             Email::Mailer->new->send(
375             to => $to,
376             from => $from,
377             subject => $subject,
378             html => $html,
379             width => 120,
380             );
381              
382             If you set a width to 0, this will be interpreted as meaning not to wrap text
383             lines.
384              
385             =head2 embed
386              
387             By default, if your HTML has links to things like images or CSS, those resources
388             will be pulled in and embedded into the email message. If you don't want that
389             behavior, turn it off by explicitly setting "embed" to a false value.
390              
391             Email::Mailer->new->send(
392             to => $to,
393             from => $from,
394             subject => $subject,
395             html => $html,
396             embed => 0,
397             );
398              
399             =head2 attachments
400              
401             This parameter if needed should be an arrayref of hashrefs that define the
402             attachments to add to an email. Each hashref should define a "ctype" for the
403             content type of the attachment and either a "source" or both a "name" and
404             "content" key. The "source" value should be a local relative path/file. The
405             "content" value should be binary data, and the "name" value should be the
406             filename of the attachment.
407              
408             use IO::All 'io';
409              
410             Email::Mailer->send(
411             to => $to,
412             from => $from,
413             subject => $subject,
414             html => $html,
415             text => $text,
416             attachments => [
417             {
418             ctype => 'application/pdf',
419             source => 'file.pdf',
420             },
421             {
422             ctype => 'application/pdf',
423             content => io('file.pdf')->binary->all,
424             encoding => 'base64',
425             name => 'file.pdf',
426             },
427             ],
428             );
429              
430             An optional parameter of "encoding" can be supplied in a hashref to
431             "attachments" to indicate what encoding the attachment should be encoded as.
432             If not specified, the default is "base64" encoding, which works in most cases.
433             Another popular choice is "quoted-printable".
434              
435             =head2 process
436              
437             This parameter expects a subref that will be called to process any templates.
438             You can hook in any template system you'd like. The subref will be passed the
439             template text and a hashref of the data for the message.
440              
441             use Template;
442              
443             my $tt = Template->new;
444             my $tmail = Email::Mailer->new(
445             from => $from,
446             subject => \$subject,
447             html => \$html,
448             process => sub {
449             my ( $template, $data ) = @_;
450             my $content;
451             $tt->process( \$template, $data, \$content );
452             return $content;
453             },
454             );
455              
456             =head2 data
457              
458             This parameter is the hashref of data that'll get passed to the "process"
459             subref.
460              
461             $tmail->send($_) for (
462             { to => 'person_0@example.com', data => { name => 'Person 0' } },
463             { to => 'person_1@example.com', data => { name => 'Person 1' } },
464             );
465              
466             =head2 transport
467              
468             By default, this module will try to pick an appropriate transport. (Well,
469             technically, L<Email::Sender::Simple> does that for us.) If you want to override
470             that and set your own transport, use the "transport" parameter.
471              
472             use Email::Sender::Transport::SMTP;
473              
474             Email::Mailer->send(
475             to => $to,
476             from => $from,
477             subject => $subject,
478             html => $html,
479             transport => Email::Sender::Transport::SMTP->new({
480             host => 'smtp.example.com',
481             port => 25,
482             }),
483             );
484              
485             =head1 AUTOMATIC HEADER-IFICATION
486              
487             There are some automatic header-ification features to be aware of. Unless you
488             specify a value, the C<Content-Type> and C<Content-Transfer-Encoding> are
489             set as "text/plain; charset=us-ascii" and "quoted-printable" respectively, as
490             if you set the following:
491              
492             Email::Mailer->send(
493             to => $to,
494             from => $from,
495             subject => $subject,
496             html => $html,
497              
498             'Content-Type' => 'text/plain; charset=us-ascii',
499             'Content-Transfer-Encoding' => 'quoted-printable',
500             );
501              
502             Also, normally your C<to>, C<from>, and C<subject> values are left untouched;
503             however, for any of these that contain non-ASCII characters, they will be
504             mimewords-encoded via L<MIME::Words> using the character set defined in
505             C<Content-Type>. If you don't like how that works, just encode them however
506             you'd like to ASCII.
507              
508             =head1 SEE ALSO
509              
510             L<Email::MIME>, L<Email::MIME::CreateHTML>, L<Email::Sender::Simple>,
511             L<Email::Sender::Transport>, L<HTML::FormatText>, L<HTML::TreeBuilder>.
512              
513             You can also look for additional information at:
514              
515             =over 4
516              
517             =item *
518              
519             L<GitHub|https://github.com/gryphonshafer/Email-Mailer>
520              
521             =item *
522              
523             L<MetaCPAN|https://metacpan.org/pod/Email::Mailer>
524              
525             =item *
526              
527             L<GitHub Actions|https://github.com/gryphonshafer/Email-Mailer/actions>
528              
529             =item *
530              
531             L<Codecov|https://codecov.io/gh/gryphonshafer/Email-Mailer>
532              
533             =item *
534              
535             L<CPANTS|http://cpants.cpanauthors.org/dist/Email-Mailer>
536              
537             =item *
538              
539             L<CPAN Testers|http://www.cpantesters.org/distro/D/Email-Mailer.html>
540              
541             =back
542              
543             =head1 AUTHOR
544              
545             Gryphon Shafer <gryphon@cpan.org>
546              
547             =head1 COPYRIGHT AND LICENSE
548              
549             This software is Copyright (c) 2017-2021 by Gryphon Shafer.
550              
551             This is free software, licensed under:
552              
553             The Artistic License 2.0 (GPL Compatible)
554              
555             =cut