File Coverage

blib/lib/Protocol/SMTP/Client.pm
Criterion Covered Total %
statement 125 177 70.6
branch 22 48 45.8
condition 3 5 60.0
subroutine 31 40 77.5
pod 21 21 100.0
total 202 291 69.4


line stmt bran cond sub pod time code
1             package Protocol::SMTP::Client;
2             $Protocol::SMTP::Client::VERSION = '0.002';
3 7     7   185392 use strict;
  7         19  
  7         228  
4 7     7   37 use warnings;
  7         15  
  7         169  
5 7     7   8035 use utf8;
  7         74  
  7         36  
6              
7 7     7   6897 use curry;
  7         1435  
  7         185  
8 7     7   7390 use Future;
  7         105389  
  7         330  
9 7     7   7089 use Future::Utils qw(try_repeat fmap_void);
  7         15864  
  7         514  
10 7     7   5665 use Authen::SASL;
  7         13814  
  7         48  
11 7     7   7443 use MIME::Base64 qw(encode_base64 decode_base64);
  7         6232  
  7         553  
12 7     7   6582 use Encode;
  7         81138  
  7         16767  
13              
14             =head1 NAME
15              
16             Protocol::SMTP::Client - abstract client support for mail sending
17              
18             =head1 VERSION
19              
20             version 0.002
21              
22             =head1 DESCRIPTION
23              
24             Provides a client implementation for interacting with SMTP servers.
25              
26             =cut
27              
28             =head1 METHODS
29              
30             =head2 new
31              
32             Instantiates an SMTP client instance.
33              
34             Takes no parameters.
35              
36             =cut
37              
38             sub new {
39 8     8 1 1123 my $class = shift;
40 8         50 my $self = bless {@_}, $class;
41 8         65 $self->{auth_methods} = [];
42 8         23 $self->{task_queue} = [];
43 8         22 $self->{multi} = [];
44 8         28 $self
45             }
46              
47             =head2 auth_mechanism_override
48              
49             Set this on instantiation to pick a specific auth method.
50              
51             =cut
52              
53 1     1 1 6 sub auth_mechanism_override { shift->{auth_mechanism_override} }
54              
55             =head2 login
56              
57             Attempts to log in to the server. Takes the following named parameters:
58              
59             =over 4
60              
61             =item * user - the username we're logging in with, might be your email address
62             or a plain username
63              
64             =item * pass - used for password-based auth mechanisms such as PLAIN or MD5
65              
66             =back
67              
68             Note that other auth mechanisms may provide additional fields - this will
69             mostly be determined by how L deals with the authentication
70             process.
71              
72             Returns a L which resolves once login completes or fails.
73              
74             =cut
75              
76             sub login {
77 2     2 1 1444 my $self = shift;
78 2 100       10 return $self->new_future->fail('no auth?') unless my @auth = $self->auth_methods;
79              
80 1         134 my %args = @_;
81 1         4 my $auth_string = join ' ', @auth;
82 1         6 $self->debug_printf("Auth mechanisms [%s]", $auth_string);
83 1 50       5 $auth_string = $self->auth_mechanism_override if $self->auth_mechanism_override;
84 1         3 my $f = $self->new_future;
85             $self->add_task(sub {
86             my $sasl = Authen::SASL->new(
87             mechanism => $auth_string,
88             callback => {
89 0         0 user => sub { $args{user} },
90 0         0 authname => sub { $args{user} },
91 0         0 pass => sub { $args{pass} },
92             },
93 1     1   31 );
94 1         237 my $client = $sasl->client_new(
95             'smtp',
96             $args{host},
97             0,
98             );
99              
100 1         27106 my $rslt = $client->client_start;
101 1 50 33     48 $rslt = (defined($rslt) && length($rslt)) ? encode_base64($rslt, '') : '';
102 1         24 $self->write_line(join ' ',
103             'AUTH',
104             $client->mechanism,
105             $rslt
106             );
107             $self->{auth_handler} = sub {
108 0         0 my $code = shift;
109 0 0       0 if($code =~ /^5/) {
    0          
    0          
110 0         0 delete $self->{auth_handler};
111 0         0 return $f->fail(shift);
112             } elsif($code =~ /^3/) {
113 0         0 my $data = decode_base64(shift);
114 0         0 $self->write_line(
115             encode_base64(
116             $client->client_step($data),
117             ''
118             )
119             );
120             } elsif($code =~ /2/) {
121 0         0 delete $self->{auth_handler};
122 0         0 $f->done
123             }
124 1         9 };
125 1         13 $f
126 1         177 });
127 1         29 $f;
128             }
129              
130              
131             =head2 send
132              
133             Attempts to send the given email.
134              
135             Expects the following named parameters:
136              
137             =over 4
138              
139             =item * to - single email address or arrayref of recipients
140              
141             =item * from - envelope sender
142              
143             =item * data - the email content itself, currently needs to be 8BITMIME
144             encoded, please raise an RT if other formats are required.
145              
146             =item * content - the email as a Perl Unicode string, this is mutually
147             exclusive with the data parameter
148              
149             =back
150              
151             Returns a L which will resolve when the send is complete.
152              
153             =cut
154              
155             sub send {
156 0     0 1 0 my $self = shift;
157 0         0 my %args = @_;
158 0         0 my $f = $self->new_future;
159             $self->add_task(sub {
160 0     0   0 $self->send_mail(
161             %args
162             )->on_ready($f);
163 0         0 });
164 0         0 $f
165             }
166              
167             =head1 INTERNAL METHODS
168              
169             The following are used internally. They are not likely to be of much
170             use to client code, but may need to be called by implementations.
171             See L for a reference.
172              
173             =head2 new_future
174              
175             Instantiates a new L. Sometimes implementations may want
176             a L subclass which knows how to C. Defaults to L.
177              
178             =cut
179              
180             sub new_future {
181 6     6 1 52 my $factory = shift->{future_factory};
182 6 100       41 $factory ? $factory->() : Future->new;
183             }
184              
185             =head2 debug_printf
186              
187             Used internally for debugging, returns an empty list.
188              
189             =cut
190              
191             sub debug_printf {
192 15 50   15 1 52 return unless $ENV{'PERL_SMTP_DEBUG'};
193 0         0 my $self = shift;
194 0         0 my $fmt = shift;
195 0         0 printf "$fmt\n", @_;
196 0         0 return;
197             }
198              
199             =head2 write
200              
201             Uses the configured writer to send data to the remote.
202              
203             =cut
204              
205             sub write {
206 2     2 1 3 my $self = shift;
207 2         11 $self->{writer}->(@_);
208             }
209              
210             =head2 have_active_task
211              
212             Returns true if we're partway through processing something.
213              
214             =cut
215              
216 5     5 1 864 sub have_active_task { exists shift->{active_task} }
217              
218             =head2 write_line
219              
220             Frames a line appropriately for sending to a remote.
221              
222             =cut
223              
224             sub write_line {
225 2     2 1 13 my $self = shift;
226 2         5 my $line = shift;
227 2 50       18 my $f = defined(wantarray) ? $self->new_future : undef;
228 2         10 $self->debug_printf("Writing %s", $line);
229             $self->write($line . "\x0D\x0A",
230             defined(wantarray)
231 0     0   0 ? (on_flush => sub { $f->done })
232 2 50       12 : ()
233             );
234 2         89 $f
235             }
236              
237             =head2 body_encoding
238              
239             Body encoding, currently hardcoded as 8BITMIME.
240              
241             =cut
242              
243 1     1 1 13 sub body_encoding { '8BITMIME' }
244              
245             =head2 send_mail
246              
247             Sequence of writes to send email to the remote. Normally you wouldn't use
248             this directly, it'd be queued as a task by L.
249              
250             =cut
251              
252             sub send_mail {
253 2     2 1 1334 my $self = shift;
254 2         8 my %args = @_;
255 2 50       10 my @recipient = (ref($args{to}) eq 'ARRAY') ? @{$args{to}} : $args{to};
  0         0  
256              
257 2 50       38 die "Must specify either data or content" unless 1 == grep exists $args{$_}, qw(data content);
258              
259             # We accept pre-encoded data or a Perl Unicode string.
260 0 0       0 $args{data} = Encode::encode('UTF-8' => delete $args{content}) if exists $args{content};
261              
262             # TODO Since our email content is not particularly heavy, and we're
263             # dealing with 8bitmime, this uses the naïve split-into-lines approach
264             # with the entire message in memory. Binary attachments, alternative
265             # encodings and larger messages are not well handled here.
266             # This is fixed by using Aliran, of course, but we don't have that option
267             # just yet.
268 0         0 my @mail = split /\x0D\x0A/, $args{data};
269              
270             {
271 0         0 my $mail_line = 'MAIL FROM:<' . $args{from} . '>';
  0         0  
272 0 0       0 $mail_line .= ' BODY=' . $self->body_encoding if $self->body_encoding;
273 0         0 $self->write_line($mail_line);
274             }
275             $self->wait_for(250)
276             ->then(sub {
277             fmap_void {
278 0         0 $self->write_line(q{RCPT TO:<} . shift . q{>});
279             # Each recipient line should be acknowledged with 250 if valid.
280 0         0 $self->wait_for(250)
281 0     0   0 } foreach => \@recipient
282             })->then(sub {
283 0     0   0 $self->write_line(q{DATA});
284 0         0 $self->wait_for(354)
285             })->then(sub {
286 0     0   0 $self->{sending_content} = 1;
287             (fmap_void {
288 0         0 my $line = shift;
289             # RFC2821 section 4.5.2
290 0 0       0 $line = ".$line" if substr($line, 0, 1) eq '.';
291 0         0 $self->write_line($line);
292             } generate => sub {
293 0 0       0 return unless @mail;
294             shift @mail
295 0         0 })
296 0         0 })->then(sub {
297 0     0   0 $self->{sending_content} = 0;
298 0         0 $self->write_line('.');
299 0         0 $self->wait_for(250)
300 0         0 });
301             }
302              
303             =head2 check_next_task
304              
305             Called internally to check whether we have any other tasks we could be doing.
306              
307             =cut
308              
309             sub check_next_task {
310 5     5 1 7 my $self = shift;
311 5 100       9 return 0 unless @{$self->{task_queue}};
  5         28  
312              
313 2         3 my $next = shift(@{$self->{task_queue}});
  2         5  
314 2         167 $self->{active_task} = 1;
315 2         6 my $f = $next->();
316             $f->on_ready(sub {
317 1     1   15 delete $self->{active_task};
318 1         6 $self->check_next_task;
319 1         3 undef $f
320 2         59 });
321 2         23 return 1;
322             }
323              
324             =head2 has_feature
325              
326             Returns true if we have the given feature.
327              
328             =cut
329              
330 0     0 1 0 sub has_feature { $_[0]->{features}{$_[1]} }
331              
332             =head2 remote_feature
333              
334             Marks the given feature from EHLO response as supported.
335              
336             Also applies AUTH values.
337              
338             =cut
339              
340             sub remote_feature {
341 4     4 1 5 my $self = shift;
342 4         7 my ($feature, $param) = @_;
343 4 100       10 if($feature eq 'AUTH') {
344 1 50       10 $self->{auth_methods} = [split ' ', $param] unless $self->{auth_method_override};
345             } else {
346 3   100     22 $self->{features}{$feature} = $param // 1;
347             }
348             }
349              
350             =head2 auth_methods
351              
352             Internal accessor, returns the list of defined authentication methods.
353              
354             =cut
355              
356 2     2 1 4 sub auth_methods { @{shift->{auth_methods}} }
  2         17  
