File Coverage

blib/lib/Email/Sender/Transport/SMTP.pm
Criterion Covered Total %
statement 27 103 26.2
branch 0 58 0.0
condition 0 12 0.0
subroutine 9 23 39.1
pod 1 7 14.2
total 37 203 18.2


line stmt bran cond sub pod time code
1             package Email::Sender::Transport::SMTP 2.500;
2             # ABSTRACT: send email over SMTP
3              
4 2     2   123687 use Moo;
  2         7  
  2         13  
5              
6 2     2   1599 use Email::Sender::Failure::Multi;
  2         6  
  2         67  
7 2     2   974 use Email::Sender::Success::Partial;
  2         6  
  2         60  
8 2     2   953 use Email::Sender::Role::HasMessage ();
  2         6  
  2         51  
9 2     2   481 use Email::Sender::Util;
  2         5  
  2         60  
10 2     2   12 use MooX::Types::MooseLike::Base qw(Bool Int Str HashRef);
  2         4  
  2         178  
11 2     2   1187 use Net::SMTP 3.07; # SSL support, fixed datasend
  2         196267  
  2         116  
12              
13 2     2   18 use utf8 (); # See below. -- rjbs, 2015-05-14
  2         4  
  2         4024  
14              
15             #pod =head1 DESCRIPTION
16             #pod
17             #pod This transport is used to send email over SMTP, either with or without secure
18             #pod sockets (SSL/TLS). It is one of the most complex transports available, capable
19             #pod of partial success.
20             #pod
21             #pod For a potentially more efficient version of this transport, see
22             #pod L.
23             #pod
24             #pod =head1 ATTRIBUTES
25             #pod
26             #pod The following attributes may be passed to the constructor:
27             #pod
28             #pod =over 4
29             #pod
30             #pod =item C: an arrayref of names of the host to try, in order; defaults to a single element array containing C
31             #pod
32             #pod The attribute C may be given, instead, which contains a single hostname.
33             #pod
34             #pod =item C: if 'starttls', use STARTTLS; if 'ssl' (or 1), connect securely;
35             #pod if 'maybestarttls', use STARTTLS if available; otherwise, no security
36             #pod
37             #pod =item C: passed to Net::SMTP constructor for 'ssl' connections or
38             #pod to starttls for 'starttls' or 'maybestarttls' connections; should contain extra
39             #pod options for IO::Socket::SSL
40             #pod
41             #pod =item C: port to connect to; defaults to 25 for non-SSL, 465 for 'ssl',
42             #pod 587 for 'starttls'
43             #pod
44             #pod =item C: maximum time in secs to wait for server; default is 120
45             #pod
46             #pod =cut
47              
48             sub BUILD {
49 0     0 0   my ($self) = @_;
50             Carp::croak("do not pass port number to SMTP transport in host, use port parameter")
51 0 0         if grep {; /:/ } $self->hosts;
  0            
52             }
53              
54             sub BUILDARGS {
55 0     0 0   my ($self, @rest) = @_;
56 0           my $arg = $self->SUPER::BUILDARGS(@rest);
57              
58 0 0         if (exists $arg->{host}) {
59             Carp::croak("can't pass both host and hosts to constructor")
60 0 0         if exists $arg->{hosts};
61              
62 0           $arg->{hosts} = [ delete $arg->{host} ];
63             }
64              
65 0           return $arg;
66             }
67              
68             has ssl => (is => 'ro', isa => Str, default => sub { 0 });
69              
70             has _hosts => (
71             is => 'ro',
72             isa => sub {
73             die "invalid hosts in Email::Sender::Transport::SMTP constructor"
74             unless defined $_[0]
75             && (ref $_[0] eq 'ARRAY')
76             && (grep {; length } @{ $_[0] }) > 0;
77             },
78             default => sub { [ 'localhost' ] },
79             init_arg => 'hosts',
80             );
81              
82 0     0 0   sub hosts { @{ $_[0]->_hosts } }
  0            
