File Coverage

blib/lib/Email/Sender/Transport/Mailgun.pm
Criterion Covered Total %
statement 56 56 100.0
branch 9 10 90.0
condition 6 6 100.0
subroutine 15 15 100.0
pod 0 3 0.0
total 86 90 95.5


line stmt bran cond sub pod time code
1             package Email::Sender::Transport::Mailgun;
2             our $VERSION = "0.02";
3              
4 5     5   915435 use Moo;
  5         46743  
  5         26  
5             with 'Email::Sender::Transport';
6              
7 5     5   10297 use HTTP::Tiny qw( );
  5         206061  
  5         175  
8 5     5   2406 use HTTP::Tiny::Multipart qw( );
  5         10493  
  5         108  
9 5     5   1744 use JSON::MaybeXS qw( );
  5         26519  
  5         141  
10 5     5   2548 use MooX::Types::MooseLike::Base qw( ArrayRef Enum Str);
  5         31799  
  5         525  
11              
12             {
13             package
14             Email::Sender::Success::MailgunSuccess;
15 5     5   43 use Moo;
  5         23  
  5         36  
16             extends 'Email::Sender::Success';
17             has id => (
18             is => 'ro',
19             required => 1,
20             );
21 5     5   2291 no Moo;
  5         19  
  5         29  
22             }
23              
24             has [qw( api_key domain )] => (
25             is => 'ro',
26             required => 1,
27             isa => Str,
28             );
29              
30             has [qw( campaign tag )] => (
31             is => 'ro',
32             predicate => 1,
33             isa => ArrayRef[Str],
34             coerce => sub { ref $_[0] ? $_[0] : [ split(/,\s*/, $_[0]) ] },
35             );
36              
37             has deliverytime => (
38             is => 'ro',
39             predicate => 1,
40             isa => Str,
41             coerce => sub {
42             ref $_[0] eq 'DateTime'
43             ? $_[0]->strftime('%a, %d %b %Y %H:%M:%S %z') : $_[0]
44             },
45             );
46              
47             has [qw( dkim testmode tracking tracking_opens )] => (
48             is => 'ro',
49             predicate => 1,
50             isa => Enum[qw( yes no )],
51             );
52              
53             has tracking_clicks => (
54             is => 'ro',
55             predicate => 1,
56             isa => Enum[qw( yes no htmlonly )],
57             );
58              
59             has region => (
60             is => 'ro',
61             predicate => 1,
62             isa => Enum[qw( us eu )],
63             );
64              
65             has base_uri => (
66             is => 'lazy',
67 4     4   44 builder => sub { 'https://api.mailgun.net/v3' },
68             );
69              
70             has uri => (
71             is => 'lazy',
72             );
73              
74             has ua => (
75             is => 'lazy',
76 2     2   35 builder => sub { HTTP::Tiny->new(verify_SSL => 1) },
77             );
78              
79             has json => (
80             is => 'lazy',
81 2     2   32 builder => sub { JSON::MaybeXS->new },
82             );
83              
84             # https://documentation.mailgun.com/api-sending.html#sending
85             sub send_email {
86 4     4 0 25008 my ($self, $email, $env) = @_;
87              
88             my $content = {
89 4         23 to => ref $env->{to} ? join(',', @{ $env->{to} }) : $env->{to},
90 4 50       21 message => {
91             filename => 'message.mime',
92             content => $email->as_string,
93             },
94             };
95              
96 4         445 my @options = qw(
97             campaign deliverytime dkim tag testmode
98             tracking tracking_clicks tracking_opens
99             );
100              
101 4         14 for my $option (@options) {
102 32         64 my $has_option = "has_$option";
103 32 100       116 if ($self->$has_option) {
104 4         9 my $key = "o:$option";
105 4         7 $key =~ tr/_/-/;
106 4         36 $content->{$key} = $self->$option;
107             }
108             }
109              
110 4         108 my $uri = $self->uri . '/messages.mime';
111 4         91 my $response = $self->ua->post_multipart($uri, $content);
112              
113             $self->failure($response, $env->{to})
114 4 100       1441 unless $response->{success};
115              
116 1         5 return $self->success($response);
117             }
118              
119             sub success {
120 1     1 0 4 my ($self, $response) = @_;
121              
122 1         23 my $content = $self->json->decode($response->{content});
123 1         31 return Email::Sender::Success::MailgunSuccess->new(id => $content->{id});
124             }
125              
126             sub failure {
127 3     3 0 10 my ($self, $response, $recipients) = @_;
128              
129             # Most errors have { message => $message } in the content, some, such as
130             # an auth error, have just a plain string.
131 3         7 my $content = eval { $self->json->decode($response->{content}) };
  3         68  
132             my $message = $content && $content->{message}
133 3 100 100     83 ? $content->{message} : $response->{content};
134              
135 3         32 Email::Sender::Failure->throw({
136             message => $message,
137             recipients => $recipients,
138             });
139             }
140              
141             sub _build_uri {
142 5     5   5984 my $self = shift;
143              
144 5         84 my ($proto, $rest) = split('://', $self->base_uri);
145 5         34 my $api_key = $self->api_key;
146 5         13 my $domain = $self->domain;
147              
148             # adapt endpoint based on region setting.
149 5 100 100     35 $rest =~ s/(\.mailgun)/sprintf('.%s%s', $self->region, $1)/e
  1         7  
150             if defined $self->region && $self->region ne 'us';
151              
152 5         34 return "$proto://api:$api_key\@$rest/$domain";
153             }
154              
155 5     5   5180 no Moo;
  5         11  
  5         22  
