File Coverage

blib/lib/Net/SMTP/Pipelining.pm
Criterion Covered Total %
statement 21 197 10.6
branch 0 36 0.0
condition 0 3 0.0
subroutine 7 18 38.8
pod 9 9 100.0
total 37 263 14.0


line stmt bran cond sub pod time code
1             package Net::SMTP::Pipelining;
2              
3 3     3   361208 use version; $VERSION = qv('0.0.4');
  3         5907  
  3         18  
4              
5 3     3   209 use strict;
  3         6  
  3         87  
6 3     3   13 use warnings;
  3         10  
  3         79  
7 3     3   1776 use Net::Cmd;
  3         12561  
  3         258  
8 3     3   1108 use IO::Socket;
  3         25634  
  3         26  
9              
10 3     3   2317 use base("Net::SMTP");
  3         6  
  3         3105  
11              
12             sub pipeline {
13 0     0 1   my ( $self, $mail ) = @_;
14              
15 0 0         if ( !defined( $self->supports("PIPELINING") ) ) {
16 0           my $message = qq(Server does not support PIPELINING, banner was ").$self->banner().qq(");
17 0           push @{ ${*$self}{'smtp_pipeline_errors'} },
  0            
  0            
18             {
19             command => "EHLO",
20             code => "",
21             message => $message,
22             };
23 0           warn $message;
24 0           return;
25             }
26              
27 0           my @rcpts =
28             ref( $mail->{to} ) eq ref( [] )
29 0 0         ? @{ $mail->{to} }
30             : $mail->{to};
31              
32 0           my @send = ( "MAIL FROM: " . $self->_addr( $mail->{mail} ) );
33 0           push @send, map { "RCPT TO: " . $self->_addr($_) } @rcpts;
  0            
34 0           push @send, "DATA";
35              
36 0           ${*$self}{'smtp_pipeline_pending_answers'} += scalar(@send);
  0            
37 0           delete ${*$self}{'net_cmd_last_ch'};
  0            
38 0           ${*$self}{'smtp_pipeline_errors'} = [];
  0            
39 0           ${*$self}{'smtp_pipeline_sent'} = [];
  0            
40              
41             # RFC 2920:
42             # "If nonblocking operation is not supported, however, client SMTP
43             # implementations MUST also check the TCP window size and make sure
44             # that each group of commands fits entirely within the window. The
45             # window size is usually, but not always, 4K octets. Failure to
46             # perform this check can lead to deadlock conditions."
47             #
48             # We do use non-blocking IO, but there doesn't seem to be a good
49             # way of obtaining the TCP window size from a socket. All the MTAs
50             # I've examined seem to ignore this issue, except for Postfix, which
51             # simply sets the send buffer to the attempted message length and
52             # dies on error. We'll report the error and abort.
53             #
54             # TODO: Add a test for this
55 0           my $length;
56 0           my $total_msg = join( "\015\012", @send ) . "\015\012";
57 3     3   23285 do { use bytes; $length = length($total_msg); };
  3         30  
  3         16  
  0            
  0            
58 0 0         if ( $length >= $self->sockopt(SO_SNDBUF) ) {
59 0 0         if ( !$self->sockopt( SO_SNDBUF, $length ) ) {
60 0           $self->reset();
61 0           push @{ ${*$self}{'smtp_pipeline_errors'} },
  0            
  0            
62             {
63             command => $total_msg,
64             code => 599,
65             message =>
66             "Message too large for TCP window and could not set SO_SNDBUF to length $length: $!"
67             };
68 0           push @{ ${*$self}{'smtp_pipeline_rcpts'}{'failed'} }, @rcpts;
  0            
  0            
69 0           return;
70             }
71             }
72 0           for (@send) {
73 0           $self->command($_);
74 0           push @{ ${*$self}{'smtp_pipeline_sent'} }, $_;
  0            
  0            
75             }
76 0           my $success = $self->_pipe_flush();
77 0           my @codes = @{ $self->pipe_codes() };
  0            
78              
79 0 0         my $prev_send = scalar(@codes) > scalar(@send) ? 1 : 0;
80              
81 0           my %failed;
82              
83 0           for my $i ( $prev_send .. $#codes ) {
84 0 0         my $exp = $i == $#codes ? 3 : 2;
85 0           my ($command) = ( $send[ $i - $prev_send ] =~ m/^(\w+)/ );
86 0 0         if ( $codes[$i] =~ m/^$exp/ ) {
87 0 0         if ( $command eq "RCPT" ) {
88 0           push @{ ${*$self}{'smtp_pipeline_rcpts'}{'accepted'} },
  0            
  0            
89             $rcpts[ $i - $prev_send - 1 ];
90             }
91             } else {
92              
93 0           push @{ ${*$self}{'smtp_pipeline_errors'} },
  0            
  0            
94             {
95             command => $send[ $i - $prev_send ],
96             code => $codes[$i],
97 0           message => ${*$self}{'smtp_pipeline_messages'}[$i],
98             };
99 0 0         if ( $command eq "RCPT" ) {
100 0           push @{ ${*$self}{'smtp_pipeline_rcpts'}{'failed'} },
  0            
  0            
101             $rcpts[ $i - $prev_send - 1 ];
102             } else {
103 0           $failed{$command} = $codes[$i];
104             }
105             }
106             }
107 0 0 0       if ( exists $failed{"MAIL"} || exists $failed{"DATA"} ) {
108 0           $self->reset();
109 0           return;
110             }
111              
112 0 0         if ( scalar @{ ${*$self}{'smtp_pipeline_rcpts'}{'failed'} } > 0 ) {
  0            
  0            
113 0 0         if ( scalar @{ ${*$self}{'smtp_pipeline_rcpts'}{'accepted'} } > 0 ) {
  0            
  0            
114 0           $success = undef;
115             } else {
116              
117             # From RFC 2920:
118             # "Client SMTP implementations that employ pipelining MUST check ALL
119             # statuses associated with each command in a group. For example, if
120             # none of the RCPT TO recipient addresses were accepted the client must
121             # then check the response to the DATA command -- the client cannot
122             # assume that the DATA command will be rejected just because none of
123             # the RCPT TO commands worked. If the DATA command was properly
124             # rejected the client SMTP can just issue RSET, but if the DATA command
125             # was accepted the client SMTP should send a single dot."
126             #
127             # Untested (because Net::Server::Mail doesn't apparently allow you
128             # to return a 354 after all recipients have failed), but this should work
129 0           $self->_pipe_dataend();
130 0           return;
131             }
132             }
133              
134 0 0         $success = $self->datasend( $mail->{data} ) ? $success : undef;
135              
136 0           push @{ ${*$self}{'smtp_pipeline_sent'} }, $mail->{data};
  0            
  0            
137              
138 0           ${*$self}{'smtp_pipeline_pending_answers'}++;
  0            
139              
140 0           $self->_pipe_dataend();
141              
142 0           return $success;
143             }
144              
145             sub pipe_recipients {
146 0     0 1   return ${ *{ $_[0] } }{'smtp_pipeline_rcpts'};
  0            
  0            
147             }
148              
149             sub pipe_rcpts_failed {
150 0     0 1   return ${ *{ $_[0] } }{'smtp_pipeline_rcpts'}{'failed'};
  0            
  0            
151             }
152              
153             sub pipe_rcpts_succeeded {
154 0     0 1   return ${ *{ $_[0] } }{'smtp_pipeline_rcpts'}{'succeeded'};
  0            
  0            
155             }
156              
157             sub pipe_sent {
158 0     0 1   return ${ *{ $_[0] } }{'smtp_pipeline_sent'};
  0            
  0            
159             }
160              
161             sub pipe_errors {
162 0     0 1   return ${ *{ $_[0] } }{'smtp_pipeline_errors'};
  0            
  0            
163             }
164              
165             sub _pipe_dataend {
166 0     0     my $self = shift;
167 0           my $end = "\015\012.\015\012";
168              
169             # TODO: add test for failed write
170 0 0         syswrite( $self, $end, 5 ) or warn "Last character not sent: $!";
171 0 0         $self->debug_print( 1, ".\n" )
172             if ( $self->debug );
173             }
174              
175             sub pipe_flush {
176 0     0 1   my $self = shift;
177 0           ${*$self}{'smtp_pipeline_sent'} = [];
  0            
178 0           $self->_pipe_flush();
179             }
180              
181             sub _pipe_flush {
182 0     0     my $self = shift;
183              
184 0           ${*$self}{'smtp_pipeline_messages'} = [];
  0            
185 0           ${*$self}{'smtp_pipeline_codes'} = [];
  0            
186 0           ${*$self}{'net_cmd_resp'} = [];
  0            
187              
188 0           while (
189 0           scalar @{ ${*$self}{'smtp_pipeline_messages'} }
  0            
  0            
190             < ${*$self}{'smtp_pipeline_pending_answers'} )
191             {
192 0           $self->response();
193 0           push @{ ${*$self}{'smtp_pipeline_messages'} }, [ $self->message() ];
  0            
  0            
194 0           push @{ ${*$self}{'smtp_pipeline_codes'} }, $self->code();
  0            
  0            
195 0           ${*$self}{'net_cmd_resp'} = [];
  0            
196             }
197 0           push @{ ${*$self}{'net_cmd_resp'} },
  0            
  0            
198 0           ${*$self}{'smtp_pipeline_messages'}[-1];
199 0           ${*$self}{'smtp_pipeline_pending_answers'} = 0;
  0            
200 0           delete ${*$self}{'net_cmd_last_ch'};
  0            
201              
202 0 0         if (scalar( @{ $self->pipe_codes() } )
  0            
  0            
203             > scalar( @{ $self->pipe_sent() } ) )
204             {
205 0           my $prev_code = ${*$self}{'smtp_pipeline_codes'}[0];
  0            
206 0           my $prev_resp = ${*$self}{'smtp_pipeline_messages'}[0];
  0            
207 0 0         if ( $prev_code =~ m/^2/ ) {
208 0           @{ ${*$self}{'smtp_pipeline_rcpts'}{'succeeded'} } =
  0            
  0            
209 0           @{ ${*$self}{'smtp_pipeline_rcpts'}{'accepted'} };
  0            
210 0           ${*$self}{'smtp_pipeline_rcpts'}{'failed'} = [];
  0            
211             } else {
212 0           ${*$self}{'smtp_pipeline_rcpts'}{'succeeded'} = [];
  0            
213 0           @{ ${*$self}{'smtp_pipeline_rcpts'}{'failed'} } =
  0            
  0            
214 0           @{ ${*$self}{'smtp_pipeline_rcpts'}{'accepted'} };
  0            
215 0           push @{ ${*$self}{'smtp_pipeline_errors'} },
  0            
  0            
216             {
217             command => "DATA",
218             code => $prev_code,
219             message => $prev_resp,
220             };
221             }
222             } else {
223 0           ${*$self}{'smtp_pipeline_rcpts'}{'failed'} = [];
  0            
224 0           ${*$self}{'smtp_pipeline_rcpts'}{'succeeded'} = [];
  0            
225             }
226 0           ${*$self}{'smtp_pipeline_rcpts'}{'accepted'} = [];
  0            
227              
228 0 0         if ( grep { $_ !~ /^[23]/ } @{ $self->pipe_codes() } ) {
  0            
  0            
229 0           return;
230             } else {
231 0           return 1;
232             }
233             }
234              
235             sub pipe_codes {
236 0     0 1   my $self = shift;
237 0           ${*$self}{'smtp_pipeline_codes'};
  0            
238             }
239              
240             sub pipe_messages {
241 0     0 1   my $self = shift;
242 0           ${*$self}{'smtp_pipeline_messages'};
  0            
243             }
244              
245             1; # Magic true value required at end of module
246             __END__