83              
84 0     0 1   sub host { $_[0]->_hosts->[0] }
85              
86             has _security => (
87             is => 'ro',
88             lazy => 1,
89             init_arg => undef,
90             default => sub {
91             my $ssl = $_[0]->ssl;
92             return '' unless $ssl;
93             $ssl = lc $ssl;
94             return 'starttls' if 'starttls' eq $ssl;
95             return 'maybestarttls' if 'maybestarttls' eq $ssl;
96             return 'ssl' if $ssl eq 1 or $ssl eq 'ssl';
97              
98             Carp::cluck(qq{"ssl" argument to Email::Sender::Transport::SMTP was "$ssl" rather than one of the permitted values: maybestarttls, starttls, ssl});
99              
100             return 1;
101             },
102             );
103              
104             has ssl_options => (is => 'ro', isa => HashRef, default => sub { {} });
105              
106             has port => (
107             is => 'ro',
108             isa => Int,
109             lazy => 1,
110             default => sub {
111             return $_[0]->_security eq 'starttls' ? 587
112             : $_[0]->_security eq 'ssl' ? 465
113             : 25
114             },
115             );
116              
117             has timeout => (is => 'ro', isa => Int, default => sub { 120 });
118              
119             #pod =item C: the username to use for auth; optional
120             #pod
121             #pod =item C: the password to use for auth; required if C is provided
122             #pod
123             #pod =item C: if true, will send data even if some recipients were rejected; defaults to false
124             #pod
125             #pod =cut
126              
127             has sasl_username => (is => 'ro', isa => Str);
128             has sasl_password => (is => 'ro', isa => Str);
129              
130             has allow_partial_success => (is => 'ro', isa => Bool, default => sub { 0 });
131              
132             #pod =item C: what to say when saying HELO; no default
133             #pod
134             #pod =item C: local address from which to connect
135             #pod
136             #pod =item C: local port from which to connect
137             #pod
138             #pod =cut
139              
140             has helo => (is => 'ro', isa => Str);
141             has localaddr => (is => 'ro');
142             has localport => (is => 'ro', isa => Int);
143              
144             #pod =item C: if true, put the L object in debug mode
145             #pod
146             #pod =back
147             #pod
148             #pod =cut
149              
150             has debug => (is => 'ro', isa => Bool, default => sub { 0 });
151              
152             # I am basically -sure- that this is wrong, but sending hundreds of millions of
153             # messages has shown that it is right enough. I will try to make it textbook
154             # later. -- rjbs, 2008-12-05
155             sub _quoteaddr {
156 0     0     my $addr = shift;
157 0           my @localparts = split /\@/, $addr;
158 0           my $domain = pop @localparts;
159 0           my $localpart = join q{@}, @localparts;
160              
161 0 0 0       return $addr # The first regex here is RFC 821 "specials" excepting dot.
      0        
162             unless $localpart =~ /[\x00-\x1F\x7F<>\(\)\[\]\\,;:@"]/
163             or $localpart =~ /^\./
164             or $localpart =~ /\.$/;
165 0           return join q{@}, qq("$localpart"), $domain;
166             }
167              
168             sub _smtp_client {
169 0     0     my ($self) = @_;
170              
171 0           my $class = "Net::SMTP";
172              
173 0           my $smtp = $class->new( $self->_net_smtp_args );
174              
175 0 0         unless ($smtp) {
176 0           $self->_throw(
177             sprintf "unable to establish SMTP connection to (%s) port %s",
178             (join q{, }, $self->hosts),
179             $self->port,
180             );
181             }
182              
183 0 0         if ($self->_security eq 'starttls') {
184             $self->_throw("can't STARTTLS: " . $smtp->message)
185 0 0         unless $smtp->starttls(%{ $self->ssl_options });
  0            
186             }
187              
188 0 0         if ($self->_security eq 'maybestarttls') {
189 0 0         if ( $smtp->supports('STARTTLS', 500, ["Command unknown: 'STARTTLS'"]) ) {
190             $self->_throw("can't STARTTLS: " . $smtp->message)
191 0 0         unless $smtp->starttls(%{ $self->ssl_options });
  0            
192             }
193             }
194              
195 0 0         if ($self->sasl_username) {
196 0 0         $self->_throw("sasl_username but no sasl_password")
197             unless defined $self->sasl_password;
198              
199 0 0         unless ($smtp->auth($self->sasl_username, $self->sasl_password)) {
200 0 0         if ($smtp->message =~ /MIME::Base64|Authen::SASL/) {
201 0           Carp::confess("SMTP auth requires MIME::Base64 and Authen::SASL");
202             }
203              
204 0           $self->_throw('failed AUTH', $smtp);
205             }
206             }
207              
208 0           return $smtp;
209             }
210              
211             sub _net_smtp_args {
212 0     0     my ($self) = @_;
213              
214             return (
215             [ $self->hosts ],
216             Port => $self->port,
217             Timeout => $self->timeout,
218             Debug => $self->debug,
219              
220             (($self->_security eq 'ssl')
221 0 0         ? (SSL => 1, %{ $self->ssl_options })
  0 0          
    0          
    0          
222             : ()),
223              
224             defined $self->helo ? (Hello => $self->helo) : (),
225             defined $self->localaddr ? (LocalAddr => $self->localaddr) : (),
226             defined $self->localport ? (LocalPort => $self->localport) : (),
227             );
228             }
229              
230             sub _throw {
231 0     0     my ($self, @rest) = @_;
232 0           Email::Sender::Util->_failure(@rest)->throw;
233             }
234              
235             sub send_email {
236 0     0 0   my ($self, $email, $env) = @_;
237              
238             Email::Sender::Failure->throw("no valid addresses in recipient list")
239 0 0         unless my @to = grep { defined and length } @{ $env->{to} };
  0 0          
  0            
240              
241 0           my $smtp = $self->_smtp_client;
242              
243 0     0     my $FAULT = sub { $self->_throw($_[0], $smtp); };
  0            
244              
245 0 0         $smtp->mail(_quoteaddr($env->{from}))
246             or $FAULT->("$env->{from} failed after MAIL FROM");
247              
248 0           my @failures;
249             my @ok_rcpts;
250              
251 0           for my $addr (@to) {
252 0 0         if ($smtp->to(_quoteaddr($addr))) {
253 0           push @ok_rcpts, $addr;
254             } else {
255             # my ($self, $error, $smtp, $error_class, @rest) = @_;
256 0           push @failures, Email::Sender::Util->_failure(
257             undef,
258             $smtp,
259             recipients => [ $addr ],
260             );
261             }
262             }
263              
264             # This logic used to include: or (@ok_rcpts == 1 and $ok_rcpts[0] eq '0')
265             # because if called without SkipBad, $smtp->to can return 1 or 0. This
266             # should not happen because we now always pass SkipBad and do the counting
267             # ourselves. Still, I've put this comment here (a) in memory of the
268             # suffering it caused to have to find that problem and (b) in case the
269             # original problem is more insidious than I thought! -- rjbs, 2008-12-05
270              
271 0 0 0       if (
      0        
272             @failures
273             and ((@ok_rcpts == 0) or (! $self->allow_partial_success))
274             ) {
275 0 0         $failures[0]->throw if @failures == 1;
276              
277 0 0         my $message = sprintf '%s recipients were rejected during RCPT',
278             @ok_rcpts ? 'some' : 'all';
279              
280 0           Email::Sender::Failure::Multi->throw(
281             message => $message,
282             failures => \@failures,
283             );
284             }
285              
286             # restore Pobox's support for streaming, code-based messages, and arrays here
287             # -- rjbs, 2008-12-04
288              
289 0 0         $smtp->data or $FAULT->("error at DATA start");
290              
291 0           my $msg_string = $email->as_string;
292 0           my $hunk_size = $self->_hunk_size;
293              
294 0           while (length $msg_string) {
295 0           my $next_hunk = substr $msg_string, 0, $hunk_size, '';
296              
297 0 0         $smtp->datasend($next_hunk) or $FAULT->("error at during DATA");
298             }
299              
300 0 0         $smtp->dataend or $FAULT->("error at after DATA");
301              
302 0           my $message = $smtp->message;
303              
304 0           $self->_message_complete($smtp);
305              
306             # We must report partial success (failures) if applicable.
307 0 0         return $self->success({ message => $message }) unless @failures;
308 0           return $self->partial_success({
309             message => $message,
310             failure => Email::Sender::Failure::Multi->new({
311             message => 'some recipients were rejected during RCPT',
312             failures => \@failures
313             }),
314             });
315             }
316              
317 0     0     sub _hunk_size { 2**20 } # send messages to DATA in hunks of 1 mebibyte
318              
319             sub success {
320 0     0 0   my $self = shift;
321 0           my $success = Moo::Role->create_class_with_roles('Email::Sender::Success', 'Email::Sender::Role::HasMessage')->new(@_);
322             }
323              
324             sub partial_success {
325 0     0 0   my $self = shift;
326 0           my $partial_success = Moo::Role->create_class_with_roles('Email::Sender::Success::Partial', 'Email::Sender::Role::HasMessage')->new(@_);
327             }
328              
329 0     0     sub _message_complete { $_[1]->quit; }
330              
331             #pod =head1 PARTIAL SUCCESS
332             #pod
333             #pod If C was set when creating the transport, the transport
334             #pod may return L objects. Consult that module's
335             #pod documentation.
336             #pod
337             #pod =cut
338              
339             with 'Email::Sender::Transport';
340 2     2   21 no Moo;
  2         4  
  2         16  
341             1;
342              
343             __END__