File Coverage

blib/lib/Email/Postman.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Email::Postman;
2 1     1   27288 use Carp;
  1         2  
  1         77  
3 1     1   549 use Moose;
  0            
  0            
4             use Log::Log4perl qw/:easy/;
5              
6             use Email::Abstract;
7             use Email::Address;
8             use Net::DNS;
9             use Net::SMTP;
10              
11             use Email::Postman::Report;
12              
13             unless( Log::Log4perl->initialized() ){
14             Log::Log4perl->easy_init($DEBUG);
15             }
16              
17             my $LOGGER = Log::Log4perl->get_logger();
18              
19             has 'dns_resolv' => ( is => 'ro' , isa => 'Net::DNS::Resolver', required => 1, lazy => 1 , builder => '_build_dns_resolv' );
20              
21             ## The sending domain.
22             has 'hello' => ( is => 'ro' , isa => 'Str', required => 1, default => 'localdomain');
23              
24             ## The sender.
25             has 'from' => ( is => 'ro' , isa => 'Str', required => 1, default => '"Local user" <localuser@localdomain>');
26              
27             has 'from_address' => ( is => 'ro' , isa => 'Str' , required => 1 , lazy => 1 , builder => '_build_from_address' );
28              
29             ## Just a flag.
30             has 'debug' => ( is => 'rw' , isa => 'Bool', required => 1 , default => 0);
31              
32             sub _build_dns_resolv{
33             my ($self) = @_;
34             return Net::DNS::Resolver->new();
35             }
36              
37             sub _build_from_address{
38             my ($self) = @_;
39             my ( $recpt , @rest ) = Email::Address->parse($self->from());
40             return $recpt->address();
41             }
42              
43              
44             =head1 NAME
45              
46             Email::Postman - Send multirecipient emails to the world.
47              
48             =head1 VERSION
49              
50             Version 0.03
51              
52             =cut
53              
54             our $VERSION = '0.03';
55              
56             sub deliver{
57             my ($self, $email) = @_;
58              
59             ## Make sure we have an email abstract.
60             unless( ( ref($email) || '' ) eq 'Email::Abstract' ){
61             $email = Email::Abstract->new($email);
62             }
63             ## We have an email abstract.
64             ## Make sure bccs are really blind.
65             my @bcc = $email->get_header('bcc');
66             $email->set_header('bcc');
67              
68             my @To = $email->get_header('To');
69             my @cc = $email->get_header('cc');
70              
71              
72             my @reports = ();
73              
74             ## Do the tos headers
75             foreach my $to ( @To ){
76             my @recpts = Email::Address->parse($to);
77             foreach my $recpt ( @recpts ){
78             my $report = $self->_deliver_email_to($email, $recpt);
79             $report->about_header('To');
80             push @reports , $report;
81             }
82             }
83              
84             ## Do the cc ones.
85             foreach my $to ( @cc ){
86             my @recpts = Email::Address->parse($to);
87             foreach my $recpt ( @recpts ){
88             my $report = $self->_deliver_email_to($email, $recpt);
89             $report->about_header('cc');
90             push @reports , $report;
91             }
92             }
93              
94             ## Do the Bcc ones.
95             foreach my $to ( @bcc ){
96             my @recpts = Email::Address->parse($to);
97             foreach my $recpt ( @recpts ){
98             $email->set_header('bcc' => $recpt->original() );
99             my $report = $self->_deliver_email_to($email, $recpt);
100             $report->about_header('bcc');
101             push @reports , $report;
102             }
103             }
104              
105             ## Reset the bcc to what they were.
106             $email->set_header('bcc', @bcc);
107              
108             return @reports;
109             }
110              
111              
112             ## Deliver to one and ONLY one recipient and return a report.
113             sub _deliver_email_to{
114             my ($self, $email , $recpt) = @_;
115             $LOGGER->debug("Delivering to '$recpt'");
116              
117             my $res = $self->dns_resolv();
118              
119             my $report = Email::Postman::Report->new({ about_email => $recpt->address() });
120              
121             my @mx = Net::DNS::mx( $res, $recpt->host());
122             unless( @mx ){
123             $report->set_failure_message("No MX host could be found for host '".$recpt->host()."'");
124             return $report;
125             }
126              
127             ## Try each mx and return on the first success.
128             foreach my $mx ( @mx ){
129             $report->reset();
130             my $exchange = $mx->exchange();
131             ## Works in taint mode.
132             ( $exchange ) = ( $exchange =~ m/(.+)/ );
133             $LOGGER->debug("Trying to deliver at ".$exchange);
134              
135             my $smtp = Net::SMTP->new($exchange,
136             Hello => $self->hello(),
137             Debug => $self->debug(),
138             Timeout => 5,
139             ExactAddresses => 1,
140             );
141             unless( $smtp ){
142             $report->set_failure_message("No SMTP for exchange '$exchange'");
143             $LOGGER->warn("Cannot build smtp for ".$exchange);
144             ## And jump to next. This MX could be down.
145             next;
146             }
147              
148             unless( $smtp->mail($self->from_address()) ){
149             $report->set_failure_message("SMTP MAIL failure for '".$self->from_address()."' : ".$smtp->message());
150             ## We trust ANY MX about this thing,
151             ## so we can just return the report. Same thing for any failures below.
152             return $report;
153             }
154             unless( $smtp->recipient($recpt->address()) ){
155             $report->set_failure_message("SMTP RECIPIENT failure for '".$recpt->address()."' : ".$smtp->message());
156             return $report;
157             }
158             unless( $smtp->data($email->as_string()) ){
159             $report->set_failure_message("SMTP DATA failure: ".$smtp->message());
160             return $report;
161             }
162             unless( $smtp->dataend() ){
163             $report->set_failure_message("SMTP DATAEND failure: ".$smtp->message());
164             return $report;
165             }
166              
167             unless( $smtp->quit() ){
168             $report->set_failure_message("SMTP QUIT failure: ".$smtp->message());
169             return $report;
170             }
171              
172             $report->success(1);
173             $report->message('Success');
174             ## No need to try anything else. That is a success!
175             return $report;
176             } ## End of MX loop.
177              
178             ## This is only in the case some MX are down
179             return $report;
180             }
181              
182             __PACKAGE__->meta->make_immutable();
183              
184             __END__
185              
186             =head1 SYNOPSIS
187              
188             my $postman = Email::Postman->new({ hello => 'my-domain.com', from => 'postmaster@domain.com' } );
189              
190             my $email = any Email::Abstract compatible email.
191              
192             my @reports = $postman->deliver($email);
193              
194             =head1 ATTRIBUTES
195              
196             =head2 hello
197              
198             The domain from which the emails will be sent. Defaults to 'localdomain'
199              
200             =head2 from
201              
202             The default 'from' ENVELOPPE email address. Defaults to 'localuser@localdomain'
203              
204             Note that this is NOT related to the 'From' header that your L<Email::Abstract> object should have.
205              
206             =head2 debug
207              
208             Just a debugging flag. Defaults to 0
209              
210             =head1 METHODS
211              
212             =head2 deliver
213              
214             Deliver the given email (something compatible with L<Email::Abstract> (or an email Abstract itself) to its recipients.
215             and returns an array of L<Email::Postman::Report> about the success/failures of email address the delivery was attempted.
216              
217             Note 1: This method will attempt to deliver the email using SMTP using a direct connection
218             to the MX records of the recipient's domains.
219              
220             Note 2: that this method CAN be slow, due to distant email servers response times. You are encouraged to
221             use this asynchronously.
222              
223             Usage:
224              
225             my @report = $this->deliver($email);
226              
227             =head1 AUTHOR
228              
229             Jerome Eteve, C<< <jerome.eteve at gmail.com> >>
230              
231             =head1 BUGS
232              
233             Please report any bugs or feature requests to C<bug-email-postman at rt.cpan.org>, or through
234             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Email-Postman>. I will be notified, and then you'll
235             automatically be notified of progress on your bug as I make changes.
236              
237              
238              
239              
240             =head1 SUPPORT
241              
242             You can find documentation for this module with the perldoc command.
243              
244             perldoc Email::Postman
245              
246              
247             You can also look for information at:
248              
249             =over 4
250              
251             =item * RT: CPAN's request tracker (report bugs here)
252              
253             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Email-Postman>
254              
255             =item * AnnoCPAN: Annotated CPAN documentation
256              
257             L<http://annocpan.org/dist/Email-Postman>
258              
259             =item * CPAN Ratings
260              
261             L<http://cpanratings.perl.org/d/Email-Postman>
262              
263             =item * Search CPAN
264              
265             L<http://search.cpan.org/dist/Email-Postman/>
266              
267             =back
268              
269              
270             =head1 ACKNOWLEDGEMENTS
271              
272              
273             =head1 LICENSE AND COPYRIGHT
274              
275             Copyright 2013 Jerome Eteve.
276              
277             This program is free software; you can redistribute it and/or modify it
278             under the terms of either: the GNU General Public License as published
279             by the Free Software Foundation; or the Artistic License.
280              
281             See http://dev.perl.org/licenses/ for more information.
282              
283              
284             =cut