File Coverage

blib/lib/Net/POP3.pm
Criterion Covered Total %
statement 91 289 31.4
branch 22 156 14.1
condition 8 81 9.8
subroutine 19 58 32.7
pod 26 26 100.0
total 166 610 27.2


line stmt bran cond sub pod time code
1             # Net::POP3.pm
2             #
3             # Copyright (C) 1995-2004 Graham Barr. All rights reserved.
4             # Copyright (C) 2013-2016, 2020 Steve Hay. All rights reserved.
5             # This module is free software; you can redistribute it and/or modify it under
6             # the same terms as Perl itself, i.e. under the terms of either the GNU General
7             # Public License or the Artistic License, as specified in the F file.
8              
9             package Net::POP3;
10              
11 5     5   317894 use 5.008001;
  5         57  
12              
13 5     5   21 use strict;
  5         11  
  5         85  
14 5     5   17 use warnings;
  5         10  
  5         102  
15              
16 5     5   20 use Carp;
  5         30  
  5         239  
17 5     5   1653 use IO::Socket;
  5         31735  
  5         27  
18 5     5   3556 use Net::Cmd;
  5         11  
  5         288  
19 5     5   1900 use Net::Config;
  5         10  
  5         597  
20              
21             our $VERSION = "3.14";
22              
23             # Code for detecting if we can use SSL
24             my $ssl_class = eval {
25             require IO::Socket::SSL;
26             # first version with default CA on most platforms
27 5     5   34 no warnings 'numeric';
  5         11  
  5         438  
28             IO::Socket::SSL->VERSION(2.007);
29             } && 'IO::Socket::SSL';
30              
31             my $nossl_warn = !$ssl_class &&
32             'To use SSL please install IO::Socket::SSL with version>=2.007';
33              
34             # Code for detecting if we can use IPv6
35             my $family_key = 'Domain';
36             my $inet6_class = eval {
37             require IO::Socket::IP;
38 5     5   28 no warnings 'numeric';
  5         5  
  5         263  
39             IO::Socket::IP->VERSION(0.25) || die;
40             $family_key = 'Family';
41             } && 'IO::Socket::IP' || eval {
42             require IO::Socket::INET6;
43 5     5   23 no warnings 'numeric';
  5         10  
  5         16758  
44             IO::Socket::INET6->VERSION(2.62);
45             } && 'IO::Socket::INET6';
46              
47              
48 3     3 1 252 sub can_ssl { $ssl_class };
49 1     1 1 108 sub can_inet6 { $inet6_class };
50              
51             our @ISA = ('Net::Cmd', $inet6_class || 'IO::Socket::INET');
52              
53             sub new {
54 3     3 1 702603 my $self = shift;
55 3   33     222 my $type = ref($self) || $self;
56 3         33 my ($host, %arg);
57 3 50       69 if (@_ % 2) {
58 3         256 $host = shift;
59 3         73 %arg = @_;
60             }
61             else {
62 0         0 %arg = @_;
63 0         0 $host = delete $arg{Host};
64             }
65 3 50       30 my $hosts = defined $host ? [$host] : $NetConfig{pop3_hosts};
66 3         12 my $obj;
67              
68 3 100       59 if ($arg{SSL}) {
69             # SSL from start
70 2 50       28 die $nossl_warn if !$ssl_class;
71 2   50     60 $arg{Port} ||= 995;
72             }
73              
74 3 50       47 $arg{Timeout} = 120 if ! defined $arg{Timeout};
75              
76 3         6 foreach my $h (@{$hosts}) {
  3         13  
77             $obj = $type->SUPER::new(
78             PeerAddr => ($host = $h),
79             PeerPort => $arg{Port} || 'pop3(110)',
80             Proto => 'tcp',
81             $family_key => $arg{Domain} || $arg{Family},
82             LocalAddr => $arg{LocalAddr},
83             LocalPort => exists($arg{ResvPort}) ? $arg{ResvPort} : $arg{LocalPort},
84             Timeout => $arg{Timeout},
85             )
86 3 50 100     525 and last;
    50 33        
87             }
88              
89             return
90 3 50       5120 unless defined $obj;
91              
92 3         6 ${*$obj}{'net_pop3_arg'} = \%arg;
  3         9  
93 3         22 ${*$obj}{'net_pop3_host'} = $host;
  3         8  
94 3 100       8 if ($arg{SSL}) {
95 2 50       64 Net::POP3::_SSL->start_SSL($obj,%arg) or return;
96             }
97              
98 3         37 $obj->autoflush(1);
99 3 50       372 $obj->debug(exists $arg{Debug} ? $arg{Debug} : undef);
100              
101 3 50       70 unless ($obj->response() == CMD_OK) {
102 0         0 $obj->close();
103 0         0 return;
104             }
105              
106 3         29 ${*$obj}{'net_pop3_banner'} = $obj->message;
  3         10  
107              
108 3         13 $obj;
109             }
110              
111              
112             sub host {
113 0     0 1 0 my $me = shift;
114 0         0 ${*$me}{'net_pop3_host'};
  0         0  
115             }
116              
117             ##
118             ## We don't want people sending me their passwords when they report problems
119             ## now do we :-)
120             ##
121              
122              
123 0 0   0 1 0 sub debug_text { $_[2] =~ /^(pass|rpop)/i ? "$1 ....\n" : $_[2]; }
124              
125              
126             sub login {
127 0 0 0 0 1 0 @_ >= 1 && @_ <= 3 or croak 'usage: $pop3->login([$user[, $pass]])';
128 0         0 my ($me, $user, $pass) = @_;
129              
130 0 0       0 if (@_ <= 2) {
131 0         0 ($user, $pass) = $me->_lookup_credentials($user);
132             }
133              
134 0 0       0 $me->user($user)
135             and $me->pass($pass);
136             }
137              
138             sub starttls {
139 1     1 1 1073 my $self = shift;
140 1 50       4 $ssl_class or die $nossl_warn;
141 1 50       16 $self->_STLS or return;
142             Net::POP3::_SSL->start_SSL($self,
143 1 50       24 %{ ${*$self}{'net_pop3_arg'} }, # (ssl) args given in new
  1         3  
  1         27  
144             @_ # more (ssl) args
145             ) or return;
146 1         5 return 1;
147             }
148              
149             sub apop {
150 0 0 0 0 1 0 @_ >= 1 && @_ <= 3 or croak 'usage: $pop3->apop([$user[, $pass]])';
151 0         0 my ($me, $user, $pass) = @_;
152 0         0 my $banner;
153             my $md;
154              
155 0 0       0 if (eval { local $SIG{__DIE__}; require Digest::MD5 }) {
  0 0       0  
  0         0  
156 0         0 $md = Digest::MD5->new();
157             }
158 0         0 elsif (eval { local $SIG{__DIE__}; require MD5 }) {
  0         0  
159 0         0 $md = MD5->new();
160             }
161             else {
162 0         0 carp "You need to install Digest::MD5 or MD5 to use the APOP command";
163 0         0 return;
164             }
165              
166             return
167 0 0       0 unless ($banner = (${*$me}{'net_pop3_banner'} =~ /(<.*>)/)[0]);
  0         0  
168              
169 0 0       0 if (@_ <= 2) {
170 0         0 ($user, $pass) = $me->_lookup_credentials($user);
171             }
172              
173 0         0 $md->add($banner, $pass);
174              
175             return
176 0 0       0 unless ($me->_APOP($user, $md->hexdigest));
177              
178 0         0 $me->_get_mailbox_count();
179             }
180              
181              
182             sub user {
183 0 0   0 1 0 @_ == 2 or croak 'usage: $pop3->user($user)';
184 0 0       0 $_[0]->_USER($_[1]) ? 1 : undef;
185             }
186              
187              
188             sub pass {
189 0 0   0 1 0 @_ == 2 or croak 'usage: $pop3->pass($pass)';
190              
191 0         0 my ($me, $pass) = @_;
192              
193             return
194 0 0       0 unless ($me->_PASS($pass));
195              
196 0         0 $me->_get_mailbox_count();
197             }
198              
199              
200             sub reset {
201 0 0   0 1 0 @_ == 1 or croak 'usage: $obj->reset()';
202              
203 0         0 my $me = shift;
204              
205 0 0       0 return 0
206             unless ($me->_RSET);
207              
208 0 0       0 if (defined ${*$me}{'net_pop3_mail'}) {
  0         0  
209 0         0 local $_;
210 0         0 foreach (@{${*$me}{'net_pop3_mail'}}) {
  0         0  
  0         0  
211 0         0 delete $_->{'net_pop3_deleted'};
212             }
213             }
214             }
215              
216              
217             sub last {
218 0 0   0 1 0 @_ == 1 or croak 'usage: $obj->last()';
219              
220             return
221 0 0 0     0 unless $_[0]->_LAST && $_[0]->message =~ /(\d+)/;
222              
223 0         0 return $1;
224             }
225              
226              
227             sub top {
228 0 0 0 0 1 0 @_ == 2 || @_ == 3 or croak 'usage: $pop3->top($msgnum[, $numlines])';
229 0         0 my $me = shift;
230              
231             return
232 0 0 0     0 unless $me->_TOP($_[0], $_[1] || 0);
233              
234 0         0 $me->read_until_dot;
235             }
236              
237              
238             sub popstat {
239 0 0   0 1 0 @_ == 1 or croak 'usage: $pop3->popstat()';
240 0         0 my $me = shift;
241              
242             return ()
243 0 0 0     0 unless $me->_STAT && $me->message =~ /(\d+)\D+(\d+)/;
244              
245 0   0     0 ($1 || 0, $2 || 0);
      0        
246             }
247              
248              
249             sub list {
250 0 0 0 0 1 0 @_ == 1 || @_ == 2 or croak 'usage: $pop3->list([$msgnum])';
251 0         0 my $me = shift;
252              
253             return
254 0 0       0 unless $me->_LIST(@_);
255              
256 0 0       0 if (@_) {
257 0         0 $me->message =~ /\d+\D+(\d+)/;
258 0   0     0 return $1 || undef;
259             }
260              
261 0 0       0 my $info = $me->read_until_dot
262             or return;
263              
264 0         0 my %hash = map { (/(\d+)\D+(\d+)/) } @$info;
  0         0  
265              
266 0         0 return \%hash;
267             }
268              
269              
270             sub get {
271 0 0 0 0 1 0 @_ == 2 or @_ == 3 or croak 'usage: $pop3->get($msgnum[, $fh])';
272 0         0 my $me = shift;
273              
274             return
275 0 0       0 unless $me->_RETR(shift);
276              
277 0         0 $me->read_until_dot(@_);
278             }
279              
280              
281             sub getfh {
282 0 0   0 1 0 @_ == 2 or croak 'usage: $pop3->getfh($msgnum)';
283 0         0 my $me = shift;
284              
285 0 0       0 return unless $me->_RETR(shift);
286 0         0 return $me->tied_fh;
287             }
288              
289              
290             sub delete {
291 0 0   0 1 0 @_ == 2 or croak 'usage: $pop3->delete($msgnum)';
292 0         0 my $me = shift;
293 0 0       0 return 0 unless $me->_DELE(@_);
294 0         0 ${*$me}{'net_pop3_deleted'} = 1;
  0         0  
295             }
296              
297              
298             sub uidl {
299 0 0 0 0 1 0 @_ == 1 || @_ == 2 or croak 'usage: $pop3->uidl([$msgnum])';
300 0         0 my $me = shift;
301 0         0 my $uidl;
302              
303 0 0       0 $me->_UIDL(@_)
304             or return;
305 0 0       0 if (@_) {
306 0         0 $uidl = ($me->message =~ /\d+\s+([\041-\176]+)/)[0];
307             }
308             else {
309 0 0       0 my $ref = $me->read_until_dot
310             or return;
311 0         0 $uidl = {};
312 0         0 foreach my $ln (@$ref) {
313 0         0 my ($msg, $uid) = $ln =~ /^\s*(\d+)\s+([\041-\176]+)/;
314 0         0 $uidl->{$msg} = $uid;
315             }
316             }
317 0         0 return $uidl;
318             }
319              
320              
321             sub ping {
322 0 0   0 1 0 @_ == 2 or croak 'usage: $pop3->ping($user)';
323 0         0 my $me = shift;
324              
325 0 0 0     0 return () unless $me->_PING(@_) && $me->message =~ /(\d+)\D+(\d+)/;
326              
327 0   0     0 ($1 || 0, $2 || 0);
      0        
328             }
329              
330              
331             sub _lookup_credentials {
332 0     0   0 my ($me, $user) = @_;
333              
334 0         0 require Net::Netrc;
335              
336             $user ||= eval { local $SIG{__DIE__}; (getpwuid($>))[0] }
337             || $ENV{NAME}
338             || $ENV{USER}
339 0   0     0 || $ENV{LOGNAME};
      0        
340              
341 0         0 my $m = Net::Netrc->lookup(${*$me}{'net_pop3_host'}, $user);
  0         0  
342 0   0     0 $m ||= Net::Netrc->lookup(${*$me}{'net_pop3_host'});
  0         0  
343              
344 0 0 0     0 my $pass = $m
345             ? $m->password || ""
346             : "";
347              
348 0         0 ($user, $pass);
349             }
350              
351              
352             sub _get_mailbox_count {
353 0     0   0 my ($me) = @_;
354 0 0       0 my $ret = ${*$me}{'net_pop3_count'} =
  0         0  
355             ($me->message =~ /(\d+)\s+message/io) ? $1 : ($me->popstat)[0];
356              
357 0 0       0 $ret ? $ret : "0E0";
358             }
359              
360              
361 0     0   0 sub _STAT { shift->command('STAT' )->response() == CMD_OK }
362 0     0   0 sub _LIST { shift->command('LIST', @_)->response() == CMD_OK }
363 0     0   0 sub _RETR { shift->command('RETR', $_[0])->response() == CMD_OK }
364 0     0   0 sub _DELE { shift->command('DELE', $_[0])->response() == CMD_OK }
365 0     0   0 sub _NOOP { shift->command('NOOP' )->response() == CMD_OK }
366 0     0   0 sub _RSET { shift->command('RSET' )->response() == CMD_OK }
367 3     3   80 sub _QUIT { shift->command('QUIT' )->response() == CMD_OK }
368 0     0   0 sub _TOP { shift->command( 'TOP', @_)->response() == CMD_OK }
369 0     0   0 sub _UIDL { shift->command('UIDL', @_)->response() == CMD_OK }
370 0     0   0 sub _USER { shift->command('USER', $_[0])->response() == CMD_OK }
371 0     0   0 sub _PASS { shift->command('PASS', $_[0])->response() == CMD_OK }
372 0     0   0 sub _APOP { shift->command('APOP', @_)->response() == CMD_OK }
373 0     0   0 sub _PING { shift->command('PING', $_[0])->response() == CMD_OK }
374 0     0   0 sub _RPOP { shift->command('RPOP', $_[0])->response() == CMD_OK }
375 0     0   0 sub _LAST { shift->command('LAST' )->response() == CMD_OK }
376 0     0   0 sub _CAPA { shift->command('CAPA' )->response() == CMD_OK }
377 1     1   20 sub _STLS { shift->command("STLS", )->response() == CMD_OK }
378              
379              
380             sub quit {
381 3     3 1 2795 my $me = shift;
382              
383 3         99 $me->_QUIT;
384 3         21 $me->close;
385             }
386              
387              
388             sub DESTROY {
389 0     0   0 my $me = shift;
390              
391 0 0 0     0 if (defined fileno($me) and ${*$me}{'net_pop3_deleted'}) {
  0         0  
392 0         0 $me->reset;
393 0         0 $me->quit;
394             }
395             }
396              
397             ##
398             ## POP3 has weird responses, so we emulate them to look the same :-)
399             ##
400              
401              
402             sub response {
403 7     7 1 20 my $cmd = shift;
404 7 50       60 my $str = $cmd->getline() or return;
405 7         3388 my $code = "500";
406              
407 7 50       29 $cmd->debug_print(0, $str)
408             if ($cmd->debug);
409              
410 7 50       79 if ($str =~ s/^\+OK\s*//io) {
    0          
411 7         15 $code = "200";
412             }
413             elsif ($str =~ s/^\+\s*//io) {
414 0         0 $code = "300";
415             }
416             else {
417 0         0 $str =~ s/^-ERR\s*//io;
418             }
419              
420 7         21 ${*$cmd}{'net_cmd_resp'} = [$str];
  7         44  
421 7         17 ${*$cmd}{'net_cmd_code'} = $code;
  7         42  
422              
423 7         39 substr($code, 0, 1);
424             }
425              
426              
427             sub capa {
428 0     0 1 0 my $this = shift;
429 0         0 my ($capa, %capabilities);
430              
431             # Fake a capability here
432 0 0       0 $capabilities{APOP} = '' if ($this->banner() =~ /<.*>/);
433              
434 0 0       0 if ($this->_CAPA()) {
435 0         0 $capabilities{CAPA} = 1;
436 0         0 $capa = $this->read_until_dot();
437 0         0 %capabilities = (%capabilities, map {/^\s*(\S+)\s*(.*)/} @$capa);
  0         0  
438             }
439             else {
440              
441             # Check AUTH for SASL capabilities
442 0 0       0 if ($this->command('AUTH')->response() == CMD_OK) {
443 0         0 my $mechanism = $this->read_until_dot();
444 0         0 $capabilities{SASL} = join " ", map {m/([A-Z0-9_-]+)/} @{$mechanism};
  0         0  
  0         0  
445             }
446             }
447              
448 0         0 return ${*$this}{'net_pop3e_capabilities'} = \%capabilities;
  0         0  
449             }
450              
451              
452             sub capabilities {
453 0     0 1 0 my $this = shift;
454              
455 0 0       0 ${*$this}{'net_pop3e_capabilities'} || $this->capa;
  0         0  
456             }
457              
458              
459             sub auth {
460 0     0 1 0 my ($self, $username, $password) = @_;
461              
462 0 0       0 eval {
463 0         0 require MIME::Base64;
464 0         0 require Authen::SASL;
465             } or $self->set_status(500, ["Need MIME::Base64 and Authen::SASL todo auth"]), return 0;
466              
467 0         0 my $capa = $self->capa;
468 0   0     0 my $mechanisms = $capa->{SASL} || 'CRAM-MD5';
469              
470 0         0 my $sasl;
471              
472 0 0 0     0 if (ref($username) and UNIVERSAL::isa($username, 'Authen::SASL')) {
473 0         0 $sasl = $username;
474 0   0     0 my $user_mech = $sasl->mechanism || '';
475 0         0 my @user_mech = split(/\s+/, $user_mech);
476 0         0 my %user_mech;
477 0         0 @user_mech{@user_mech} = ();
478              
479 0         0 my @server_mech = split(/\s+/, $mechanisms);
480             my @mech = @user_mech
481 0 0       0 ? grep { exists $user_mech{$_} } @server_mech
  0         0  
482             : @server_mech;
483 0 0       0 unless (@mech) {
484 0         0 $self->set_status(
485             500,
486             [ 'Client SASL mechanisms (',
487             join(', ', @user_mech),
488             ') do not match the SASL mechnism the server announces (',
489             join(', ', @server_mech), ')',
490             ]
491             );
492 0         0 return 0;
493             }
494              
495 0         0 $sasl->mechanism(join(" ", @mech));
496             }
497             else {
498 0 0       0 die "auth(username, password)" if not length $username;
499 0         0 $sasl = Authen::SASL->new(
500             mechanism => $mechanisms,
501             callback => {
502             user => $username,
503             pass => $password,
504             authname => $username,
505             }
506             );
507             }
508              
509             # We should probably allow the user to pass the host, but I don't
510             # currently know and SASL mechanisms that are used by smtp that need it
511 0         0 my ($hostname) = split /:/, ${*$self}{'net_pop3_host'};
  0         0  
512 0         0 my $client = eval { $sasl->client_new('pop', $hostname, 0) };
  0         0  
513              
514 0 0       0 unless ($client) {
515 0         0 my $mech = $sasl->mechanism;
516 0         0 $self->set_status(
517             500,
518             [ " Authen::SASL failure: $@",
519             '(please check if your local Authen::SASL installation',
520             "supports mechanism '$mech'"
521             ]
522             );
523 0         0 return 0;
524             }
525              
526             my ($token) = $client->client_start
527 0 0       0 or do {
528 0         0 my $mech = $client->mechanism;
529 0         0 $self->set_status(
530             500,
531             [ ' Authen::SASL failure: $client->client_start ',
532             "mechanism '$mech' hostname #$hostname#",
533             $client->error
534             ]
535             );
536 0         0 return 0;
537             };
538              
539             # We don't support sasl mechanisms that encrypt the socket traffic.
540             # todo that we would really need to change the ISA hierarchy
541             # so we don't inherit from IO::Socket, but instead hold it in an attribute
542              
543 0         0 my @cmd = ("AUTH", $client->mechanism);
544 0         0 my $code;
545              
546 0 0 0     0 push @cmd, MIME::Base64::encode_base64($token, '')
547             if defined $token and length $token;
548              
549 0         0 while (($code = $self->command(@cmd)->response()) == CMD_MORE) {
550              
551 0 0       0 my ($token) = $client->client_step(MIME::Base64::decode_base64(($self->message)[0])) or do {
552 0         0 $self->set_status(
553             500,
554             [ ' Authen::SASL failure: $client->client_step ',
555             "mechanism '", $client->mechanism, " hostname #$hostname#, ",
556             $client->error
557             ]
558             );
559 0         0 return 0;
560             };
561              
562 0 0       0 @cmd = (MIME::Base64::encode_base64(defined $token ? $token : '', ''));
563             }
564              
565 0         0 $code == CMD_OK;
566             }
567              
568              
569             sub banner {
570 0     0 1 0 my $this = shift;
571              
572 0         0 return ${*$this}{'net_pop3_banner'};
  0         0  
573             }
574              
575             {
576             package Net::POP3::_SSL;
577             our @ISA = ( $ssl_class ? ($ssl_class):(), 'Net::POP3' );
578 0     0   0 sub starttls { die "POP3 connection is already in SSL mode" }
579             sub start_SSL {
580 3     3   30 my ($class,$pop3,%arg) = @_;
581 3         12 delete @arg{ grep { !m{^SSL_} } keys %arg };
  16         111  
582 3   33     15 ( $arg{SSL_verifycn_name} ||= $pop3->host )
583             =~s{(?
584             $arg{SSL_hostname} = $arg{SSL_verifycn_name}
585 3 50 33     106 if ! defined $arg{SSL_hostname} && $class->can_client_sni;
586 3   50     56 $arg{SSL_verifycn_scheme} ||= 'pop3';
587 3         46 my $ok = $class->SUPER::start_SSL($pop3,%arg);
588 3 50       23155 $@ = $ssl_class->errstr if !$ok;
589 3         22 return $ok;
590             }
591             }
592              
593              
594              
595             1;
596              
597             __END__