357              
358             =head2 send_greeting
359              
360             Sends the EHLO greeting and handles the resulting feature list.
361              
362             =cut
363              
364             sub send_greeting {
365 1     1 1 451 my $self = shift;
366             # Start with our greeting, which should receive back a nice list of features
367 1         5 $self->write_line(
368             q{EHLO localhost}
369             );
370             $self->wait_for(250)->on_done(sub {
371 1     1   49 $self->{remote_domain} = shift;
372 1         4 for (@_) {
373 4         20 my ($feature, $param) = /^(\S+)(?: (.*))?$/;
374 4         11 $self->remote_feature($feature => $param);
375             }
376 1         4 });
377             }
378              
379             =head2 starttls
380              
381             Switch to TLS mode.
382              
383             =cut
384              
385             sub starttls {
386 0     0 1 0 my $self = shift;
387 0         0 $self->write_line(q{STARTTLS});
388 0         0 $self->wait_for(220)
389             }
390              
391             =head2 startup
392              
393             Get initial startup banner.
394              
395             =cut
396              
397             sub startup {
398 1     1 1 66 my $self = shift;
399             $self->wait_for(220)->on_done(sub {
400 1     1   57 $self->{remote_banner} = shift;
401 1         5 });
402             }
403              
404             =head2 wait_for
405              
406             Waits for the given status code.
407              
408             If we get something else, will mark as a failure.
409              
410             =cut
411              
412             sub wait_for {
413 2     2 1 3 my $self = shift;
414 2         2 my $code = shift;
415 2         6 my $f = $self->new_future;
416 2         33 push @{$self->{pending}}, [ $code => $f ];
  2         14  
417 2         19 $f
418             }
419              
420             =head2 handle_line
421              
422             Handle input line from remote.
423              
424             =cut
425              
426             sub handle_line {
427 6     6 1 5801 my $self = shift;
428 6         9 my $line = shift;
429 6         15 $self->debug_printf("Received line: %s", $line);
430              
431 6         33 my ($code, $multi, $remainder) = $line =~ /^(\d{3})([- ])(.*)$/;
432 6 50       16 if($self->{auth_handler}) {
433 0         0 return $self->{auth_handler}->($code, $remainder);
434             }
435              
436 6         6 push @{$self->{multi}}, $remainder;
  6         13  
437              
438 6 100       22 if($multi eq ' ') {
439 2         3 my $task = shift @{$self->{pending}};
  2         5  
440 2 50       8 if($task->[0] == $code) {
441 2         3 $self->debug_printf("Applying line [$_] for multi-line task") for @{$self->{multi}};
  2         30  
442 2         4 $task->[1]->done(@{$self->{multi}});
  2         14  
443             } else {
444 0         0 $self->debug_printf("We had an unexpected code - $code instead of " . $task->[0]);
445 0         0 $task->[1]->fail($code => $remainder, 'expected ' . $task->[0]);
446             }
447 2         19 $self->{multi} = [];
448 2 50       3 $self->check_next_task unless @{$self->{pending}};
  2         13  
449             }
450             }
451              
452             =head2 add_task
453              
454             Add another task to the queue.
455              
456             =cut
457              
458             sub add_task {
459 2     2 1 5 my $self = shift;
460 2         5 my $task = shift;
461 2         4 push @{$self->{task_queue}}, $task;
  2         8  
462 2 50       8 $self->check_next_task unless $self->have_active_task;
463             }
464              
465             1;
466              
467             =head1 AUTHOR
468              
469             Tom Molesworth
470              
471             =head1 LICENSE
472              
473             Copyright Tom Molesworth 2012-2014. Licensed under the same terms as Perl itself.