File Coverage

blib/lib/Mail/Transport/POP3.pm
Criterion Covered Total %
statement 30 221 13.5
branch 0 130 0.0
condition 0 41 0.0
subroutine 10 33 30.3
pod 19 20 95.0
total 59 445 13.2


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Mail-Box-POP3 version 4.01.
2             # The POD got stripped from this file by OODoc version 3.05.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2001-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package Mail::Transport::POP3;{
13             our $VERSION = '4.01';
14             }
15              
16 4     4   28 use parent 'Mail::Transport::Receive';
  4         14  
  4         34  
17              
18 4     4   16801 use strict;
  4         17  
  4         105  
19 4     4   19 use warnings;
  4         8  
  4         234  
20              
21 4     4   22 use Log::Report 'mail-box-pop3', import => [ qw/error fault __x/ ];
  4         9  
  4         25  
22              
23 4     4   3322 use IO::Socket ();
  4         121895  
  4         143  
24 4     4   2917 use IO::Socket::IP ();
  4         42853  
  4         222  
25 4     4   4904 use IO::Socket::SSL qw/SSL_VERIFY_NONE/;
  4         445649  
  4         58  
26 4     4   715 use Socket qw/$CRLF/;
  4         9  
  4         768  
27 4     4   36 use Digest::MD5 qw/md5_hex/;
  4         17  
  4         382  
28 4     4   3087 use MIME::Base64 qw/encode_base64/;
  4         4770  
  4         16456  
