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