File Coverage

blib/lib/Email/Sender/Transport/SMTP.pm
Criterion Covered Total %
statement 27 111 24.3
branch 0 62 0.0
condition 0 18 0.0
subroutine 9 24 37.5
pod 1 7 14.2
total 37 222 16.6


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