156             1;
157             __END__
158              
159             =encoding utf-8
160              
161             =for stopwords deliverytime dkim hardcode mailouts prepend templated testmode
162              
163             =head1 NAME
164              
165             Email::Sender::Transport::Mailgun - Mailgun transport for Email::Sender
166              
167             =head1 SYNOPSIS
168              
169             use Email::Sender::Simple qw( sendmail );
170             use Email::Sender::Transport::Mailgun qw( );
171              
172             my $transport = Email::Sender::Transport::Mailgun->new(
173             api_key => '...',
174             domain => '...',
175             );
176              
177             my $message = ...;
178              
179             sendmail($message, { transport => $transport });
180              
181             =head1 DESCRIPTION
182              
183             This transport delivers mail via Mailgun's messages.mime API.
184              
185             =head2 Why use this module?
186              
187             The SMTP transport can also be used to send messages through Mailgun. In this
188             case, Mailgun options must be specified with Mailgun-specific MIME headers.
189              
190             This module exposes those options as attributes, which can be set in code, or
191             via C<EMAIL_SENDER_TRANSPORT_> environment variables.
192              
193             =head2 Why not use this module?
194              
195             This module uses Mailgun's messages.mime API, not the full-blown messages API.
196              
197             If you want to use advanced Mailgun features such as templated batch mailouts
198             or mailing lists, you're better off using something like L<WebService::Mailgun>
199             or L<WWW::Mailgun>.
200              
201             =head1 REQUIRED ATTRIBUTES
202              
203             The attributes all correspond directly to Mailgun parameters.
204              
205             =head2 api_key
206              
207             Mailgun API key. See L<https://documentation.mailgun.com/api-intro.html#authentication>
208              
209             =head2 domain
210              
211             Mailgun domain. See L<https://documentation.mailgun.com/api-intro.html#base-url>
212              
213             =head1 OPTIONAL ATTRIBUTES
214              
215             These (except region) correspond to the C<o:> options in the C<messages.mime>
216             section of L<https://documentation.mailgun.com/api-sending.html#sending>
217              
218             =head2 campaign
219              
220             Id of the campaign. Comma-separated string list or arrayref of strings.
221              
222             =head2 deliverytime
223              
224             Desired time of delivery. String or DateTime object.
225              
226             =head2 dkim
227              
228             Enables/disables DKIM signatures. C<'yes'> or C<'no'>.
229              
230             =head2 region
231              
232             Defines used Mailgun region. C<'us'> (default) or C<'eu'>.
233              
234             See L<https://documentation.mailgun.com/en/latest/api-intro.html#mailgun-regions>.
235              
236             =head2 tag
237              
238             Tag string. Comma-separated string list or arrayref of strings.
239              
240             =head2 testmode
241              
242             Enables sending in test mode. C<'yes'> or C<'no'>.
243              
244             =head2 tracking
245              
246             Toggles tracking. C<'yes'> or C<'no'>.
247              
248             =head2 tracking_clicks
249              
250             Toggles clicks tracking. C<'yes'>, C<'no'> or C<'html_only'>.
251              
252             =head2 tracking_opens
253              
254             Toggles open tracking. C<'yes'> or C<'no'>.
255              
256             =head1 MIME HEADERS
257              
258             The C<o:> options above can also be specified using the C<X-Mailgun-> headers
259             listed here L<https://documentation.mailgun.com/user_manual.html#sending-via-smtp>
260              
261             If a single-valued option is specified in both the options and the headers,
262             experimentation shows the header takes precedence. This doesn't seem to be
263             documented, so don't rely on this behaviour.
264              
265             Multi-valued options use both the options and the headers.
266              
267             =head1 ENVIRONMENT
268              
269             The great strength of Email::Sender is that you don't need to hardcode your
270             transport, nor any of the options relating to that transport. They can all be
271             specified via environment variables.
272              
273             To select the Mailgun transport, use C<EMAIL_SENDER_TRANSPORT=Mailgun>.
274              
275             To specify any of the attributes above, prepend the attribute name with
276             C<EMAIL_SENDER_TRANSPORT_>.
277              
278             =over
279              
280             =item EMAIL_SENDER_TRANSPORT_api_key
281              
282             =item EMAIL_SENDER_TRANSPORT_domain
283              
284             =item EMAIL_SENDER_TRANSPORT_campaign
285              
286             =item EMAIL_SENDER_TRANSPORT_deliverytime
287              
288             =item EMAIL_SENDER_TRANSPORT_dkim
289              
290             =item EMAIL_SENDER_TRANSPORT_region
291              
292             =item EMAIL_SENDER_TRANSPORT_tag
293              
294             =item EMAIL_SENDER_TRANSPORT_testmode
295              
296             =item EMAIL_SENDER_TRANSPORT_tracking
297              
298             =item EMAIL_SENDER_TRANSPORT_tracking_clicks
299              
300             =item EMAIL_SENDER_TRANSPORT_tracking_opens
301              
302             =back
303              
304             =head1 LICENSE
305              
306             Copyright (C) Stephen Thirlwall.
307              
308             This library is free software; you can redistribute it and/or modify
309             it under the same terms as Perl itself.
310              
311             =head1 AUTHOR
312              
313             Stephen Thirlwall E<lt>sdt@cpan.orgE<gt>
314              
315             =cut