File Coverage

lib/AnyEvent/SMTP/Client.pm
Criterion Covered Total %
statement 127 169 75.1
branch 30 66 45.4
condition 9 21 42.8
subroutine 21 23 91.3
pod 1 1 100.0
total 188 280 67.1


line stmt bran cond sub pod time code
1             package AnyEvent::SMTP::Client;
2              
3             =head1 NAME
4              
5             AnyEvent::SMTP::Client - Simple asyncronous SMTP Client
6              
7             =cut
8              
9 11     11   5480 use AnyEvent;
  11         27  
  11         312  
10 11     11   64 use common::sense;
  11         20  
  11         92  
11             m{# trying to cheat with cpants game ;)
12             use strict;
13             use warnings;
14             }x;
15              
16 11     11   809 use base 'Object::Event';
  11         26  
  11         2264  
17              
18 11     11   11637 use AnyEvent::Handle;
  11         10706  
  11         255  
19 11     11   1527 use AnyEvent::Socket;
  11         16634  
  11         1949  
20 11     11   68 use AnyEvent::DNS;
  11         14  
  11         350  
21 11     11   54 use AnyEvent::Util;
  11         23  
  11         949  
22              
23 11     11   3540 use Sys::Hostname;
  11         1306  
  11         558  
24 11     11   771 use Mail::Address;
  11         2613  
  11         253  
25              
26 11     11   562 use AnyEvent::SMTP::Conn;
  11         18  
  11         436  
27              
28 11     11   54 our $VERSION = $AnyEvent::SMTP::VERSION;use AnyEvent::SMTP ();
  11         20  
  11         19419  
