File Coverage

blib/lib/Mojo/SMTP/Client.pm
Criterion Covered Total %
statement 301 314 95.8
branch 74 96 77.0
condition 10 14 71.4
subroutine 60 62 96.7
pod 3 3 100.0
total 448 489 91.6


line stmt bran cond sub pod time code
1             package Mojo::SMTP::Client;
2              
3 3     3   454547 use Mojo::Base 'Mojo::EventEmitter';
  3         338169  
  3         19  
4 3     3   4314 use Mojo::IOLoop;
  3         290579  
  3         16  
5 3     3   136 use Mojo::IOLoop::Client;
  3         5  
  3         17  
6 3     3   1387 use Mojo::IOLoop::Delay;
  3         2687  
  3         24  
7 3     3   109 use Mojo::IOLoop::Stream;
  3         6  
  3         22  
8 3     3   70 use Mojo::Util 'b64_encode';
  3         5  
  3         143  
9 3     3   1255 use Mojo::SMTP::Client::Response;
  3         8  
  3         18  
10 3     3   1429 use Mojo::SMTP::Client::Exception;
  3         8  
  3         138  
11 3     3   32 use Scalar::Util 'weaken';
  3         6  
  3         117  
12 3     3   14 use Carp;
  3         5  
  3         252  
13              
14             our $VERSION = '0.19';
15              
16             use constant {
17 3         13921 CMD_OK => 2,
18             CMD_MORE => 3,
19            
20             CMD_CONNECT => 1,
21             CMD_EHLO => 2,
22             CMD_HELO => 3,
23             CMD_STARTTLS => 10,
24             CMD_AUTH => 11,
25             CMD_FROM => 4,
26             CMD_TO => 5,
27             CMD_DATA => 6,
28             CMD_DATA_END => 7,
29             CMD_RESET => 8,
30             CMD_QUIT => 9,
31 3     3   17 };
  3         4  