29              
30             #--------------------
31              
32 0   0 0     sub _OK($) { substr(shift // '', 0, 3) eq '+OK' }
33              
34             sub init($)
35 0     0 0   { my ($self, $args) = @_;
36 0           $args->{via} = 'pop3';
37 0   0       $args->{port} ||= 110;
38              
39 0 0         $self->SUPER::init($args) or return;
40              
41 0   0       $self->{MTP_auth} = $args->{authenticate} || 'AUTO';
42 0           $self->{MTP_ssl} = $args->{use_ssl};
43              
44 0   0       my $opts = $self->{MTP_ssl_opts} = $args->{ssl_options} || {};
45 0   0       $opts->{verify_hostname} ||= 0;
46 0   0       $opts->{SSL_verify_mode} ||= SSL_VERIFY_NONE;
47              
48 0 0         $self->socket or return; # establish connection
49 0           $self;
50             }
51              
52             #--------------------
53              
54 0     0 1   sub useSSL() { $_[0]->{MTP_ssl} }
55              
56              
57 0     0 1   sub SSLOptions() { $_[0]->{MTP_ssl_opts} }
58              
59             #--------------------
60              
61             sub ids(;@)
62 0     0 1   { my $self = shift;
63 0 0         $self->socket or return;
64 0 0         wantarray ? @{$self->{MTP_n2uidl}} : $self->{MTP_n2uidl};
  0            
65             }
66              
67              
68             sub messages()
69 0     0 1   { my $self = shift;
70              
71             wantarray
72 0 0         or error __x"cannot get all messages of pop3 at once via messages().";
73              
74 0           $self->{MTP_messages};
75             }
76              
77              
78 0     0 1   sub folderSize() { $_[0]->{MTP_folder_size} }
79              
80              
81             sub header($;$)
82 0     0 1   { my ($self, $uidl, $bodylines) = @_;
83 0 0         $uidl or return;
84              
85 0   0       $bodylines //= 0;;
86 0 0         my $socket = $self->socket or return;
87 0 0         my $n = $self->id2n($uidl) or return;
88              
89 0           $self->sendList($socket, "TOP $n $bodylines$CRLF");
90             }
91              
92              
93             sub message($;$)
94 0     0 1   { my ($self, $uidl) = @_;
95 0 0         $uidl or return;
96              
97 0 0         my $socket = $self->socket or return;
98 0 0         my $n = $self->id2n($uidl) or return;
99 0 0         my $message = $self->sendList($socket, "RETR $n$CRLF") or return;
100              
101             # Some POP3 servers add a trailing empty line
102 0 0 0       pop @$message if @$message && $message->[-1] =~ m/^[\012\015]*$/;
103              
104             $self->{MTP_fetched}{$uidl} = undef # mark this ID as fetched
105 0 0         unless exists $self->{MTP_nouidl};
106              
107 0           $message;
108             }
109              
110              
111             sub messageSize($)
112 0     0 1   { my ($self, $uidl) = @_;
113 0 0         $uidl or return;
114              
115 0           my $list;
116 0 0         unless($list = $self->{MTP_n2length})
117 0 0         { my $socket = $self->socket or return;
118 0 0         my $raw = $self->sendList($socket, "LIST$CRLF") or return;
119 0           my @n2length;
120 0           foreach (@$raw)
121 0           { m#^(\d+) (\d+)#;
122 0           $n2length[$1] = $2;
123             }
124 0           $self->{MTP_n2length} = $list = \@n2length;
125             }
126              
127 0 0         my $n = $self->id2n($uidl) or return;
128 0           $list->[$n];
129             }
130              
131              
132             sub deleted($@)
133 0   0 0 1   { my $dele = shift->{MTP_dele} ||= {};
134 0 0         (shift) ? @$dele{ @_ } = () : delete @$dele{ @_ };
135             }
136              
137              
138             sub deleteFetched()
139 0     0 1   { my $self = shift;
140 0           $self->deleted(1, keys %{$self->{MTP_fetched}});
  0            
141             }
142              
143              
144             sub disconnect()
145 0     0 1   { my $self = shift;
146              
147 0           my $quit;
148 0 0         if($self->{MTP_socket}) # can only disconnect once
149 0 0         { if(my $socket = $self->socket)
150 0   0       { my $dele = $self->{MTP_dele} || {};
151 0           while(my $uidl = each %$dele)
152 0 0         { my $n = $self->id2n($uidl) or next;
153 0 0         $self->send($socket, "DELE $n$CRLF") or last;
154             }
155              
156 0           $quit = $self->send($socket, "QUIT$CRLF");
157 0           close $socket;
158             }
159             }
160              
161 0           delete @$self{ qw(MTP_socket MTP_dele MTP_uidl2n MTP_n2uidl MTP_n2length MTP_fetched) };
162 0           _OK $quit;
163             }
164              
165              
166             sub fetched(;$)
167 0     0 1   { my $self = shift;
168 0 0         return if exists $self->{MTP_nouidl};
169 0           $self->{MTP_fetched};
170             }
171              
172              
173 0     0 1   sub id2n($$) { $_[0]->{MTP_uidl2n}{$_[1]} }
174              
175             #--------------------
176              
177             sub socket()
178 0     0 1   { my $self = shift;
179              
180             # Do we (still) have a working connection which accepts commands?
181 0           my $socket = $self->_connection;
182 0 0         return $socket if defined $socket;
183              
184             exists $self->{MTP_nouidl}
185 0 0         or error __x"can not re-connect reliably to server which doesn't support UIDL";
186              
187             # (Re-)establish the connection
188 0 0         $socket = $self->login or return;
189 0 0         $self->status($socket) or return;
190 0           $self->{MTP_socket} = $socket;
191             }
192              
193              
194              
195             sub send($$)
196 0     0 1   { my $self = shift;
197 0           my $socket = shift;
198 0           my $response;
199              
200 0 0         if(eval { print $socket @_} )
  0            
201 0           { $response = <$socket>;
202 0 0         defined $response or fault __x"cannot read POP3 from socket";
203             }
204             else
205 0           { error __x"cannot write POP3 to socket: {error}", error => $@;
206             }
207 0           $response;
208             }
209              
210              
211             sub sendList($$)
212 0     0 1   { my ($self, $socket) = (shift, shift);
213 0           my $response = $self->send($socket, @_);
214 0 0 0       $response && _OK $response or return;
215              
216 0           my @list;
217 0           while(my $line = <$socket>)
218 0 0         { last if $line =~ m#^\.\r?\n#s;
219 0           $line =~ s#^\.##;
220 0           push @list, $line;
221             }
222              
223 0           \@list;
224             }
225              
226             sub DESTROY()
227 0     0     { my $self = shift;
228 0           $self->SUPER::DESTROY;
229 0 0         $self->disconnect if $self->{MTP_socket}; # only when open
230             }
231              
232             sub _connection()
233 0     0     { my $self = shift;
234 0   0       my $socket = $self->{MTP_socket} // return;
235              
236             # Check if we (still) got a connection
237 0           eval { print $socket "NOOP$CRLF" };
  0            
238 0 0 0       if($@ || ! <$socket> )
239 0           { delete $self->{MTP_socket};
240 0           return undef;
241             }
242              
243 0           $socket;
244             }
245              
246              
247              
248             sub login(;$)
249 0     0 1   { my $self = shift;
250              
251             # Check if we can make a connection
252              
253 0           my ($host, $port, $username, $password) = $self->remoteHost;
254 0 0 0       $username && $password
255             or error __x"POP3 requires a username and password.";
256              
257 0           my $socket;
258 0 0         if($self->useSSL)
259 0           { my $opts = $self->SSLOptions;
260 0           $socket = eval { IO::Socket::SSL->new(PeerAddr => "$host:$port", %$opts) };
  0            
261             }
262             else
263 0           { $socket = eval { IO::Socket::IP->new("$host:$port") };
  0            
264             }
265              
266 0 0         $socket
267             or fault __x"cannot connect to {service} for POP3", service => "$host:$port";
268              
269             # Check if it looks like a POP server
270              
271 0           my $connected;
272 0           my $authenticate = $self->{MTP_auth};
273 0           my $welcome = <$socket>;
274 0 0         _OK $welcome
275             or error __x"server at {service} does not seem to be talking POP3.", service => "$host:$port";
276              
277             # Check APOP login if automatic or APOP specifically requested
278 0 0 0       if($authenticate eq 'AUTO' || $authenticate eq 'APOP')
279 0 0         { if($welcome =~ m#^\+OK .*(<\d+\.\d+\@[^>]+>)#)
280 0           { my $md5 = md5_hex $1.$password;
281 0           my $response = $self->send($socket, "APOP $username $md5$CRLF");
282 0           $connected = _OK $response;
283             }
284             }
285              
286             # Check USER/PASS login if automatic and failed or LOGIN specifically
287             # requested.
288 0 0         unless($connected)
289 0 0 0       { if($authenticate eq 'AUTO' || $authenticate eq 'LOGIN')
290 0 0         { my $response = $self->send($socket, "USER $username$CRLF") or return;
291              
292 0 0         if(_OK $response)
293 0 0         { my $response2 = $self->send($socket, "PASS $password$CRLF") or return;
294 0           $connected = _OK $response2;
295             }
296             }
297             }
298              
299             # Try OAUTH2 login
300 0 0 0       if(! $connected && $authenticate =~ /^OAUTH2/)
301             { # Borrowed from Net::POP3::XOAuth2 0.0.2 by Kizashi Nagata (also Perl license)
302 0           my $token = encode_base64 "user=$username\001auth=Bearer $password\001\001";
303 0           $token =~ s/[\r\n]//g; # no base64 newlines, anywhere
304              
305 0 0         if($authenticate eq 'OAUTH2_SEP')
306             { # Microsofts way
307             # https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
308 0 0         my $response = $self->send($socket, "AUTH XOAUTH2$CRLF") or return;
309              
310 0 0         if($response =~ /^\+/) # Office365 sends + here, not +OK
311 0 0         { my $response2 = $self->send($socket, "$token$CRLF") or return;
312 0           $connected = _OK $response2;
313             }
314             }
315             else
316 0 0         { my $response = $self->send($socket, "AUTH XOAUTH2 $token$CRLF") or return;
317 0           $connected = _OK $response;
318             }
319             }
320              
321             # If we're still not connected now, we have an error
322 0 0         unless($connected)
323 0 0         { $authenticate eq 'AUTO'
324             ? (error __x"could not authenticate using any login method.")
325             : (error __x"could not authenticate using '{type}' method", type => $authenticate);
326             }
327              
328 0           $socket;
329             }
330              
331              
332              
333             sub status($;$)
334 0     0 1   { my ($self, $socket) = @_;
335              
336             # Check if we can do a STAT
337              
338 0 0         my $stat = $self->send($socket, "STAT$CRLF") or return;
339 0 0         if($stat !~ m#^\+OK (\d+) (\d+)#)
340 0           { delete $self->{MTP_messages};
341 0           delete $self->{MTP_size};
342 0           error __x"POP3 Could not do a STAT";
343 0           return;
344             }
345 0           $self->{MTP_messages} = my $nr_msgs = $1;
346 0           $self->{MTP_folder_size} = $2;
347              
348             # Check if we can do a UIDL
349              
350 0 0         my $uidl = $self->send($socket, "UIDL$CRLF") or return;
351 0           $self->{MTP_nouidl} = undef;
352 0           delete $self->{MTP_uidl2n}; # drop the reverse lookup: UIDL -> number
353              
354 0 0         if(_OK $uidl)
355 0           { my @n2uidl;
356 0           $n2uidl[$nr_msgs] = undef; # pre-alloc
357              
358 0           while(my $line = <$socket>)
359 0 0         { last if substr($line, 0, 1) eq '.';
360 0 0         $line =~ m#^(\d+) (.+?)\r?\n# or next;
361 0           $n2uidl[$1] = $2;
362             }
363              
364 0           shift @n2uidl; # make message 1 into index 0
365 0           $self->{MTP_n2uidl} = \@n2uidl;
366 0           delete $self->{MTP_n2length};
367 0           delete $self->{MTP_nouidl};
368             }
369             else
370             { # We can't do UIDL, we need to fake it
371 0 0         my $list = $self->send($socket, "LIST$CRLF") or return;
372 0           my (@n2length, @n2uidl);
373              
374 0 0         if(_OK $list)
375 0           { $n2length[$nr_msgs] = $n2uidl[$nr_msgs] = undef; # alloc all
376              
377 0           my ($host, $port) = $self->remoteHost;
378 0           while(my $line = <$socket>)
379 0 0         { last if substr($line, 0, 1) eq '.';
380 0 0         $line =~ m#^(\d+) (\d+)# or next;
381 0           $n2length[$1] = $2;
382 0           $n2uidl[$1] = "$host:$port:$1"; # fake UIDL, for id only
383             }
384 0           shift @n2length; shift @n2uidl; # make 1st message in index 0
  0            
385             }
386 0           $self->{MTP_n2length} = \@n2length;
387 0           $self->{MTP_n2uidl} = \@n2uidl;
388             }
389              
390 0           my $i = 1;
391 0           my %uidl2n = map +($_ => $i++), @{$self->{MTP_n2uidl}};
  0            
392 0           $self->{MTP_uidl2n} = \%uidl2n;
393              
394 0           1;
395             }
396              
397             #--------------------
398              
399             sub url(;$)
400 0     0 1   { my $self = shift;
401 0           my ($host, $port, $user, $pwd) = $self->remoteHost;
402 0 0         my $proto = $self->useSSL ? 'pop3s' : 'pop3';
403 0           "$proto://$user:$pwd\@$host:$port";
404             }
405              
406             #--------------------
407              
408             1;