29              
30             # vvv This code was partly derived from AnyEvent::HTTP vvv
31             our $MAXCON = 10; # Maximum number of connections to any host
32             our %MAXCON; # Maximum number of connections to concrete host
33             our $ACTIVE = 0; # Currently active connections
34             our %ACTIVE;
35             my %CO_SLOT; # number of open connections, and wait queue, per host
36              
37             sub _slot_schedule;
38             sub _slot_schedule($) {
39 84     84   160 my $host = shift;
40 84 100       279 my $mc = exists $MAXCON{$host} ? $MAXCON{$host} : $MAXCON;
41 84   66     658 while (!$mc or ( $mc > 0 and $CO_SLOT{$host}[0] < $mc )) {
      66        
42 90 100       10040 if (my $cb = shift @{ $CO_SLOT{$host}[1] }) {
  90         318  
43             # somebody wants that slot
44 42         85 ++$CO_SLOT{$host}[0];
45 42         55 ++$ACTIVE;
46 42         77 ++$ACTIVE{$host};
47             $cb->(AnyEvent::Util::guard {
48 42     42   79 --$ACTIVE;
49 42 100       294 --$ACTIVE{$host} > 0 or delete $ACTIVE{$host};
50 42         123 --$CO_SLOT{$host}[0];
51             #warn "Release slot (have $ACTIVE) by @{[ (caller)[1,2] ]}\n";
52 42         118 _slot_schedule $host;
53 42         374 });
54             } else {
55             # nobody wants the slot, maybe we can forget about it
56 48 100       146 delete $CO_SLOT{$host} unless $CO_SLOT{$host}[0];
57 48         291 last;
58             }
59             }
60             }
61              
62             # wait for a free slot on host, call callback
63             sub _get_slot($$) {
64 42     42   172 push @{ $CO_SLOT{$_[0]}[1] }, $_[1];
  42         117  
65 42         108 _slot_schedule $_[0];
66             }
67              
68             sub _tcp_connect($$$;$) {
69 42     42   89 my ($host,$port,$cb,$pr) = @_;
70             #warn "Need slot $host (have $ACTIVE)";
71             _get_slot $host, sub {
72 42     42   77 my $sg = shift;
73             #warn "Have slot $host (have $ACTIVE)";
74             tcp_connect($host,$port,sub {
75 42         10651 $cb->(@_,$sg);
76 42         494 }, $pr);
77             }
78 42         297 }
79             # ^^^ This code was partly derived from AnyEvent::HTTP ^^^
80              
81              
82              
83             =head1 SYNOPSIS
84              
85             use AnyEvent::SMTP::Client 'sendmail';
86            
87             sendmail
88             from => 'mons@cpan.org',
89             to => 'mons@cpan.org', # SMTP host will be detected from addres by MX record
90             data => 'Test message '.time().' '.$$,
91             cb => sub {
92             if (my $ok = shift) {
93             warn "Successfully sent";
94             }
95             if (my $err = shift) {
96             warn "Failed to send: $err";
97             }
98             }
99             ;
100              
101             =head1 DESCRIPTION
102              
103             Asyncronously connect to SMTP server, resolve MX, if needed, then send HELO => MAIL => RCPT => DATA => QUIT and return responce
104              
105             =head1 FUNCTIONS
106              
107             =head2 sendmail ... , cb => $cb->(OK,ERR)
108              
109             Argument names are case insensitive. So, it may be calles as
110              
111             sendmail From => ..., To => ..., ...
112              
113             and as
114              
115             sendmail from => ..., to => ..., ...
116              
117             Arguments description are below
118              
119             =over 4
120              
121             =item host => 'smtp.server'
122              
123             SMTP server to use. Optional. By default will be resolved MX record
124              
125             =item port => 2525
126              
127             SMTP server port. Optional. By default = 25
128              
129             =item server => 'some.server:25'
130              
131             SMTP server. The same as pair of host:port
132              
133             =item helo => 'hostname'
134              
135             HELO message. Optional. By default = hostname()
136              
137             =item from => 'mail@addr.ess'
138              
139             =item to => 'mail@addr.ess'
140              
141             =item to => [ 'mail@addr.ess', ... ]
142              
143             =item data => 'Message body'
144              
145             =item Message => 'Message body'
146              
147             Message text. For message composing may be used, for ex: L
148              
149             =item timeout => int
150              
151             Use timeout during network operations
152              
153             =item debug => 0 | 1
154              
155             Enable connection debugging
156              
157             =item cb => $cb->(OK,ERR)
158              
159             Callback.
160              
161             When $args{to} is a single argument:
162              
163             OK - latest response from server
164             If OK is undef, then something failed, see ERR
165             ERR - error response from server
166              
167             When $args{to} is an array:
168              
169             OK - hash of success responces or undef.
170             keys are addresses, values are responces
171              
172             ERR - hash of error responces.
173             keys are addresses, values are responces
174              
175             See examples
176              
177             =item cv => AnyEvent->condvar
178              
179             If passed, used as group callback operand
180              
181             sendmail ... cv => $cv, cb => sub { ...; };
182              
183             is the same as
184              
185             $cv->begin;
186             sendmail ... cb => sub { ...; $cv->end };
187              
188             =back
189              
190             =head1 VARIABLES
191              
192             =head2 $MAXCON [ = 10]
193              
194             Maximum number of connections to any host. Default is 10
195              
196             =head2 %MAXCON
197              
198             Per-host configuration for maximum number of connection
199              
200             Please note, host is hostname passed in argument, or resolved MX record.
201              
202             So, if passed C 'localhost'>, should be used C<$MAXCON{localhost}>, if passed C '127.0.0.1'>, should be used C<$MAXCON{'127.0.0.1'}>
203              
204             # set default limit to 20
205             $AnyEvent::SMTP::Client::MAXCON = 20;
206            
207             # don't limit localhost connections
208             $AnyEvent::SMTP::Client::MAXCON{'localhost'} = 0;
209            
210             # big limit for one of gmail MX
211             $AnyEvent::SMTP::Client::MAXCON{'gmail-smtp-in.l.google.com.'} = 100;
212              
213             =head2 $ACTIVE
214              
215             Number of currently active connections
216              
217             =head2 %ACTIVE
218              
219             Number of currently active connections per host
220              
221             =cut
222              
223             sub import {
224 1     1   11 my $me = shift;
225 1         3 my $pkg = caller;
226 11     11   67 no strict 'refs';
  11         19  
  11         25252  
227 1 50       15 @_ or return;
228 0         0 for (@_) {
229 0 0       0 if ( $_ eq 'sendmail') {
230 0         0 *{$pkg.'::'.$_} = \&$_;
  0         0  
231             } else {
232 0         0 require Carp; Carp::croak "$_ is not exported by $me";
  0         0  
233             }
234             }
235             }
236              
237             sub sendmail(%) {
238 42     42 1 296512 my %args = @_;
239 42         324 my @keys = keys %args;
240 42         637 @args{map lc, @keys} = delete @args{ @keys };
241 42   0     263 $args{data} ||= delete $args{message} || delete $args{body};
      33        
242 42   33     532 $args{helo} ||= hostname();
243 42 50       604 if ($args{server}) {
244 0         0 my ($h,$p) = $args{server} =~ /^([^:]+)(?:|:(\d+))$/;
245 0 0       0 $args{host} = $h or return $args{cb}(undef,"Bad option value for `server'");
246 0 0       0 $args{port} = $p if defined $p;
247             }
248 42   50     130 $args{port} ||= 25;
249 42   100     258 $args{timeout} ||= 30;
250              
251 42         63 my ($run,$cv,$res,$err);
252 42 50       247 $args{cv}->begin if $args{cv};
253 42         1730 $cv = AnyEvent->condvar;
254             my $end = sub{
255 42     42   539 undef $run;
256 42         62 undef $cv;
257 42 100       268 $args{cb}( $res, defined $err ? $err : () );
258 42 50       61324 $args{cv}->end if $args{cv};
259 42         799 %args = ();
260 42         510 };
261 42         167 $cv->begin($end);
262            
263 42 50       406 ($args{from},my @rcpt) = map { $_->address } map { Mail::Address->parse($_) } $args{from},ref $args{to} ? @{$args{to}} : $args{to};
  84         7500  
  84         8725  
  0         0  
264            
265             $run = sub {
266 42     42   102 my ($host,$port,@to) = @_;
267 42 50       119 warn "connecting to $host:$port\n" if $args{debug};
268 42         61 my ($exc,$con,$slot_guard);
269             my $cb = sub {
270 42         96 undef $exc;
271 42 100       7184 $con and $con->close;
272 42         65 undef $slot_guard;
273 42         179 undef $con;
274 42 50       7876 if (@rcpt > 1) {
275             #warn "multi cb @to: @_";
276 0 0       0 if ($_[0]) {
277 0         0 @$res{@to} = ($_[0])x@to;
278             } else {
279 0         0 @$err{@to} = ($_[1])x@to;
280             }
281             } else {
282             #warn "single cb @to: @_";
283 42         100 ($res,$err) = @_;
284             }
285 42         203 $cv->end;
286 42         305 };
287 42         164 $cv->begin;
288             _tcp_connect $host,$port,sub {
289 42         82 $slot_guard = pop;
290 42 100       137 my $fh = shift
291             or return $cb->(undef, "$!");
292 41         446 $con = AnyEvent::SMTP::Conn->new( fh => $fh, debug => $args{debug}, timeout => $args{timeout} );
293             $exc = $con->reg_cb(
294             disconnect => sub {
295 0 0       0 $con or return;
296 0         0 $cb->(undef,$_[1]);
297             },
298 41         447 );
299             $con->line(ok => 220, cb => sub {
300 41 50       108 shift or return $cb->(undef, @_);
301             $con->command("HELO $args{helo}", ok => 250, cb => sub {
302 41 50       189 shift or return $cb->(undef, @_);
303             $con->command("MAIL FROM:<$args{from}>", ok => 250, cb => sub {
304 41 50       120 shift or return $cb->(undef, @_);
305              
306 41         10199 my $cv1 = AnyEvent->condvar;
307             $cv1->begin(sub {
308 41         722 undef $cv1;
309             $con->command("DATA", ok => 354, cb => sub {
310 41 50       129 shift or return $cb->(undef, @_);
311 41         603 $con->reply("$args{data}");
312             $con->command(".", ok => 250, cb => sub {
313 41 50       162 my $reply = shift or return $cb->(undef, @_);
314 41         137 $cb->($reply);
315 41         2361 });
316 41         489 });
317 41         745 });
318              
319 41         382 for ( @to ) {
320 41         136 $cv1->begin;
321             $con->command("RCPT TO:<$_>", ok => 250, cb => sub {
322 41 50       137 shift or return $cb->(undef, @_);
323 41         236 $cv1->end;
324 41         501 });
325             }
326              
327 41         1709 $cv1->end;
328 41         552 });
329              
330 41         395 });
331 41         5093 });
332 42 50       629 }, sub { $args{timeout} || 30 };
  42         13576  
333            
334 42         1025 };
335            
336 42 50       187 if ($args{host}) {
337 42         127 $run->($args{host},$args{port}, @rcpt);
338             } else {
339 0         0 my %domains;
340 0 0       0 my $dns = AnyEvent::DNS->new(
341             $args{timeout} ? ( timeout => [ $args{timeout} ] ) : ()
342             );
343 0         0 $dns->os_config;
344 0         0 for (@rcpt) {
345 0         0 my ($domain) = /^.+\@(.+)$/;
346 0   0     0 push @{ $domains{$domain} ||= [] }, $_;
  0         0  
347             }
348 0         0 for my $domain (keys %domains) {
349 0         0 $cv->begin;
350             $dns->resolve( $domain => mx => sub {
351 0 0   0   0 if ($AnyEvent::VERSION > 6.0) {
352 0         0 @_ = map $_->[5], sort { $a->[4] <=> $b->[4] } @_;
  0         0  
353             } else {
354 0         0 @_ = map $_->[4], sort { $a->[3] <=> $b->[3] } @_;
  0         0  
355             }
356 0 0       0 warn "MX($domain) = [ @_ ]\n" if $args{debug};
357 0 0       0 if (@_) {
358 0         0 $run->(shift, $args{port}, @{ delete $domains{$domain} });
  0         0  
359             } else {
360 0 0       0 if (@rcpt > 1) {
361 0         0 @$err{ @{ $domains{$domain} } } = ( "No MX record for domain $domain" )x@{ $domains{$domain} };
  0         0  
  0         0  
362             } else {
363 0         0 $err = "No MX record for domain $domain";
364             }
365             }
366 0         0 $cv->end;
367 0         0 });
368             }
369 0         0 undef $dns;
370             }
371 42         728 $cv->end;
372             defined wantarray
373 0     0     ? AnyEvent::Util::guard { $end->(undef, "Cancelled"); }
374 42 50       395 : ();
375             }
376              
377             =head1 BUGS
378              
379             Bug reports are welcome in CPAN's request tracker L
380              
381             =head1 AUTHOR
382              
383             Mons Anderson, C<< >>
384              
385             =head1 COPYRIGHT & LICENSE
386              
387             Copyright 2009 Mons Anderson, all rights reserved.
388              
389             This program is free software; you can redistribute it and/or modify it
390             under the same terms as Perl itself.
391              
392             =cut
393              
394             1;