32              
33             our %CMD = (
34             &CMD_CONNECT => 'CMD_CONNECT',
35             &CMD_EHLO => 'CMD_EHLO',
36             &CMD_HELO => 'CMD_HELO',
37             &CMD_STARTTLS => 'CMD_STARTTLS',
38             &CMD_AUTH => 'CMD_AUTH',
39             &CMD_FROM => 'CMD_FROM',
40             &CMD_TO => 'CMD_TO',
41             &CMD_DATA => 'CMD_DATA',
42             &CMD_DATA_END => 'CMD_DATA_END',
43             &CMD_RESET => 'CMD_RESET',
44             &CMD_QUIT => 'CMD_QUIT',
45             );
46              
47             has address => 'localhost';
48             has port => sub { $_[0]->tls ? 465 : 25 };
49             has tls => 0;
50             has 'tls_ca';
51             has 'tls_cert';
52             has 'tls_key';
53             has tls_verify => 1;
54             has hello => 'localhost.localdomain';
55             has connect_timeout => sub { $ENV{MOJO_CONNECT_TIMEOUT} || 10 };
56             has inactivity_timeout => sub { $ENV{MOJO_INACTIVITY_TIMEOUT} // 20 };
57             has ioloop => sub { Mojo::IOLoop->new };
58             has autodie => 0;
59              
60             sub new {
61 15     15 1 50398 my $class = shift;
62            
63 15         522 my $self = $class->SUPER::new(@_);
64 15         1008 weaken(my $this = $self);
65            
66             $self->{resp_checker} = sub {
67 84     84   1797 my ($delay, $resp) = @_;
68 84         320 $this->emit(response => $this->{last_cmd}, $resp);
69            
70 82 100       5179 unless (substr($resp->code, 0, 1) == $this->{expected_code}) {
71 2         52 die $resp->error(Mojo::SMTP::Client::Exception::Response->new($resp->message)->code($resp->code));
72             }
73 80         314 $delay->pass($resp);
74 15         898 };
75            
76 15         111 $self->{cmds} = [];
77            
78 15         371 $self;
79             }
80              
81             sub send {
82 17     17 1 204356 my $self = shift;
83 17 100       181 my $cb = @_ % 2 == 0 ? undef : pop;
84            
85 17         91 my @steps;
86 17 100       131 $self->{nb} = $cb ? 1 : 0;
87            
88             # user changed SMTP server or server sent smth while it shouldn't
89 17 50 33     147 if ($self->{stream} && (($self->{server} ne $self->_server) ||
      66        
90             ($self->{stream}->is_readable && !$self->{starttls} && !$self->{authorized} &&
91             grep {$self->{last_cmd} == $_} (CMD_CONNECT, CMD_DATA_END, CMD_RESET)))
92             ) {
93 1         8 $self->_rm_stream();
94             }
95            
96 17 100       474 unless ($self->{stream}) {
97             push @steps, sub {
98 16     16   6001 my $delay = shift;
99             # connect
100 16         51 $self->{starttls} = $self->{authorized} = 0;
101 16         106 $self->emit('start');
102 15         294 $self->{server} = $self->_server;
103 15         533 $self->{last_cmd} = CMD_CONNECT;
104            
105 15         49 my $connect_cb = $delay->begin;
106 15         167 $self->{client} = Mojo::IOLoop::Client->new(reactor => $self->_ioloop->reactor);
107 15         1442 $self->{client}->on(connect => $connect_cb);
108 15         200 $self->{client}->on(error => $connect_cb);
109             $self->{client}->connect(
110 15         161 address => $self->address,
111             port => $self->port,
112             timeout => $self->connect_timeout,
113             tls => $self->tls,
114             tls_ca => $self->tls_ca,
115             tls_cert => $self->tls_cert,
116             tls_key => $self->tls_key,
117             tls_options => { SSL_verify_mode => $self->tls_verify },
118             );
119             },
120             sub {
121             # read response
122 15     15   36505 my $delay = shift;
123 15         39 delete $self->{client};
124             # check is this a handle
125 15 50       29 Mojo::SMTP::Client::Exception::Stream->throw($_[0]) unless eval { *{$_[0]} };
  15         78  
  15         85  
126            
127 15         113 $self->_make_stream($_[0], $self->_ioloop);
128 15         127 $self->_read_response($delay->begin);
129 15         238 $self->{expected_code} = CMD_OK;
130             },
131             # check response
132 16         473 $self->{resp_checker};
133            
134 16 100 100     288 if (!@_ || $_[0] ne 'hello') {
135 14         167 unshift @_, hello => $self->hello;
136             }
137             }
138             else {
139 1         5 $self->{stream}->start;
140             }
141            
142 17         268 push @{$self->{cmds}}, @_;
  17         277  
143 17         189 push @steps, $self->_make_cmd_steps();
144            
145             # non-blocking
146 17         606 my $delay = $self->{delay} = Mojo::IOLoop::Delay->new->ioloop($self->_ioloop)->steps(@steps);
147             $self->{finally} = sub {
148 17 100   17   3594 shift if @_ == 2; # delay
149            
150 17 50       82 if ($cb) {
151 17         37 my $r = $_[0];
152 17 100       144 unless ($r->isa('Mojo::SMTP::Client::Response')) {
153             # some error occured, which throwed an exception
154 5         57 $r = Mojo::SMTP::Client::Response->new('', error => $r);
155             }
156            
157 17         68 delete $self->{delay};
158 17         62 delete $self->{finally};
159            
160 17         65 $cb->($self, $r);
161 17         4934 $cb = undef;
162             }
163 17         5197 };
164 17         240 $delay->catch($self->{finally});
165            
166             # blocking
167 17         2056 my $resp;
168 17 100       101 unless ($self->{nb}) {
169             $cb = sub {
170 4     4   9 $resp = pop;
171 4         55 };
172 4         47 $delay->wait;
173 4 100 66     2814 return $self->autodie && $resp->error ? die $resp->error : $resp;
174             }
175             }
176              
177             sub prepend_cmd {
178 1     1 1 30 my $self = shift;
179 1 50       26 croak "no active `send' calls" unless exists $self->{delay};
180            
181 1         14 unshift @{ $self->{cmds} }, @_;
  1         16  
182             }
183              
184             sub _ioloop {
185 49     49   2347 my ($self) = @_;
186 49 100       508 return $self->{nb} ? Mojo::IOLoop->singleton : $self->ioloop;
187             }
188              
189             sub _server {
190 17     17   36 my $self = shift;
191 17         102 return $self->address.':'.$self->port.':'.$self->tls;
192             }
193              
194             sub _make_stream {
195 17     17   114 my ($self, $sock, $loop) = @_;
196            
197 17         122 weaken $self;
198             my $error_handler = sub {
199 2 50   2   111 delete($self->{cleanup_cb})->() if $self->{cleanup_cb};
200 2         31 $self->_rm_stream();
201            
202 2         68 $self->{delay}->reject($_[0]);
203 17         157 };
204            
205 17         433 $self->{stream} = Mojo::IOLoop::Stream->new($sock);
206 17         1301 $self->{stream}->reactor($loop->reactor);
207 17         392 $self->{stream}->start;
208             $self->{stream}->on(timeout => sub {
209 2     2   977960 $error_handler->(Mojo::SMTP::Client::Exception::Stream->new('Inactivity timeout'));
210 17         2332 });
211             $self->{stream}->on(error => sub {
212 0     0   0 $error_handler->(Mojo::SMTP::Client::Exception::Stream->new($_[-1]));
213 17         259 });
214             $self->{stream}->on(close => sub {
215 0     0   0 $error_handler->(Mojo::SMTP::Client::Exception::Stream->new('Socket closed unexpectedly by remote side'));
216 17         180 });
217             }
218              
219             sub _make_cmd_steps {
220 72     72   168 my ($self) = @_;
221            
222 72         175 my ($cmd, $arg) = splice @{ $self->{cmds} }, 0, 2;
  72         356  
223 72 100       323 unless ($cmd) {
224             # no more commands
225 11 100       35 if ($self->{stream}) {
226 4         16 $self->{stream}->timeout(0);
227 4         92 $self->{stream}->stop;
228             }
229 11         193 return $self->{finally};
230             }
231            
232 61 50       553 if ( my $sub = $self->can("_cmd_$cmd") ) {
233             return (
234             $self->$sub($arg), sub {
235 55     55   8803 my ($delay, $resp) = @_;
236            
237 55         192 $delay->pass($resp);
238 55         1038 $delay->steps( $self->_make_cmd_steps() );
239             }
240 61         276 );
241             }
242            
243 0         0 croak 'unrecognized command: ', $cmd;
244             }
245              
246             # EHLO/HELO
247             sub _cmd_hello {
248 16     16   58 my ($self, $arg) = @_;
249 16         101 weaken $self;
250            
251             return (
252             sub {
253 13     13   2972 my $delay = shift;
254 13         158 $self->_write_cmd('EHLO ' . $arg, CMD_EHLO);
255 13         365 $self->_read_response($delay->begin);
256 13         91 $self->{expected_code} = CMD_OK;
257             },
258             sub {
259 13     13   331 eval { $self->{resp_checker}->(@_); $_[1]->{checked} = 1 };
  13         47  
  10         244  
260 13 100       645 if (my $e = $@) {
261 3 100       67 die $e unless $e->isa('Mojo::SMTP::Client::Response');
262 1         9 my $delay = shift;
263            
264 1         9 $self->_write_cmd('HELO ' . $arg, CMD_HELO);
265 1         48 $self->_read_response($delay->begin);
266             }
267             },
268             sub {
269 11     11   1784 my ($delay, $resp) = @_;
270 11 100       45 return $delay->pass($resp) if delete $resp->{checked};
271 1         23 $self->{resp_checker}->($delay, $resp);
272             }
273 16         865 );
274             }
275              
276             # STARTTLS
277             sub _cmd_starttls {
278 2     2   13 my ($self, $arg) = @_;
279 2         17 weaken $self;
280            
281 2 50       186 require IO::Socket::SSL and IO::Socket::SSL->VERSION(0.98);
282            
283             return (
284             sub {
285 2     2   197 my $delay = shift;
286 2         24 $self->_write_cmd('STARTTLS', CMD_STARTTLS);
287 2         77 $self->_read_response($delay->begin);
288 2         16 $self->{expected_code} = CMD_OK;
289             },
290             $self->{resp_checker},
291             sub {
292 2     2   380 my ($delay, $resp) = @_;
293 2         20 $self->{stream}->stop;
294 2         86 $self->{stream}->timeout(0);
295            
296 2         49 my ($tls_cb, $tid, $loop, $sock);
297            
298             my $error_handler = sub {
299 0         0 $loop->remove($tid);
300 0         0 $loop->reactor->remove($sock);
301 0         0 $sock = undef;
302 0 0       0 $tls_cb->($delay, undef, @_>=2 ? $_[1] : 'Inactivity timeout');
303 0         0 $tls_cb = $delay = undef;
304 2         26 };
305            
306             $sock = IO::Socket::SSL->start_SSL(
307             $self->{stream}->steal_handle,
308 2 50       27 SSL_ca_file => $self->tls_ca,
    50          
309             SSL_cert_file => $self->tls_cert,
310             SSL_key_file => $self->tls_key,
311             SSL_verify_mode => $self->tls_verify,
312             SSL_verifycn_name => $self->address,
313             SSL_verifycn_scheme => $self->tls_ca ? 'smtp' : undef,
314             SSL_startHandshake => 0,
315             SSL_error_trap => $error_handler
316             )
317             or return $delay->pass(0, $IO::Socket::SSL::SSL_ERROR);
318            
319 2         3740 $tls_cb = $delay->begin;
320 2         33 $loop = $self->_ioloop;
321            
322 2         23 $tid = $loop->timer($self->inactivity_timeout => $error_handler);
323            
324             $loop->reactor->io($sock => sub {
325 6 100       4241 if ($sock->connect_SSL) {
326 2         406 $loop->remove($tid);
327 2         138 $loop->reactor->remove($sock);
328 2         99 $self->_make_stream($sock, $loop);
329 2         11 $self->{starttls} = 1;
330 2         16 $sock = $loop = undef;
331 2         10 $tls_cb->($delay, $resp);
332 2         206 $tls_cb = $delay = undef;
333 2         4 return;
334             }
335            
336 4 50       2402 return $loop->reactor->watch($sock, 1, 0)
337             if $IO::Socket::SSL::SSL_ERROR == IO::Socket::SSL::SSL_WANT_READ();
338 0 0       0 return $loop->reactor->watch($sock, 0, 1)
339             if $IO::Socket::SSL::SSL_ERROR == IO::Socket::SSL::SSL_WANT_WRITE();
340            
341 2         222 })->watch($sock, 0, 1);
342             },
343             sub {
344 2     2   95 my ($delay, $resp, $error) = @_;
345 2 50       20 unless ($resp) {
346 0         0 $self->_rm_stream();
347 0         0 Mojo::SMTP::Client::Exception::Stream->throw($error);
348             }
349            
350 2         8 $delay->pass($resp);
351             }
352 2         120 );
353             }
354              
355             # AUTH
356             sub _cmd_auth {
357 2     2   18 my ($self, $arg) = @_;
358 2         9 weaken $self;
359            
360 2   100     46 my $type = lc($arg->{type} // 'plain');
361            
362             my $set_auth_ok = sub {
363 2     2   397 my ($delay, $resp) = @_;
364 2         7 $self->{authorized} = 1;
365 2         34 $delay->pass($resp);
366 2         51 };
367            
368 2 100       10 if ($type eq 'plain') {
369             return (
370             sub {
371 1     1   71 my $delay = shift;
372 1         13 $self->_write_cmd('AUTH PLAIN '.b64_encode(join("\0", '', $arg->{login}, $arg->{password}), ''), CMD_AUTH);
373 1         34 $self->_read_response($delay->begin);
374 1         10 $self->{expected_code} = CMD_OK;
375             },
376             $self->{resp_checker},
377 1         22 $set_auth_ok
378             );
379             }
380            
381 1 50       33 if ($type eq 'login') {
382             return (
383             # start auth
384             sub {
385 1     1   67 my $delay = shift;
386 1         12 $self->_write_cmd('AUTH LOGIN', CMD_AUTH);
387 1         32 $self->_read_response($delay->begin);
388 1         7 $self->{expected_code} = CMD_MORE;
389             },
390             $self->{resp_checker},
391             # send username
392             sub {
393 1     1   185 my $delay = shift;
394 1         23 $self->_write_cmd(b64_encode($arg->{login}, ''), CMD_AUTH);
395 1         33 $self->_read_response($delay->begin);
396 1         7 $self->{expected_code} = CMD_MORE;
397             },
398             $self->{resp_checker},
399             # send password
400             sub {
401 1     1   180 my $delay = shift;
402 1         17 $self->_write_cmd(b64_encode($arg->{password}, ''), CMD_AUTH);
403 1         33 $self->_read_response($delay->begin);
404 1         7 $self->{expected_code} = CMD_OK;
405             },
406             $self->{resp_checker},
407 1         48 $set_auth_ok
408             );
409             }
410            
411 0         0 croak 'unrecognized auth method: ', $type;
412             }
413              
414             # FROM
415             sub _cmd_from {
416 12     12   26 my ($self, $arg) = @_;
417 12         39 weaken $self;
418            
419             return (
420             sub {
421 12     12   1068 my $delay = shift;
422 12         58 $self->_write_cmd('MAIL FROM:<'.$arg.'>', CMD_FROM);
423 12         360 $self->_read_response($delay->begin);
424 12         106 $self->{expected_code} = CMD_OK;
425             },
426             $self->{resp_checker}
427 12         262 );
428             }
429              
430             # TO
431             sub _cmd_to {
432 13     13   31 my ($self, $arg) = @_;
433 13         45 weaken $self;
434            
435 13         18 my @steps;
436            
437 13 100       52 for my $to (ref $arg ? @$arg : $arg) {
438             push @steps, sub {
439 16     16   1996 my $delay = shift;
440 16         72 $self->_write_cmd('RCPT TO:<'.$to.'>', CMD_TO);
441 16         451 $self->_read_response($delay->begin);
442 16         115 $self->{expected_code} = CMD_OK;
443             },
444             $self->{resp_checker}
445 16         167 }
446            
447 13         88 return @steps;
448             }
449              
450             # DATA
451             sub _cmd_data {
452 7     7   17 my ($self, $arg) = @_;
453 7         41 weaken $self;
454            
455 7         12 my @steps;
456            
457             push @steps, sub {
458 7     7   749 my $delay = shift;
459 7         37 $self->_write_cmd('DATA', CMD_DATA);
460 7         224 $self->_read_response($delay->begin);
461 7         59 $self->{expected_code} = CMD_MORE;
462             },
463 7         89 $self->{resp_checker};
464            
465 7 100       34 if (ref $arg eq 'CODE') {
466 2         9 my ($data_writer, $data_writer_cb);
467 2         0 my $was_nl;
468 2         0 my $last_ch;
469            
470             $data_writer = sub {
471 44     44   18224 my $delay = shift;
472 44 100       102 unless ($data_writer_cb) {
473 2         22 $data_writer_cb = $delay->begin;
474             $self->{cleanup_cb} = sub {
475 2         10 undef $data_writer;
476 2         54 };
477             }
478            
479 44         97 my $data = $arg->();
480 44 100       399 $data = $$data if ref $data;
481            
482 44 100       94 unless (length($data) > 0) {
483 2 50       15 $self->_write_cmd(($was_nl ? '' : Mojo::SMTP::Client::Response::CRLF).'.', CMD_DATA_END);
484 2         49 $self->_read_response($data_writer_cb);
485 2         15 $self->{expected_code} = CMD_OK;
486 2         6 return delete($self->{cleanup_cb})->();
487             }
488             # The following part if heavily inspired by Net::Cmd
489 42         54 my $first_ch = '';
490             # We have not send anything yet, so last_ch = "\012" means we are at the start of a line (^. -> ..)
491 42 100       80 $last_ch = "\012" unless defined $last_ch;
492 42 100       97 if ($last_ch eq "\015") {
    100          
493             # Remove \012 so it does not get prefixed with another \015 below
494             # and escape the . if there is one following it because the fixup
495             # below will not find it
496 2 50       48 $first_ch = "\012" if $data =~ s/^\012(\.?)/$1$1/;
497             }
498             elsif ($last_ch eq "\012") {
499             # Fixup below will not find the . as the first character of the buffer
500 7 100       25 $first_ch = "." if $data =~ /^\./;
501             }
502 42         293 $data =~ s/\015?\012(\.?)/\015\012$1$1/g;
503 42         76 substr($data, 0, 0) = $first_ch;
504 42         61 $last_ch = substr($data, -1, 1);
505 42         74 $was_nl = _has_nl($data);
506 42         109 $self->{stream}->write($data, $data_writer);
507 2         38 };
508            
509 2         14 push @steps, $data_writer, $self->{resp_checker};
510             }
511             else {
512             push @steps, sub {
513 5     5   962 my $delay = shift;
514 5 50       89 (ref $arg ? $$arg : $arg) =~ s/\015?\012(\.?)/\015\012$1$1/g; # turn . into .. if it's first character of the line and normalize newline
515 5 50       41 $self->{stream}->write(ref $arg ? $$arg : $arg, $delay->begin);
516             },
517             sub {
518 5     5   1768 my $delay = shift;
519 5 50       29 $self->_write_cmd((_has_nl($arg) ? '' : Mojo::SMTP::Client::Response::CRLF).'.', CMD_DATA_END);
520 5         139 $self->_read_response($delay->begin);
521 5         74 $self->{expected_code} = CMD_OK;
522             },
523             $self->{resp_checker}
524 5         108 }
525            
526 7         58 return @steps;
527             }
528              
529             # RESET
530             sub _cmd_reset {
531 2     2   23 my ($self, $arg) = @_;
532 2         25 weaken $self;
533            
534             return (
535             sub {
536 2     2   257 my $delay = shift;
537 2         15 $self->_write_cmd('RSET', CMD_RESET);
538 2         47 $self->_read_response($delay->begin);
539 2         29 $self->{expected_code} = CMD_OK;
540             },
541             $self->{resp_checker}
542 2         58 );
543             }
544              
545             # QUIT
546             sub _cmd_quit {
547 7     7   17 my ($self, $arg) = @_;
548 7         26 weaken $self;
549            
550             return (
551             sub {
552 7     7   768 my $delay = shift;
553 7         23 $self->_write_cmd('QUIT', CMD_QUIT);
554 7         226 $self->_read_response($delay->begin);
555 7         52 $self->{expected_code} = CMD_OK;
556             },
557             $self->{resp_checker}, sub {
558 7     7   1326 my $delay = shift;
559 7         29 $self->_rm_stream();
560 7         1850 $delay->pass(@_);
561             }
562 7         269 );
563             }
564              
565             sub _write_cmd {
566 71     71   186 my ($self, $cmd, $cmd_const) = @_;
567 71         130 $self->{last_cmd} = $cmd_const;
568 71         290 $self->{stream}->write($cmd.Mojo::SMTP::Client::Response::CRLF);
569             }
570              
571             sub _read_response {
572 86     86   830 my ($self, $cb) = @_;
573 86         266 $self->{stream}->timeout($self->inactivity_timeout);
574 86         2552 my $resp = '';
575            
576             $self->{stream}->on(read => sub {
577 86     86   223481 $resp .= $_[-1];
578 86 100       829 if ($resp =~ /^\d+(?:\s[^\n]*)?\n$/m) {
579 84         408 $self->{stream}->unsubscribe('read');
580 84         1114 $cb->($self, Mojo::SMTP::Client::Response->new($resp));
581             }
582 86         590 });
583             }
584              
585             sub _rm_stream {
586 15     15   46 my $self = shift;
587 15         184 $self->{stream}->unsubscribe('close')
588             ->unsubscribe('timeout')
589             ->unsubscribe('error')
590             ->unsubscribe('read');
591 15         1170 delete $self->{stream};
592             }
593              
594             sub _has_nl {
595 47 50   47   166 substr(ref $_[0] ? ${$_[0]} : $_[0], -2, 2) eq Mojo::SMTP::Client::Response::CRLF;
  0         0  
596             }
597              
598             sub DESTROY {
599 15     15   6395 my $self = shift;
600 15 100       2002 if ($self->{stream}) {
601 5         161 $self->_rm_stream();
602             }
603             }
604              
605             1;
606              
607             __END__