File Coverage

blib/lib/Email/Sender/Transport/SMTPS.pm
Criterion Covered Total %
statement 18 80 22.5
branch 0 50 0.0
condition 0 12 0.0
subroutine 6 16 37.5
pod 0 3 0.0
total 24 161 14.9


line stmt bran cond sub pod time code
1             package Email::Sender::Transport::SMTPS;
2              
3 1     1   152137 use Moo;
  1         10415  
  1         5  
4 1     1   3718 use MooX::Types::MooseLike::Base qw(Bool Int Str);
  1         12075  
  1         124  
5             # ABSTRACT: Email::Sender joins Net::SMTPS
6              
7 1     1   891 use Email::Sender::Failure::Multi;
  1         77331  
  1         48  
8 1     1   796 use Email::Sender::Success::Partial;
  1         4423  
  1         48  
9 1     1   761 use Email::Sender::Util;
  1         8821  
  1         2112  
10             our $VERSION = '0.05';
11              
12             has host => (is => 'ro', isa => Str, default => sub { 'localhost' });
13             has ssl => (is => 'ro', isa => Str);
14             my $SSLArgs = sub {
15             my $ref = shift;
16             die "ssl_args must be a hash reference" unless ref($ref) eq 'HASH';
17             for my $key (keys %$ref) {
18             die "Invalid key in ssl_args: $key (must start with SSL_)" unless $key =~ /^SSL_/;
19             }
20             return $ref;
21             };
22             has ssl_args => (
23             is => 'ro',
24             isa => $SSLArgs,
25             default => sub { {} },
26             );
27             has port => (
28             is => 'ro',
29             isa => Int,
30             lazy => 1,
31             default => sub { return ($_[0]->ssl and $_[0]->ssl eq 'starttls') ? 587 : $_[0]->ssl ? 465 : 25; },
32             );
33              
34             has timeout => (is => 'ro', isa => Int, default => sub { 120 });
35              
36             has sasl_username => (is => 'ro', isa => Str);
37             has sasl_password => (is => 'ro', isa => Str);
38              
39             has allow_partial_success => (is => 'ro', isa => Bool, default => sub { 0 });
40              
41             has helo => (is => 'ro', isa => Str);
42             has localaddr => (is => 'ro');
43             has localport => (is => 'ro', isa => Int);
44             has debug => (is => 'ro', isa => Bool);
45              
46             # I am basically -sure- that this is wrong, but sending hundreds of millions of
47             # messages has shown that it is right enough. I will try to make it textbook
48             # later. -- rjbs, 2008-12-05
49             sub _quoteaddr {
50 0     0     my $addr = shift;
51 0           my @localparts = split /\@/, $addr;
52 0           my $domain = pop @localparts;
53 0           my $localpart = join q{@}, @localparts;
54              
55             # this is probably a little too paranoid
56 0 0 0       return $addr unless $localpart =~ /[^\w.+-]/ or $localpart =~ /^\./;
57 0           return join q{@}, qq("$localpart"), $domain;
58             }
59              
60             sub _smtp_client {
61 0     0     my ($self) = @_;
62              
63 0           my $class = "Net::SMTP";
64 0 0         if ($self->ssl) {
65 0           require Net::SMTPS;
66 0           $class = "Net::SMTPS";
67             } else {
68 0           require Net::SMTP;
69             }
70              
71 0           my $smtp = $class->new( $self->_net_smtp_args );
72              
73 0 0         $self->_throw("unable to establish SMTP connection") unless $smtp;
74              
75 0 0         if ($self->sasl_username) {
76 0 0         $self->_throw("sasl_username but no sasl_password")
77             unless defined $self->sasl_password;
78              
79 0 0         unless ($smtp->auth($self->sasl_username, $self->sasl_password)) {
80 0 0         if ($smtp->message =~ /MIME::Base64|Authen::SASL/) {
81 0           Carp::confess("SMTP auth requires MIME::Base64 and Authen::SASL");
82             }
83              
84 0           $self->_throw('failed AUTH', $smtp);
85             }
86             }
87              
88 0           return $smtp;
89             }
90              
91             sub _net_smtp_args {
92 0     0     my ($self) = @_;
93              
94             # compatible
95 0           my $ssl = $self->ssl;
96 0 0 0       $ssl = 'ssl' if $self->ssl and $self->ssl ne 'starttls';
97             return (
98             $self->host,
99             Port => $self->port,
100             Timeout => $self->timeout,
101             defined $ssl ? (doSSL => $ssl) : (),
102 0 0         defined $self->ssl_args ? %{ $self->ssl_args } : (),
  0 0          
    0          
    0          
    0          
    0          
103             defined $self->helo ? (Hello => $self->helo) : (),
104             defined $self->localaddr ? (LocalAddr => $self->localaddr) : (),
105             defined $self->localport ? (LocalPort => $self->localport) : (),
106             defined $self->debug ? (Debug => $self->debug) : (),
107             );
108             }
109              
110             sub _throw {
111 0     0     my ($self, @rest) = @_;
112 0           Email::Sender::Util->_failure(@rest)->throw;
113             }
114              
115             sub send_email {
116 0     0 0   my ($self, $email, $env) = @_;
117              
118             Email::Sender::Failure->throw("no valid addresses in recipient list")
119 0 0         unless my @to = grep { defined and length } @{ $env->{to} };
  0 0          
  0            
120              
121 0           my $smtp = $self->_smtp_client;
122              
123 0     0     my $FAULT = sub { $self->_throw($_[0], $smtp); };
  0            
124              
125 0 0         $smtp->mail(_quoteaddr($env->{from}))
126             or $FAULT->("$env->{from} failed after MAIL FROM:");
127              
128 0           my @failures;
129             my @ok_rcpts;
130              
131 0           for my $addr (@to) {
132 0 0         if ($smtp->to(_quoteaddr($addr))) {
133 0           push @ok_rcpts, $addr;
134             } else {
135             # my ($self, $error, $smtp, $error_class, @rest) = @_;
136 0           push @failures, Email::Sender::Util->_failure(
137             undef,
138             $smtp,
139             recipients => [ $addr ],
140             );
141             }
142             }
143              
144             # This logic used to include: or (@ok_rcpts == 1 and $ok_rcpts[0] eq '0')
145             # because if called without SkipBad, $smtp->to can return 1 or 0. This
146             # should not happen because we now always pass SkipBad and do the counting
147             # ourselves. Still, I've put this comment here (a) in memory of the
148             # suffering it caused to have to find that problem and (b) in case the
149             # original problem is more insidious than I thought! -- rjbs, 2008-12-05
150              
151 0 0 0       if (
      0        
152             @failures
153             and ((@ok_rcpts == 0) or (! $self->allow_partial_success))
154             ) {
155 0 0         $failures[0]->throw if @failures == 1;
156              
157 0 0         my $message = sprintf '%s recipients were rejected during RCPT',
158             @ok_rcpts ? 'some' : 'all';
159              
160 0           Email::Sender::Failure::Multi->throw(
161             message => $message,
162             failures => \@failures,
163             );
164             }
165              
166             # restore Pobox's support for streaming, code-based messages, and arrays here
167             # -- rjbs, 2008-12-04
168              
169 0 0         $smtp->data or $FAULT->("error at DATA start");
170              
171 0           my $msg_string = $email->as_string;
172 0           my $hunk_size = $self->_hunk_size;
173              
174 0           while (length $msg_string) {
175 0           my $next_hunk = substr $msg_string, 0, $hunk_size, '';
176 0 0         $smtp->datasend($next_hunk) or $FAULT->("error at during DATA");
177             }
178              
179 0 0         $smtp->dataend or $FAULT->("error at after DATA");
180              
181 0           my $message = $smtp->message;
182              
183 0           $self->_message_complete($smtp);
184              
185             # We must report partial success (failures) if applicable.
186 0 0         return $self->success({ message => $message }) unless @failures;
187 0           return $self->partial_success({
188             message => $message,
189             failure => Email::Sender::Failure::Multi->new({
190             message => 'some recipients were rejected during RCPT',
191             failures => \@failures
192             }),
193             });
194             }
195              
196 0     0     sub _hunk_size { 2**20 } # send messages to DATA in hunks of 1 mebibyte
197              
198             sub success {
199 0     0 0   my $self = shift;
200 0           my $success = Moo::Role->create_class_with_roles('Email::Sender::Success', 'Email::Sender::Role::HasMessage')->new(@_);
201             }
202              
203             sub partial_success {
204 0     0 0   my $self = shift;
205 0           my $partial_success = Moo::Role->create_class_with_roles('Email::Sender::Success::Partial', 'Email::Sender::Role::HasMessage')->new(@_);
206             }
207              
208 0     0     sub _message_complete { $_[1]->quit; }
209              
210             with 'Email::Sender::Transport';
211 1     1   10 no Moo;
  1         2  
  1         6  
212             1;
213             __END__