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   518230 use Mojo::Base 'Mojo::EventEmitter';
  3         408247  
  3         24  
4 3     3   5231 use Mojo::IOLoop;
  3         342005  
  3         20  
5 3     3   149 use Mojo::IOLoop::Client;
  3         8  
  3         18  
6 3     3   1575 use Mojo::IOLoop::Delay;
  3         3066  
  3         29  
7 3     3   128 use Mojo::IOLoop::Stream;
  3         8  
  3         20  
8 3     3   86 use Mojo::Util 'b64_encode';
  3         6  
  3         134  
9 3     3   1482 use Mojo::SMTP::Client::Response;
  3         8  
  3         25  
10 3     3   1532 use Mojo::SMTP::Client::Exception;
  3         11  
  3         143  
11 3     3   27 use Scalar::Util 'weaken';
  3         8  
  3         124  
12 3     3   17 use Carp;
  3         7  
  3         248  
13              
14             our $VERSION = '0.20_2';
15              
16             use constant {
17 3         17034 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   19 };
  3         6  
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 58143 my $class = shift;
62            
63 15         650 my $self = $class->SUPER::new(@_);
64 15         1102 weaken(my $this = $self);
65            
66             $self->{resp_checker} = sub {
67 84     84   2189 my ($delay, $resp) = @_;
68 84         357 $this->emit(response => $this->{last_cmd}, $resp);
69            
70 82 100       6475 unless (substr($resp->code, 0, 1) == $this->{expected_code}) {
71 2         55 die $resp->error(Mojo::SMTP::Client::Exception::Response->new($resp->message)->code($resp->code));
72             }
73 80         358 $delay->pass($resp);
74 15         924 };
75            
76 15         191 $self->{cmds} = [];
77            
78 15         561 $self;
79             }
80              
81             sub send {
82 17     17 1 205615 my $self = shift;
83 17 100       215 my $cb = @_ % 2 == 0 ? undef : pop;
84            
85 17         119 my @steps;
86 17 100       213 $self->{nb} = $cb ? 1 : 0;
87            
88             # user changed SMTP server or server sent smth while it shouldn't
89 17 50 33     159 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         24 $self->_rm_stream();
94             }
95            
96 17 100       415 unless ($self->{stream}) {
97             push @steps, sub {
98 16     16   7483 my $delay = shift;
99             # connect
100 16         86 $self->{starttls} = $self->{authorized} = 0;
101 16         238 $self->emit('start');
102 15         334 $self->{server} = $self->_server;
103 15         554 $self->{last_cmd} = CMD_CONNECT;
104            
105 15         63 my $connect_cb = $delay->begin;
106 15         253 $self->{client} = Mojo::IOLoop::Client->new(reactor => $self->_ioloop->reactor);
107 15         1868 $self->{client}->on(connect => $connect_cb);
108 15         224 $self->{client}->on(error => $connect_cb);
109             $self->{client}->connect(
110 15         210 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   40676 my $delay = shift;
123 15         53 delete $self->{client};
124             # check is this a handle
125 15 50       29 Mojo::SMTP::Client::Exception::Stream->throw($_[0]) unless eval { *{$_[0]} };
  15         35  
  15         95  
126            
127 15         143 $self->_make_stream($_[0], $self->_ioloop);
128 15         241 $self->_read_response($delay->begin);
129 15         263 $self->{expected_code} = CMD_OK;
130             },
131             # check response
132 16         602 $self->{resp_checker};
133            
134 16 100 100     375 if (!@_ || $_[0] ne 'hello') {
135 14         207 unshift @_, hello => $self->hello;
136             }
137             }
138             else {
139 1         5 $self->{stream}->start;
140             }
141            
142 17         419 push @{$self->{cmds}}, @_;
  17         333  
143 17         283 push @steps, $self->_make_cmd_steps();
144            
145             # non-blocking
146 17         858 my $delay = $self->{delay} = Mojo::IOLoop::Delay->new->ioloop($self->_ioloop)->steps(@steps);
147             $self->{finally} = sub {
148 17 100   17   3998 shift if @_ == 2; # delay
149            
150 17 50       105 if ($cb) {
151 17         44 my $r = $_[0];
152 17 100       151 unless ($r->isa('Mojo::SMTP::Client::Response')) {
153             # some error occured, which throwed an exception
154 5         90 $r = Mojo::SMTP::Client::Response->new('', error => $r);
155             }
156            
157 17         62 delete $self->{delay};
158 17         68 delete $self->{finally};
159            
160 17         86 $cb->($self, $r);
161 17         5944 $cb = undef;
162             }
163 17         6456 };
164 17         246 $delay->catch($self->{finally});
165            
166             # blocking
167 17         2423 my $resp;
168 17 100       149 unless ($self->{nb}) {
169             $cb = sub {
170 4     4   10 $resp = pop;
171 4         84 };
172 4         65 $delay->wait;
173 4 100 66     3530 return $self->autodie && $resp->error ? die $resp->error : $resp;
174             }
175             }
176              
177             sub prepend_cmd {
178 1     1 1 28 my $self = shift;
179 1 50       16 croak "no active `send' calls" unless exists $self->{delay};
180            
181 1         19 unshift @{ $self->{cmds} }, @_;
  1         25  
182             }
183              
184             sub _ioloop {
185 49     49   2650 my ($self) = @_;
186 49 100       719 return $self->{nb} ? Mojo::IOLoop->singleton : $self->ioloop;
187             }
188              
189             sub _server {
190 17     17   51 my $self = shift;
191 17         206 return $self->address.':'.$self->port.':'.$self->tls;
192             }
193              
194             sub _make_stream {
195 17     17   191 my ($self, $sock, $loop) = @_;
196            
197 17         170 weaken $self;
198             my $error_handler = sub {
199 2 50   2   130 delete($self->{cleanup_cb})->() if $self->{cleanup_cb};
200 2         46 $self->_rm_stream();
201            
202 2         83 $self->{delay}->reject($_[0]);
203 17         246 };
204            
205 17         475 $self->{stream} = Mojo::IOLoop::Stream->new($sock);
206 17         1473 $self->{stream}->reactor($loop->reactor);
207 17         397 $self->{stream}->start;
208             $self->{stream}->on(timeout => sub {
209 2     2   976320 $error_handler->(Mojo::SMTP::Client::Exception::Stream->new('Inactivity timeout'));
210 17         2603 });
211             $self->{stream}->on(error => sub {
212 0     0   0 $error_handler->(Mojo::SMTP::Client::Exception::Stream->new($_[-1]));
213 17         433 });
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         206 });
217             }
218              
219             sub _make_cmd_steps {
220 72     72   196 my ($self) = @_;
221            
222 72         142 my ($cmd, $arg) = splice @{ $self->{cmds} }, 0, 2;
  72         360  
223 72 100       305 unless ($cmd) {
224             # no more commands
225 11 100       44 if ($self->{stream}) {
226 4         20 $self->{stream}->timeout(0);
227 4         102 $self->{stream}->stop;
228             }
229 11         204 return $self->{finally};
230             }
231            
232 61 50       602 if ( my $sub = $self->can("_cmd_$cmd") ) {
233             return (
234             $self->$sub($arg), sub {
235 55     55   11289 my ($delay, $resp) = @_;
236            
237 55         187 $delay->pass($resp);
238 55         1198 $delay->steps( $self->_make_cmd_steps() );
239             }
240 61         406 );
241             }
242            
243 0         0 croak 'unrecognized command: ', $cmd;
244             }
245              
246             # EHLO/HELO
247             sub _cmd_hello {
248 16     16   67 my ($self, $arg) = @_;
249 16         81 weaken $self;
250            
251             return (
252             sub {
253 13     13   3609 my $delay = shift;
254 13         146 $self->_write_cmd('EHLO ' . $arg, CMD_EHLO);
255 13         443 $self->_read_response($delay->begin);
256 13         108 $self->{expected_code} = CMD_OK;
257             },
258             sub {
259 13     13   414 eval { $self->{resp_checker}->(@_); $_[1]->{checked} = 1 };
  13         76  
  10         270  
260 13 100       804 if (my $e = $@) {
261 3 100       83 die $e unless $e->isa('Mojo::SMTP::Client::Response');
262 1         13 my $delay = shift;
263            
264 1         15 $self->_write_cmd('HELO ' . $arg, CMD_HELO);
265 1         42 $self->_read_response($delay->begin);
266             }
267             },
268             sub {
269 11     11   2063 my ($delay, $resp) = @_;
270 11 100       51 return $delay->pass($resp) if delete $resp->{checked};
271 1         31 $self->{resp_checker}->($delay, $resp);
272             }
273 16         1086 );
274             }
275              
276             # STARTTLS
277             sub _cmd_starttls {
278 2     2   8 my ($self, $arg) = @_;
279 2         19 weaken $self;
280            
281 2 50       222 require IO::Socket::SSL and IO::Socket::SSL->VERSION(0.98);
282            
283             return (
284             sub {
285 2     2   220 my $delay = shift;
286 2         31 $self->_write_cmd('STARTTLS', CMD_STARTTLS);
287 2         86 $self->_read_response($delay->begin);
288 2         36 $self->{expected_code} = CMD_OK;
289             },
290             $self->{resp_checker},
291             sub {
292 2     2   630 my ($delay, $resp) = @_;
293 2         15 $self->{stream}->stop;
294 2         106 $self->{stream}->timeout(0);
295            
296 2         58 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         32 };
305            
306             $sock = IO::Socket::SSL->start_SSL(
307             $self->{stream}->steal_handle,
308 2 50       25 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         4402 $tls_cb = $delay->begin;
320 2         48 $loop = $self->_ioloop;
321            
322 2         18 $tid = $loop->timer($self->inactivity_timeout => $error_handler);
323            
324             $loop->reactor->io($sock => sub {
325 6 100       4448 if ($sock->connect_SSL) {
326 2         525 $loop->remove($tid);
327 2         123 $loop->reactor->remove($sock);
328 2         199 $self->_make_stream($sock, $loop);
329 2         26 $self->{starttls} = 1;
330 2         5 $sock = $loop = undef;
331 2         22 $tls_cb->($delay, $resp);
332 2         278 $tls_cb = $delay = undef;
333 2         15 return;
334             }
335            
336 4 50       2945 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         221 })->watch($sock, 0, 1);
342             },
343             sub {
344 2     2   76 my ($delay, $resp, $error) = @_;
345 2 50       14 unless ($resp) {
346 0         0 $self->_rm_stream();
347 0         0 Mojo::SMTP::Client::Exception::Stream->throw($error);
348             }
349            
350 2         21 $delay->pass($resp);
351             }
352 2         146 );
353             }
354              
355             # AUTH
356             sub _cmd_auth {
357 2     2   20 my ($self, $arg) = @_;
358 2         9 weaken $self;
359            
360 2   100     40 my $type = lc($arg->{type} // 'plain');
361            
362             my $set_auth_ok = sub {
363 2     2   419 my ($delay, $resp) = @_;
364 2         6 $self->{authorized} = 1;
365 2         6 $delay->pass($resp);
366 2         37 };
367            
368 2 100       11 if ($type eq 'plain') {
369             return (
370             sub {
371 1     1   82 my $delay = shift;
372 1         15 $self->_write_cmd('AUTH PLAIN '.b64_encode(join("\0", '', $arg->{login}, $arg->{password}), ''), CMD_AUTH);
373 1         29 $self->_read_response($delay->begin);
374 1         7 $self->{expected_code} = CMD_OK;
375             },
376             $self->{resp_checker},
377 1         27 $set_auth_ok
378             );
379             }
380            
381 1 50       27 if ($type eq 'login') {
382             return (
383             # start auth
384             sub {
385 1     1   94 my $delay = shift;
386 1         22 $self->_write_cmd('AUTH LOGIN', CMD_AUTH);
387 1         40 $self->_read_response($delay->begin);
388 1         21 $self->{expected_code} = CMD_MORE;
389             },
390             $self->{resp_checker},
391             # send username
392             sub {
393 1     1   227 my $delay = shift;
394 1         34 $self->_write_cmd(b64_encode($arg->{login}, ''), CMD_AUTH);
395 1         41 $self->_read_response($delay->begin);
396 1         18 $self->{expected_code} = CMD_MORE;
397             },
398             $self->{resp_checker},
399             # send password
400             sub {
401 1     1   193 my $delay = shift;
402 1         33 $self->_write_cmd(b64_encode($arg->{password}, ''), CMD_AUTH);
403 1         32 $self->_read_response($delay->begin);
404 1         25 $self->{expected_code} = CMD_OK;
405             },
406             $self->{resp_checker},
407 1         54 $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   50 my ($self, $arg) = @_;
417 12         43 weaken $self;
418            
419             return (
420             sub {
421 12     12   1343 my $delay = shift;
422 12         68 $self->_write_cmd('MAIL FROM:<'.$arg.'>', CMD_FROM);
423 12         428 $self->_read_response($delay->begin);
424 12         101 $self->{expected_code} = CMD_OK;
425             },
426             $self->{resp_checker}
427 12         271 );
428             }
429              
430             # TO
431             sub _cmd_to {
432 13     13   88 my ($self, $arg) = @_;
433 13         64 weaken $self;
434            
435 13         21 my @steps;
436            
437 13 100       68 for my $to (ref $arg ? @$arg : $arg) {
438             push @steps, sub {
439 16     16   2240 my $delay = shift;
440 16         120 $self->_write_cmd('RCPT TO:<'.$to.'>', CMD_TO);
441 16         565 $self->_read_response($delay->begin);
442 16         135 $self->{expected_code} = CMD_OK;
443             },
444             $self->{resp_checker}
445 16         206 }
446            
447 13         106 return @steps;
448             }
449              
450             # DATA
451             sub _cmd_data {
452 7     7   30 my ($self, $arg) = @_;
453 7         39 weaken $self;
454            
455 7         17 my @steps;
456            
457             push @steps, sub {
458 7     7   931 my $delay = shift;
459 7         30 $self->_write_cmd('DATA', CMD_DATA);
460 7         240 $self->_read_response($delay->begin);
461 7         61 $self->{expected_code} = CMD_MORE;
462             },
463 7         106 $self->{resp_checker};
464            
465 7 100       87 if (ref $arg eq 'CODE') {
466 2         25 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   26296 my $delay = shift;
472 44 100       128 unless ($data_writer_cb) {
473 2         8 $data_writer_cb = $delay->begin;
474             $self->{cleanup_cb} = sub {
475 2         9 undef $data_writer;
476 2         46 };
477             }
478            
479 44         125 my $data = $arg->();
480 44 100       482 $data = $$data if ref $data;
481            
482 44 100       106 unless (length($data) > 0) {
483 2 50       13 $self->_write_cmd(($was_nl ? '' : Mojo::SMTP::Client::Response::CRLF).'.', CMD_DATA_END);
484 2         56 $self->_read_response($data_writer_cb);
485 2         14 $self->{expected_code} = CMD_OK;
486 2         11 return delete($self->{cleanup_cb})->();
487             }
488             # The following part if heavily inspired by Net::Cmd
489 42         72 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       82 $last_ch = "\012" unless defined $last_ch;
492 42 100       120 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       53 $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       51 $first_ch = "." if $data =~ /^\./;
501             }
502 42         355 $data =~ s/\015?\012(\.?)/\015\012$1$1/g;
503 42         100 substr($data, 0, 0) = $first_ch;
504 42         77 $last_ch = substr($data, -1, 1);
505 42         86 $was_nl = _has_nl($data);
506 42         142 $self->{stream}->write($data, $data_writer);
507 2         54 };
508            
509 2         26 push @steps, $data_writer, $self->{resp_checker};
510             }
511             else {
512             push @steps, sub {
513 5     5   1041 my $delay = shift;
514 5 50       88 (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       60 $self->{stream}->write(ref $arg ? $$arg : $arg, $delay->begin);
516             },
517             sub {
518 5     5   2935 my $delay = shift;
519 5 50       42 $self->_write_cmd((_has_nl($arg) ? '' : Mojo::SMTP::Client::Response::CRLF).'.', CMD_DATA_END);
520 5         199 $self->_read_response($delay->begin);
521 5         41 $self->{expected_code} = CMD_OK;
522             },
523             $self->{resp_checker}
524 5         116 }
525            
526 7         63 return @steps;
527             }
528              
529             # RESET
530             sub _cmd_reset {
531 2     2   21 my ($self, $arg) = @_;
532 2         24 weaken $self;
533            
534             return (
535             sub {
536 2     2   284 my $delay = shift;
537 2         27 $self->_write_cmd('RSET', CMD_RESET);
538 2         64 $self->_read_response($delay->begin);
539 2         34 $self->{expected_code} = CMD_OK;
540             },
541             $self->{resp_checker}
542 2         69 );
543             }
544              
545             # QUIT
546             sub _cmd_quit {
547 7     7   20 my ($self, $arg) = @_;
548 7         26 weaken $self;
549            
550             return (
551             sub {
552 7     7   883 my $delay = shift;
553 7         29 $self->_write_cmd('QUIT', CMD_QUIT);
554 7         255 $self->_read_response($delay->begin);
555 7         62 $self->{expected_code} = CMD_OK;
556             },
557             $self->{resp_checker}, sub {
558 7     7   1752 my $delay = shift;
559 7         37 $self->_rm_stream();
560 7         2165 $delay->pass(@_);
561             }
562 7         286 );
563             }
564              
565             sub _write_cmd {
566 71     71   245 my ($self, $cmd, $cmd_const) = @_;
567 71         176 $self->{last_cmd} = $cmd_const;
568 71         322 $self->{stream}->write($cmd.Mojo::SMTP::Client::Response::CRLF);
569             }
570              
571             sub _read_response {
572 86     86   1014 my ($self, $cb) = @_;
573 86         282 $self->{stream}->timeout($self->inactivity_timeout);
574 86         3153 my $resp = '';
575            
576             $self->{stream}->on(read => sub {
577 86     86   230772 $resp .= $_[-1];
578 86 100       956 if ($resp =~ /^\d+(?:\s[^\n]*)?\n$/m) {
579 84         430 $self->{stream}->unsubscribe('read');
580 84         1252 $cb->($self, Mojo::SMTP::Client::Response->new($resp));
581             }
582 86         603 });
583             }
584              
585             sub _rm_stream {
586 15     15   61 my $self = shift;
587 15         222 $self->{stream}->unsubscribe('close')
588             ->unsubscribe('timeout')
589             ->unsubscribe('error')
590             ->unsubscribe('read');
591 15         1560 delete $self->{stream};
592             }
593              
594             sub _has_nl {
595 47 50   47   185 substr(ref $_[0] ? ${$_[0]} : $_[0], -2, 2) eq Mojo::SMTP::Client::Response::CRLF;
  0         0  
596             }
597              
598             sub DESTROY {
599 15     15   7643 my $self = shift;
600 15 100       2187 if ($self->{stream}) {
601 5         140 $self->_rm_stream();
602             }
603             }
604              
605             1;
606              
607             __END__