File Coverage

blib/lib/Amazon/SES.pm
Criterion Covered Total %
statement 79 81 97.5
branch n/a
condition n/a
subroutine 11 11 100.0
pod n/a
total 90 92 97.8


line stmt bran cond sub pod time code
1 1     1   200051 use Moops;
  1         38274  
  1         5  
2              
3             # ABSTRACT: Interfaces with AWS's SES service.
4              
5 1     1   142585 class Amazon::SES {
  1         26  
  1         8  
  1         1  
  1         49  
  1         451  
  1         1678  
  1         3  
  1         1404  
  1         2  
  1         7  
  1         79  
  1         2  
  1         67  
  1         5  
  1         1  
  1         83  
  1         23  
  1         5  
  1         1  
  1         11  
  1         3087  
  1         3  
  1         7  
  1         843  
  1         2900  
  1         7  
  1         123  
  1         2  
  1         8  
  1         501  
  1         6975  
  1         10  
  1         766  
  1         2355  
  1         4  
  1         1149  
  1         3200  
  1         9  
  1         111926  
  1         5  
  1         35  
  1         3  
  1         1  
  1         29  
  1         4  
  1         1  
  1         31  
  1         4  
  1         1  
  1         109  
6 1     1   5 use strict;
  1         1  
  1         28  
7 1     1   3 use warnings;
  1         2  
  1         22  
8 1     1   4 use Carp ('croak');
  1         2  
  1         65  
9 1     1   7 use MIME::Base64;
  1         2  
  1         73  
10 1     1   606 use Time::Piece;
  1         9064  
  1         6  
11 1     1   85 use HTTP::Headers;
  1         2  
  1         26  
12 1     1   5 use LWP::UserAgent;
  1         1  
  1         39  
13 1     1   543 use AWS::Signature4;
  1         16024  
  1         43  
14 1     1   456 use Amazon::SES::Response;
  0            
  0            
15             use VM::EC2::Security::CredentialCache;
16             use HTTP::Request::Common;
17             use Kavorka qw( multi method );
18              
19              
20              
21             has 'ua' => (is => 'ro', default => sub { return LWP::UserAgent->new() } );
22             has 'use_iam_role' => ( is => 'ro', default => 0 );
23             has 'access_key' => ( is => 'ro' );
24             has 'secret_key' => ( is => 'ro' );
25             has 'region' => ( is => 'ro', default => 'us-east-1' );
26             has 'response' => ( is => 'rw' );
27            
28              
29             method call(Str $action, HashRef $args? = {}) {
30             $args->{AWSAccessKeyId} = $self->access_key;
31             $args->{Action} = $action;
32            
33             my $request = POST("https://email." . $self->region . ".amazonaws.com", $args);
34            
35             if ($self->{use_iam_role}) {
36             my $creds = VM::EC2::Security::CredentialCache->get();
37             defined($creds) || die("Unable to retrieve IAM role credentials");
38             $self->{access_key} = $creds->accessKeyId;
39             $self->{secret_key} = $creds->secretAccessKey;
40             $request->header('x-amz-security-token' => $creds->sessionToken);
41             }
42            
43            
44             # Add the signature.
45             my $signer = AWS::Signature4->new(-access_key => $self->access_key,
46             -secret_key => $self->secret_key);
47             $signer->sign($request);
48            
49             my $response = $self->ua->request($request);
50             return Amazon::SES::Response->new(response => $response,
51             action => $action );
52             }
53              
54             multi method send(MIME::Entity $message) {
55             $self->send_mime($message);
56             }
57              
58             multi method send(Str :$from,
59             Str :$body?,
60             Str :$body_html?,
61             Str :$to,
62             Str :$subject?,
63             Str :$charset = "UTF-8",
64             Str :$return_path?,
65             ) {
66             $to = [$to] unless ref($to);
67             defined($body) || defined($body_html) || die("No body specified");
68             my %call_args = (
69             'Message.Subject.Data' => $subject,
70             'Message.Subject.Charset' => $charset,
71             'Source' => $from
72             );
73            
74             if (defined($body)) {
75             $call_args{'Message.Body.Text.Data'} = $body;
76             $call_args{'Message.Body.Text.Charset'} = $charset;
77             }
78            
79            
80             if (defined($body_html)) {
81             $call_args{'Message.Body.Html.Data'} = $body_html;
82             $call_args{'Message.Body.Html.Charset'} = $charset;
83             }
84              
85             if (defined($return_path)) {
86             $call_args{'ReturnPath'} = $return_path;
87             }
88             my $i = 1;
89             map {
90             $call_args{'Destination.ToAddresses.member.' . $i++} = $_;
91             } @$to;
92            
93             $self->call( 'SendEmail', \%call_args );
94             }
95            
96            
97             method verify_email(Str $email) {
98             return $self->call( 'VerifyEmailIdentity', { EmailAddress => $email } );
99             }
100            
101             method delete_domain(Str $identity) {
102             return $self->call( 'DeleteIdentity', { Identity => $identity } );
103             }
104              
105             method delete_email(Str $identity) {
106             return $self->call( 'DeleteIdentity', { Identity => $identity } );
107             }
108              
109             method delete_identity(Str $identity) {
110             return $self->call( 'DeleteIdentity', { Identity => $identity } );
111             }
112            
113            
114             method list_emails(Int :$limit?,
115             Int :$offset?) {
116             my %call_args = ( IdentityType => 'EmailAddress' );
117            
118             defined($limit) && ($call_args{MaxItems} = $limit);
119             defined($offset) && ($call_args{NextToken} = $offset);
120             my $r = $self->call( 'ListIdentities', \%call_args );
121             }
122            
123            
124             method list_domains(Int :$limit?,
125             Int :$offset?) {
126             my %call_args = ( IdentityType => 'Domain' );
127            
128             defined($limit) && ($call_args{MaxItems} = $limit);
129             defined($offset) && ($call_args{NextToken} = $offset);
130             my $r = $self->call( 'ListIdentities', \%call_args );
131             }
132            
133             method get_quota() {
134             return $self->call('GetSendQuota');
135             }
136            
137             method get_statistics() {
138             return $self->call('GetSendStatistics');
139             }
140            
141             method send_mime(Str|MIME::Entity $message) {
142             my $src = $message;
143             if (ref($message) && $message->isa("MIME::Entity") ) {
144             $src = $message->stringify;
145             }
146            
147             return $self->call( 'SendRawEmail',
148             { 'RawMessage.Data' => MIME::Base64::encode_base64($src) } );
149             }
150            
151             method get_dkim_attributes(Str @identities) {
152             my %call_args = ();
153             my $i =1 ;
154             map {
155             $call_args{'Identities.member.' . $i++} = $_;
156             } @identities;
157             return $self->call( 'GetIdentityDkimAttributes', \%call_args );
158             }
159            
160             }
161              
162             1;
163              
164             __END__
165              
166             =head1 NAME
167              
168             Amazon::SES - Perl extension that implements Amazon Simple Email Service (SES) client
169              
170             =head1 SYNOPSIS
171              
172             use Amazon::SES;
173              
174             my $ses = Amazon::SES->new(access_key => '....', secret_key => '...');
175             # or
176             my $ses = Amazon::SES->new(use_iam_role => 1);
177              
178             my $r = $ses->send(
179             From => '[your SES identity]',
180             To => '[recipient]',
181             Subject => 'Hello World from SES',
182             Body => "Hello World"
183             );
184              
185             unless ( $r->is_success ) {
186             die "Could not deliver the message: " . $r->error_message;
187             }
188              
189             printf("Sent successfully. MessageID: %s\n", $r->message_id);
190              
191             ######### sending attachments
192             my $msg = MIME::Entity->build();
193             my $r = $ses->send( $msg );
194              
195             =head1 DESCRIPTION
196              
197             Implements Amazon Web Services' Simple Email Service (SES). Sess L<http://docs.aws.amazon.com/ses/latest/DeveloperGuide/Welcome.html> for details and to sign-up for the service. Forked from Net::AWS::SES, changed to use Moops and updated to support AWS signatures V4 and IAM Roles.
198              
199             =head1 GETTING STARTED
200              
201             After you sign-up for AWS SES service you need to create an C<IAM> credentials and create an C<access_key> and a C<secret_key>, which you will be needing to interface with the SES. Do not forget to grant permission to your C<IAM> to use SES. Read L<http://docs.aws.amazon.com/ses/latest/DeveloperGuide/using-credentials.html> for details.
202              
203             =head1 METHODS
204              
205             I attempted to make the method names as Perlish as possible, as opposed to direct copy/paste from the API reference. This way I felt you didn't have to be familiar with the full API reference in order to use the basic features of the service.
206              
207             If you are avid AWS developer there is a C<call()> method, which gives you access to all the documented Query actions of the AWS SES. In fact, that's what all the methods use to hide the complexity of the request/response. There are few examples of the C<call()> method in later sections.
208              
209             All the methods (including C<call()>) returns an instance of L<Response|Amazon::SES::Response>. You should check if the the call is success by testing for C<is_success> attribute of the response. If you want to gain full access to the raw parsed conents of the response I<(which originally is in XML, but we parse it into Perl hashref for you)>, C<result> attribute is all you will be needing. For the details see L<Response manual|Amazon::SES::Response>. Since C<result()> is the most important attribute of the resonse I will be giving you a sample result data in JSON notation for your reference.
210              
211             =head2 new(access_key => $key, secret_key => $s_key)
212              
213             =head2 new(access_key => $key, secret_key => $s_key, region => $region)
214              
215             =head2 new(use_iam_role => 1)
216              
217             Returns a Amazon::SES instance. C<access_key> and C<secret_key> arguments are optional if not specifying to C<use_iam_role>. C<region> is optional, and can be overriden in respective api calls. Must be a valid SES region: C<us-east-1>, C<us-west-2> or C<eu-west-1>. Default is C<us-east-1>. Must be your verified identity.
218              
219             =head2 send( $msg )
220              
221             =head2 send(%options)
222              
223             Sends an email address and returns L<Response|Amazon::SES::Response> instance.
224              
225             If the only argument is passed, it must be an instance of MIME::Entity. Example:
226              
227             $msg = MIME::Entity->build(
228             from => '[your address]',
229             to => '[your recipient]',
230             subject => 'MIME msg from AWS SES',
231             data => "<h1>Hello world from AWS SES</h1>",
232             type => 'text/html'
233             );
234              
235             $msg->attach(
236             Path => File::Spec->catfile( 't', 'image.gif' ),
237             Type => 'image/gif',
238             Encoding => 'base64'
239             );
240              
241             $ses = Amazon::SES->new(....);
242             $r = $ses->send($msg);
243              
244             unless ( $r->is_success ) {
245             die $r->error_message;
246             }
247              
248             If you don't have MIME::Entity instance handy you may use the following arguments to have AWS SES build the message for you (bold entries are required): C<From>, B<To>, B<Subject>, B<Body>, C<Body_html>, C<ReturnPath>. To send e-mail to multiple emails just pass an arrayref to C<To>.
249              
250             If C<From> is missing it defaults to your default e-mail given to C<new()>. Remember: this must be a verified e-mail. Example:
251              
252             $r = $ses->send(
253             from => '[your email address]',
254             to => '[destination email address]',
255             subject => 'Hello World'
256             body => 'Hello World'
257             );
258             unless ( $r->is_success ) {
259             die $r->error_message;
260             }
261              
262             You may provide an alternate html content by passing C<body_html> header.
263              
264             C<charset> of the e-mail is set to 'UTF-8'. As of this writing I didn't make any way to affect this.
265              
266             Success calls also return a C<message_id>, which can be accessed using a shortcut C<$r->message_id> syntax. See L<Response class|Amazon::SES::Response>.
267              
268             Sample successful response looks like this in JSON:
269              
270             {
271             "MessageId": "00000141344ce1a8-0664c3c5-e9a0-4b47-aa2e-12b0bdf6070e-000000"
272             }
273              
274             Sample error response looks like as:
275              
276             {
277             "Error": {
278             "Code": "MessageRejected",
279             "Type": "Sender",
280             "Message": "Email address is not verified."
281             },
282             "xmlns": "http://ses.amazonaws.com/doc/2010-12-01/",
283             "RequestId":"0d04b41a-20dd-11e3-b01b-51d07c103915"
284             }
285              
286              
287             =head2 verify_email($email)
288              
289             Verifies a given C<$email> with AWS SES. This results a verification e-mail be sent from AWS to the e-mail with a verification link, which must be clicked before this e-mail address appears in C<From> header. Returns a L<Response|Amazon::SES::Response> instance.
290              
291             Sample successful response:
292              
293             {} # right, it's empty.
294              
295             =head2 list_emails()
296              
297             Retrieves list e-mail addresses. Returns L<Response|Amazon::SES::Response> instance.
298              
299             Sample response:
300              
301             {
302             "Identities": ["example@example.com", "sample@example.com"]
303             }
304              
305             =head2 list_domains()
306              
307             Retrieves list of domains. Returns L<Response|Amazon::SES::Response> instance.
308              
309             {
310             "Identities": ["example1.com", "example2.com"]
311             }
312              
313             =head2 delete_email($email)
314              
315             =head2 delete_domain($domain)
316              
317             Deletes a given email or domain name from the SES. Once the identity is deleted you cannot use it in your C<From> headers. Returns L<Response|Amazon::SES::Response> instance.
318              
319             Sample response:
320              
321             { } # empty
322              
323              
324             =head2 get_quota()
325              
326             Gets your quota. Returns L<Response|Amazon::SES::Response> instance.
327              
328             Sample response:
329              
330             {
331             "Max24HourSend": "10000.0",
332             "MaxSendRate": "5.0",
333             "SentLast24Hours": "15.0"
334             }
335              
336              
337             =head2 get_statistics()
338              
339             Gets your usage statistics. Returns L<Response|Amazon::SES::Response> instance.
340              
341             Sample response:
342              
343             "SendDataPoints" : {
344             "member" : [
345             {
346             "Rejects" : "0",
347             "Timestamp" : "2013-09-14T13:07:00Z",
348             "Complaints" : "0",
349             "DeliveryAttempts" : "1",
350             "Bounces" : "0"
351             },
352             {
353             "Rejects" : "0",
354             "Timestamp" : "2013-09-17T09:37:00Z",
355             "Complaints" : "0",
356             "DeliveryAttempts" : "2",
357             "Bounces" : "0"
358             },
359             {
360             "Rejects" : "0",
361             "Timestamp" : "2013-09-17T10:07:00Z",
362             "Complaints" : "0",
363             "DeliveryAttempts" : "4",
364             "Bounces" : "0"
365             },
366             # ..................
367             ]
368             }
369              
370             =head2 get_dkim_attributes($email)
371              
372             =head2 get_dkim_attributes($domain)
373              
374             {
375             "DkimAttributes":[{
376             "entry":{
377             "value": {
378             "DkimEnabled":"true",
379             "DkimTokens":["iz26kxoyadfasfsafdsafg42jjh33gpcm","adtzf6s4edagadsfasdfsafsafr7rhvcf2c","yybjqlduafasfsafdsfc3a33dzqyyfr"],
380             "DkimVerificationStatus":"Success"
381             },
382             "key":"example@example.com"
383             }
384             }]
385             }
386              
387             =head1 ADVANCED API CALLS
388              
389             Methods documented in this library are shortcuts for C<call()> method, which is a direct interface to AWS SES. So if there is an API call that you need which does not have a shortcut here, use the C<call()> method instead. For example, instead of using C<send($message)> as above, you could've done:
390              
391             my $response = $self->call( 'SendRawEmail', {
392             'RawMessage.Data' => encode_base64( $msg->stringify )
393             } );
394              
395             Those of you who are familiar with SES API will notice that you didn't have to pass any C<Timestamp>, C<AccessKey>, or sign your message with your C<SecretKey>. This library does it for you. You just have to pass the data that is documented in the SES API reference.
396              
397             =head1 TODO
398              
399             =over 4
400              
401             =item *
402              
403             Ideally all API calls must returns their own respective responce instances, as opposed to a common L<Amazon::SES::Response|Amazon::SES::Response>.
404              
405             =item *
406              
407             All documented API queries must have respective methods in the library.
408              
409             =back
410              
411             =head1 SEE ALSO
412              
413             L<Net::AWS::SES> which this module was based on.
414              
415             L<JSON>, L<MIME::Base64>, L<Digest::HMAC_SHA1>, L<LWP::UserAgent>, L<Amazon::SES::Response>, L<XML::Simple>
416              
417             =head1 AUTHOR
418              
419             Rusty Conover rusty@luckydinosaur.com
420              
421             Sherzod B. Ruzmetov E<lt>sherzodr@cpan.orgE<gt>
422              
423             =head1 COPYRIGHT AND LICENSE
424              
425             Copyright (C) 2014 Lucky Dinosaur, LLC. http://www.luckydinosaur.com
426              
427             Portions Copyright (C) 2013 by L<Talibro LLC|https://www.talibro.com>
428              
429             This library is free software; you can redistribute it and/or modify
430             it under the same terms as Perl itself, either Perl version 5.14.2 or,
431             at your option, any later version of Perl 5 you may have available.
432              
433              
434             =cut