diff options
-rw-r--r-- | Changelog | 7 | ||||
-rw-r--r-- | INSTALL | 8 | ||||
-rwxr-xr-x | interimap | 8 | ||||
-rw-r--r-- | interimap.1 | 63 | ||||
-rw-r--r-- | interimap.sample | 6 | ||||
-rw-r--r-- | lib/Net/IMAP/InterIMAP.pm | 484 |
6 files changed, 356 insertions, 220 deletions
@@ -16,6 +16,13 @@ interimap (0.2) upstream; and NO_COMPRESSION to the compiled-in CTX options. * Use SSL_MODE_AUTO_RETRY to avoid SSL_read failures during a handshake. + * Rename the 'SSL_verify_trusted_peer', 'SSL_ca_path', and + 'SSL_cipher_list' options to 'SSL_CApath', 'SSL_verify' and + 'SSL_cipherlist', respectively. + * Add an option 'SSL_CAfile' to specify a file containing trusted + certificates to use during server certificate authentication. + * Replace IO::Socket::SSL dependency by the lower level Net::SSLeay. + * Accept non-fully qualified commands. -- Guilhem Moulin <guilhem@guilhem.org> Wed, 09 Sep 2015 00:44:35 +0200 @@ -1,6 +1,6 @@ InterIMAP depends on the following Perl modules: - - Compress::Zlib (core module) + - Compress::Raw::Zlib (core module) - Config::Tiny - DBI - DBD::SQLite @@ -8,8 +8,8 @@ InterIMAP depends on the following Perl modules: - Getopt::Long (core module) - MIME::Base64 (core module) if authentication is required - IO::Select (core module) - - IO::Socket::INET (core module) for 'type=imap' - - IO::Socket::SSL for 'type=imaps' (or 'type=imap' and 'STARTTLS=YES') + - IO::Socket::INET (core module) for 'type=imap' or 'type=imaps' + - Net::SSLeay - List::Util (core module) - POSIX (core module) - Socket (core module) @@ -18,7 +18,7 @@ InterIMAP depends on the following Perl modules: On Debian GNU/Linux systems, these modules can be installed with the following command: - apt-get install libconfig-tiny-perl libdbi-perl libdbd-sqlite3-perl libio-socket-ssl-perl + apt-get install libconfig-tiny-perl libdbi-perl libdbd-sqlite3-perl libnet-ssleay-perl However Debian GNU/Linux users can also use gbp(1) from git-buildpackage to build their own package: @@ -249,7 +249,7 @@ foreach my $name (qw/local remote/) { $config{enable} = 'QRESYNC'; $config{name} = $name; $config{'logger-fd'} = $LOGGER_FD if defined $LOGGER_FD; - $config{'compress'} //= ($name eq 'local' ? 'NO' : 'YES'); + $config{'compress'} //= ($name eq 'local' ? 0 : 1); $IMAP->{$name} = { client => Net::IMAP::InterIMAP::->new(%config) }; my $client = $IMAP->{$name}->{client}; @@ -514,7 +514,7 @@ sub sync_mailbox_list() { sync_mailbox_list(); ($lIMAP, $rIMAP) = map {$IMAP->{$_}->{client}} qw/local remote/; my $ATTRS = 'MODSEQ FLAGS INTERNALDATE '. - (((!defined $CONF->{_} or uc ($CONF->{_}->{'use-binary'} // 'YES') eq 'YES') and + (((!defined $CONF->{_} or $CONF->{_}->{'use-binary'} // 1) and !$lIMAP->incapable('BINARY') and !$rIMAP->incapable('BINARY')) ? 'BINARY' : 'BODY').'.PEEK[]'; @@ -971,8 +971,8 @@ sub sync_known_messages($$) { sub callback_new_message($$$$;$$$) { my ($idx, $mailbox, $name, $mail, $UIDs, $buff, $bufflen) = @_; - my $length = defined $mail->{RFC822} ? length($mail->{RFC822}) - : defined $mail->{BINARY} ? length($mail->{BINARY}) + my $length = defined $mail->{RFC822} ? length(${$mail->{RFC822}}) + : defined $mail->{BINARY} ? length(${$mail->{BINARY}}) : return; # not for us if ($length == 0) { msg("$name($mailbox)", "WARNING: Ignoring new 0-length message (UID $mail->{UID})"); diff --git a/interimap.1 b/interimap.1 index 988fa16..7ac8204 100644 --- a/interimap.1 +++ b/interimap.1 @@ -282,33 +282,6 @@ pre\-authenticated connections, depending on the chosen authentication mechanism. .TP -.I SSL_cipher_list -Cipher list to use for the connection. -See \fIciphers\fR(1ssl) for the format of such list. - -.TP -.I SSL_fingerprint -Fingerprint of the server certificate in the form -\fIALGO\fR$\fIDIGEST_HEX\fR, where \fIALGO\fR is the used algorithm -(default \(lqsha256\(rq). -Attempting to connect to a server with a non-matching certificate -fingerprint causes \fBInterIMAP\fR to abort the connection immediately -after the SSL/TLS handshake. - -.TP -.I SSL_verify_trusted_peer -Whether to verify that the peer certificate has been signed by a trusted -Certificate Authority. Note that using \fISSL_fingerprint\fR to specify -the fingerprint of the server certificate is orthogonal and does not -rely on Certificate Authorities. -(Default: \(lqYES\(rq.) - -.TP -.I SSL_ca_path -Directory containing the certificate(s) of the trusted Certificate -Authorities, used for server certificate verification. - -.TP .I compress Whether to use the IMAP COMPRESS extension [RFC4978] for servers advertizing it. @@ -333,6 +306,42 @@ This option is only available in the default section, and is ignored if either server does not advertize \(lqBINARY\(rq in its capability list. (Default: \(lqYES\(rq.) +.TP +.I SSL_cipher_list +The cipher list to send to the server. Although the server determines +which cipher suite is used, it should take the first supported cipher in +the list sent by the client. See \fBciphers\fR(1ssl) for more +information. + +.TP +.I SSL_fingerprint +Fingerprint of the server certificate (or its public key) in the form +\fIALGO\fR$\fIDIGEST_HEX\fR, where \fIALGO\fR is the used algorithm +(default \(lqsha256\(rq). +Attempting to connect to a server with a non-matching certificate +fingerprint causes \fBInterIMAP\fR to abort the connection immediately +after the SSL/TLS handshake. + +.TP +.I SSL_verify +Whether to verify the server certificate chain. +Note that using \fISSL_fingerprint\fR to specify the fingerprint of the +server certificate is an orthogonal authentication measure as it ignores +the CA chain. +(Default: \(lqYES\(rq.) + +.TP +.I SSL_CApath +Directory to use for server certificate verification if +\(lq\fISSL_verify\fR=YES\(rq. +This directory must be in \(lqhash format\(rq, see \fBverify\fR(1ssl) +for more information. + +.TP +.I SSL_CAfile +File containing trusted certificates to use during server certificate +authentication if \(lq\fISSL_verify\fR=YES\(rq. + .SH SUPPORTED EXTENSIONS Performance is better for servers supporting the following extensions to diff --git a/interimap.sample b/interimap.sample index b0f619a..bbf8c42 100644 --- a/interimap.sample +++ b/interimap.sample @@ -18,9 +18,9 @@ password = xxxxxxxxxxxxxxxx #compress = YES # SSL options -#SSL_cipher_list = EECDH+AES:EDH+AES:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1 +SSL_CApath = /etc/ssl/certs +#SSL_verify = YES +#SSL_cipherlist = EECDH+AES:EDH+AES:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1 #SSL_fingerprint = sha256$62E436BB329C46A628314C49BDA7C2A2E86C57B2021B9A964B8FABB6540D3605 -#SSL_verify_trusted_peer = YES -SSL_ca_path = /etc/ssl/certs # vim:ft=dosini diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index 65a0c10..57f002e 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -20,16 +20,20 @@ package Net::IMAP::InterIMAP v0.0.2; use warnings; use strict; -use Compress::Zlib qw/Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/; +use Compress::Raw::Zlib qw/Z_OK Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/; use Config::Tiny (); -use Errno 'EWOULDBLOCK'; use IO::Select (); +use Net::SSLeay (); use List::Util 'first'; use POSIX ':signal_h'; -use Socket 'SO_KEEPALIVE'; +use Socket qw/SO_KEEPALIVE SOL_SOCKET/; use Exporter 'import'; BEGIN { + Net::SSLeay::load_error_strings(); + Net::SSLeay::SSLeay_add_ssl_algorithms(); + Net::SSLeay::randomize(); + our @EXPORT_OK = qw/read_config compact_set $IMAP_text $IMAP_cond/; } @@ -48,15 +52,20 @@ my %OPTIONS = ( username => qr/\A([\x01-\x7F]+)\z/, password => qr/\A([\x01-\x7F]+)\z/, auth => qr/\A($RE_ATOM_CHAR+(?: $RE_ATOM_CHAR+)*)\z/, - command => qr/\A(\/\P{Control}+)\z/, + command => qr/\A(\P{Control}+)\z/, 'null-stderr' => qr/\A(YES|NO)\z/i, compress => qr/\A($RE_ATOM_CHAR+(?: $RE_ATOM_CHAR+)*)\z/, - SSL_fingerprint => qr/\A([A-Za-z0-9]+\$\p{AHex}+)\z/, - SSL_cipher_list => qr/\A(\P{Control}+)\z/, - SSL_verify_trusted_peer => qr/\A(YES|NO)\z/i, - SSL_ca_path => qr/\A(\P{Control}+)\z/, + SSL_fingerprint => qr/\A((?:[A-Za-z0-9]+\$)?\p{AHex}+)\z/, + SSL_cipherlist => qr/\A(\P{Control}+)\z/, + SSL_verify => qr/\A(YES|NO)\z/i, + SSL_CApath => qr/\A(\P{Control}+)\z/, + SSL_CAfile => qr/\A(\P{Control}+)\z/, ); +# Use the same buffer size as Net::SSLeay::read(), to ensure there is +# never any pending data left in the current TLS record +my $BUFSIZE = 32768; +my $CRLF = "\x0D\x0A"; ############################################################################# # Utilities @@ -100,7 +109,7 @@ sub read_config($$%) { die "Invalid option $k\n" unless defined $opts{$k}; next unless defined $conf->{$k}; die "Invalid option $k = $conf->{$k}\n" unless $conf->{$k} =~ $opts{$k}; - $conf->{$k} = $1; + $conf->{$k} = $opts{$k} ne qr/\A(YES|NO)\z/i ? $1 : uc $1 eq 'YES' ? 1 : 0; } } return \%configs; @@ -179,7 +188,9 @@ sub quote($) { return "\"$str\""; } else { - return "{".length($str)."}\r\n".$str; + # we'll later replace the non-synchronizing literal with a + # synchronizing one if need be + return "{".length($str)."+}$CRLF".$str; } } @@ -221,15 +232,6 @@ sub new($%) { my $self = { @_ }; bless $self, $class; - foreach (keys %$self) { - next unless defined $self->{$_}; - if (uc $self->{$_} eq 'YES') { - $self->{$_} = 1; - } elsif (uc $self->{$_} eq 'NO') { - $self->{$_} = 0; - } - } - # the IMAP state: one of 'UNAUTH', 'AUTH', 'SELECTED' or 'LOGOUT' # (cf RFC 3501 section 3) $self->{_STATE} = ''; @@ -237,7 +239,8 @@ sub new($%) { # in/out buffer counts and output stream $self->{_INCOUNT} = $self->{_INRAWCOUNT} = 0; $self->{_OUTCOUNT} = $self->{_OUTRAWCOUNT} = 0; - $self->{_OUTBUF} = ''; + $self->{_OUTBUF} = $self->{_INBUF} = undef; + $self->{_LITPLUS} = ''; if ($self->{type} eq 'tunnel') { my $command = $self->{command} // $self->fail("Missing tunnel command"); @@ -286,12 +289,11 @@ sub new($%) { $args{PeerPort} = $self->{port} // $self->fail("Missing option port"); my $socket = IO::Socket::INET->new(%args) or $self->fail("Cannot bind: $@"); - $self->_start_ssl($socket) if $self->{type} eq 'imaps'; + $socket->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1) or $self->fail("Can't setsockopt SO_KEEPALIVE: $!"); - $socket->sockopt(SO_KEEPALIVE, 1); + $self->_start_ssl($socket) if $self->{type} eq 'imaps'; $self->{$_} = $socket for qw/STDOUT STDIN/; } - $self->{STDIN}->autoflush(0) // $self->panic("Can't turn off autoflush: $!"); binmode $self->{$_} foreach qw/STDIN STDOUT/; # command counter @@ -405,10 +407,10 @@ sub new($%) { $self->panic($IMAP_text) unless $r eq 'OK'; if ($algo eq 'DEFLATE') { - my %args = ( -WindowBits => 0 - MAX_WBITS ); - $self->{_Z_DEFLATE} = Compress::Zlib::deflateInit(%args) // + my %args = ( -WindowBits => 0 - MAX_WBITS, -Bufsize => $BUFSIZE ); + $self->{_Z_DEFLATE} = Compress::Raw::Zlib::Deflate::->new(%args, -AppendOutput => 1) // $self->panic("Can't create deflation stream"); - $self->{_Z_INFLATE} = Compress::Zlib::inflateInit(%args) // + $self->{_Z_INFLATE} = Compress::Raw::Zlib::Inflate::->new(%args) // $self->panic("Can't create inflation stream"); } else { @@ -454,6 +456,9 @@ sub DESTROY($) { my $self = shift; $self->{_STATE} = 'LOGOUT'; + Net::SSLeay::free($self->{_SSL}) if defined $self->{_SSL}; + Net::SSLeay::CTX_free($self->{_SSL_CTX}) if defined $self->{_SSL_CTX}; + foreach (qw/STDIN STDOUT/) { $self->{$_}->close() if defined $self->{$_} and $self->{$_}->opened(); } @@ -776,21 +781,8 @@ sub append($$@) { return unless @_; $self->fail("Server did not advertise UIDPLUS (RFC 4315) capability.") unless $self->_capable('UIDPLUS'); - - my @appends; - foreach my $mail (@_) { - my $append = ''; - $append .= '('.join(' ', grep {lc $_ ne '\recent'} @{$mail->{FLAGS}}).') ' - if defined $mail->{FLAGS}; - $append .= '"'.$mail->{INTERNALDATE}.'" ' if defined $mail->{INTERNALDATE}; - my ($body, $t) = defined $mail->{RFC822} ? ($mail->{RFC822}, '') - : defined $mail->{BINARY} ? ($mail->{BINARY}, '~') - : $self->panic("Missing message body in APPEND"); - $append .= "$t\{".length($body)."\}\r\n".$body; - push @appends, $append; - } $self->fail("Server did not advertise MULTIAPPEND (RFC 3502) capability.") - unless $#appends == 0 or $self->_capable('MULTIAPPEND'); + unless $#_ == 0 or $self->_capable('MULTIAPPEND'); # dump the cache before issuing the command if we're appending to the current mailbox my ($UIDNEXT, $EXISTS, $cache, %vanished); @@ -801,7 +793,21 @@ sub append($$@) { %vanished = map {$_ => 1} @{$self->{_VANISHED}}; } - $self->_send('APPEND '.quote($mailbox).' '.join(' ',@appends)); + my $tag = $self->_cmd_init('APPEND '.quote($mailbox)); + foreach my $mail (@_) { + my $str = ' '; + $str .= '('.join(' ', grep {lc $_ ne '\recent'} @{$mail->{FLAGS}}).') ' if defined $mail->{FLAGS}; + $str .= '"'.$mail->{INTERNALDATE}.'" ' if defined $mail->{INTERNALDATE}; + my ($body, $t) = defined $mail->{RFC822} ? ($mail->{RFC822}, 0) + : defined $mail->{BINARY} ? ($mail->{BINARY}, 1) + : $self->panic("Missing message body in APPEND"); + $self->_cmd_extend(\$str); + $self->_cmd_extend_lit($body, $t); + } + + $self->_cmd_flush(); + $self->_recv($tag); + $IMAP_text =~ /\A\Q$IMAP_cond\E \[APPENDUID ([0-9]+) ([0-9:,]+)\] / or $self->panic($IMAP_text); my ($uidvalidity, $uidset) = ($1, $2); $self->_update_cache_for($mailbox, UIDVALIDITY => $uidvalidity); @@ -819,9 +825,8 @@ sub append($$@) { $self->panic($_); } } - $self->fail("$uidset contains ".scalar(@uids)." elements while " - .scalar(@appends)." messages were appended.") - unless $#uids == $#appends; + $self->fail("$uidset contains ".scalar(@uids)." elements while ".($#_+1)." messages were appended.") + unless $#uids == $#_; # if $mailbox is the current mailbox we need to update the cache if (defined $self->{_SELECTED} and $mailbox eq $self->{_SELECTED}) { @@ -829,12 +834,16 @@ sub append($$@) { my %vanished2 = map {$_ => 1} @{$self->{_VANISHED}}; delete $vanished2{$_} foreach keys %vanished; my $VANISHED = scalar(keys %vanished2); # number of messages VANISHED meanwhile - $cache->{EXISTS} += $#appends+1 if defined $cache->{EXISTS} and $cache->{EXISTS} + $VANISHED == $EXISTS; + $cache->{EXISTS} += $#_+1 if defined $cache->{EXISTS} and $cache->{EXISTS} + $VANISHED == $EXISTS; $cache->{UIDNEXT} = $UIDNEXT if ($cache->{UIDNEXT} // 1) < $UIDNEXT; } - $self->log("Added ".($#appends+1)." message(s) to $mailbox, got new UID ".compact_set(@uids)) - unless $self->{quiet}; + unless ($self->{quiet}) { + my $msg = "Added ".($#_+1)." message(s)"; + $msg .= " to $mailbox" unless defined $self->{_SELECTED} and $mailbox eq $self->{_SELECTED}; + $msg .= ", got new UID ".compact_set(@uids); + $self->log($msg); + } return @uids; } @@ -880,18 +889,17 @@ sub notify($@) { sub slurp($) { my $self = shift; - my $stdout = $self->{STDOUT}; + my $ssl = $self->{_SSL}; my $read = 0; while (1) { - # Unprocessed data within the current SSL frame would cause + # Unprocessed data within the current TLS record would cause # select(2) to block/timeout due to the raw socket not being # ready. - unless (ref $stdout eq 'IO::Socket::SSL' and $stdout->pending() > 0) { + unless (defined $ssl and Net::SSLeay::pending($ssl) > 0) { my ($ok) = $self->{_SEL_OUT}->can_read(0); return $read unless defined $ok; } - $self->_resp( $self->_getline() ); $read++; } @@ -1189,52 +1197,81 @@ sub push_flag_updates($$@) { ############################################################################# # Private methods +# $self->_ssl_error($error, [...]) +# Log an SSL $error and exit with return value 1. +sub _ssl_error($$@) { + my $self = shift; + $self->fail(@_) unless defined $self->{_SSL}; + $self->log('SSL ERROR: ', @_); + if ($self->{debug}) { + while (my $err = Net::SSLeay::ERR_get_error()) { + $self->log(Net::SSLeay::ERR_error_string($err)); + } + } + exit 1; +} + # $self->_start_ssl($socket) -# Upgrade the $socket to IO::Socket::SSL. +# Upgrade the $socket to SSL/TLS. sub _start_ssl($$) { my ($self, $socket) = @_; - require 'IO/Socket/SSL.pm'; - require 'Net/SSLeay.pm'; - - my %sslargs = (SSL_create_ctx_callback => sub($) { - my $ctx = shift; - my $rv; + my $ctx = Net::SSLeay::CTX_new() or $self->panic("Failed to create SSL_CTX $!"); + + # https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_options.html + Net::SSLeay::CTX_set_options($ctx, + Net::SSLeay::OP_SINGLE_ECDH_USE() | + Net::SSLeay::OP_SINGLE_DH_USE() | + Net::SSLeay::OP_NO_SSLv2() | + Net::SSLeay::OP_NO_SSLv3() | + Net::SSLeay::OP_NO_COMPRESSION() ); + + # https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_mode.html + Net::SSLeay::CTX_set_mode($ctx, + Net::SSLeay::MODE_ENABLE_PARTIAL_WRITE() | + Net::SSLeay::MODE_ACCEPT_MOVING_WRITE_BUFFER() | + Net::SSLeay::MODE_AUTO_RETRY() | # don't fail SSL_read on renegociation + Net::SSLeay::MODE_RELEASE_BUFFERS() ); + + if (defined (my $ciphers = $self->{SSL_cipherlist})) { + Net::SSLeay::CTX_set_cipher_list($ctx, $ciphers) + or $self->_ssl_error("Can't set cipher list"); + } + + if ($self->{SSL_verify} // 1) { + # verify the certificate chain + my ($file, $path) = ($self->{SSL_CAfile} // '', $self->{SSL_CApath} // ''); + if ($file ne '' or $path ne '') { + Net::SSLeay::CTX_load_verify_locations($ctx, $file, $path) + or $self->_ssl_error("Can't load verify locations"); + } + Net::SSLeay::CTX_set_verify($ctx, Net::SSLeay::VERIFY_PEER()); + } + else { + Net::SSLeay::CTX_set_verify($ctx, Net::SSLeay::VERIFY_NONE()); + } - # https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_options.html - $rv = Net::SSLeay::CTX_get_options($ctx) - | Net::SSLeay::OP_SINGLE_ECDH_USE() - | Net::SSLeay::OP_SINGLE_DH_USE() - | Net::SSLeay::OP_NO_SSLv2() - | Net::SSLeay::OP_NO_SSLv3() - | Net::SSLeay::OP_NO_COMPRESSION(); - Net::SSLeay::CTX_set_options($ctx, $rv); + my $ssl = Net::SSLeay::new($ctx) or $self->fail("Can't create new SSL structure"); + Net::SSLeay::set_fd($ssl, $socket->fileno()) or $self->fail("SSL filehandle association failed"); + $self->_ssl_error("Can't initiate TLS/SSL handshake") unless Net::SSLeay::connect($ssl) == 1; - # https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_mode.html - $rv = Net::SSLeay::CTX_get_mode($ctx) - | Net::SSLeay::MODE_AUTO_RETRY() # don't fail SSL_read on renegociation - | Net::SSLeay::MODE_RELEASE_BUFFERS(); - Net::SSLeay::CTX_set_mode($ctx, $rv); - }); + if (defined (my $fpr = $self->{SSL_fingerprint})) { + # ensure we're talking to the right server + (my $algo, $fpr) = $fpr =~ /^([^\$]+)\$(.*)/ ? ($1, $2) : ('sha256', $fpr); + my $digest = pack 'H*', ($fpr =~ tr/://rd); - my $fpr = delete $self->{SSL_fingerprint}; - my $vrfy = delete $self->{SSL_verify_trusted_peer}; - $sslargs{SSL_verify_mode} = ($vrfy // 1) ? Net::SSLeay::VERIFY_PEER() : Net::SSLeay::VERIFY_NONE(); - $sslargs{$_} = $self->{$_} foreach grep /^SSL_/, keys %$self; + my $type = Net::SSLeay::EVP_get_digestbyname($algo) + or $self->_ssl_error("Can't find MD value for name '$algo'"); - IO::Socket::SSL->start_SSL($socket, %sslargs) - or $self->fail("Failed SSL handshake: $!\n$IO::Socket::SSL::SSL_ERROR"); + my $cert = Net::SSLeay::get_peer_certificate($ssl) + or $self->_ssl_error("Can't get peer certificate"); - # ensure we're talking to the right server - if (defined $fpr) { - my $algo = $fpr =~ /^([^\$]+)\$/ ? $1 : 'sha256'; - my $fpr2 = $socket->get_fingerprint($algo); - $fpr =~ s/.*\$//; - $fpr2 =~ s/.*\$//; - $self->fail("Fingerprint don't match! MiTM in action?") - unless uc $fpr eq uc $fpr2; + $self->fail("Fingerprint doesn't match! MiTM in action?") + if Net::SSLeay::X509_digest($cert, $type) ne $digest and + Net::SSLeay::X509_pubkey_digest($cert, $type) ne $digest; } + @$self{qw/_SSL _SSL_CTX/} = ($ssl, $ctx); } @@ -1248,24 +1285,31 @@ sub _getline($;$) { my $self = shift; my $len = shift // 0; - my $stdout = $self->{STDOUT}; + my ($stdout, $ssl) = @$self{qw/STDOUT _SSL/}; $self->fail("Lost connection") unless $stdout->opened(); my (@lit, @line); while(1) { - if ($self->{_OUTBUF} eq '') { + unless (defined $self->{_OUTBUF}) { + my ($buf, $n); # nothing cached: read some more - # (read at most 2^14 bytes, the maximum length of an SSL - # frame, to ensure to guaranty that there is no pending data) - my $n = $stdout->sysread(my $buf,16384,0); - $self->panic("Can't read: $!") unless defined $n; - $self->fail("0 bytes read (got EOF)") unless $n > 0; # EOF + if (defined $ssl) { + ($buf, $n) = Net::SSLeay::read($ssl, $BUFSIZE); + } else { + $n = $stdout->sysread($buf, $BUFSIZE, 0); + } + + $self->_ssl_error("Can't read: $!") unless defined $n; + $self->_ssl_error("0 bytes read (got EOF)") unless $n > 0; # EOF $self->{_OUTRAWCOUNT} += $n; if (defined (my $i = $self->{_Z_INFLATE})) { - $buf = $i->inflate($buf) // $self->panic("Inflation failed: ", $i->msg()); + $i->inflate($buf, $self->{_OUTBUF}) == Z_OK or + $self->panic("Inflation failed: ", $i->msg()); + } + else { + $self->{_OUTBUF} = $buf; } - $self->{_OUTBUF} = $buf; } if ($len == 0) { # read a regular line: stop after the first \r\n if ((my $idx = 1 + index($self->{_OUTBUF}, "\n")) > 0) { @@ -1275,27 +1319,26 @@ sub _getline($;$) { $self->{_OUTBUF} = substr($self->{_OUTBUF}, $idx); $self->{_OUTCOUNT} += length($lit) + length($line); - $line =~ s/\r\n\z// or $self->panic($line); + $line =~ s/$CRLF\z// or $self->panic($line); $self->logger('S: '.(@lit ? '[...]' : ''), $line) if $self->{debug}; - return (wantarray ? ($lit, $line) : $line); + return (wantarray ? (\$lit, $line) : $line); } else { push @line, $self->{_OUTBUF}; - $self->{_OUTBUF} = ''; + undef $self->{_OUTBUF}; } } elsif ($len > 0) { # $len bytes of literal bytes to read - if ($len <= length($self->{_OUTBUF})) { + if ($len < length($self->{_OUTBUF})) { push @lit, substr($self->{_OUTBUF}, 0, $len, ''); $len = 0; } else { push @lit, $self->{_OUTBUF}; $len -= length($self->{_OUTBUF}); - $self->{_OUTBUF} = ''; + undef $self->{_OUTBUF}; } - next; } } } @@ -1323,7 +1366,7 @@ sub _update_cache_for($$%) { if ($k eq 'UIDVALIDITY') { # try to detect UIDVALIDITY changes early (before starting the sync) $self->fail("UIDVALIDITY changed! ($cache->{UIDVALIDITY} != $v) ", - "Need to invalidate the UID cache.") + "Need to invalidate the UID cache.") if defined $cache->{UIDVALIDITY} and $cache->{UIDVALIDITY} != $v; $self->{_PCACHE}->{$mailbox}->{UIDVALIDITY} //= $v; } @@ -1332,87 +1375,150 @@ sub _update_cache_for($$%) { } -# $self->_write(@data) -# Send the given @data to the IMAP server. -# Update the interal raw byte count, but the regular byte count must -# have been updated earlier (eg, by _send_cmd). -sub _write($@) { +# $self->_cmd_init($command) +# Generate a new tag for the given $command, push both the +# concatenation to the command buffer. $command can be a scalar or a +# scalar reference. +# Use the _cmd_extend and/or _cmd_extend_lit methods to extend the +# command, and _cmd_flush to send it to the server. +sub _cmd_init($$) { my $self = shift; - foreach (@_) { - next if $_ eq ''; - $self->{STDIN}->write($_) // $self->panic("Can't write: $!"); - $self->{_INRAWCOUNT} += length($_); - } + my $tag = sprintf '%06d', $self->{_TAG}++; + my $command = (defined $self->{_INBUF} ? $CRLF : '').$tag.' '.(ref $_[0] ? ${$_[0]} : $_[0]); + $self->_cmd_extend(\$command); + return $tag; } -# $self->_z_flush([$type]) -# Flush the deflation stream, and write the compressed data. -# This method is a noop if no compression layer is active. -sub _z_flush($;$) { - my ($self,$t) = @_; - my $d = $self->{_Z_DEFLATE} // return; - $self->_write( $d->flush($t) // $self->panic("Can't flush deflation stream: ", $d->msg()) ); +# $self->_cmd_extend($args) +# Append $args to the command buffer. $args can be a scalar or a +# scalar reference. If $args contains some literal(s) and the server +# doesn't support LITERAL+, flush the command and wait for an answer +# before each literal +sub _cmd_extend($$) { + my $self = shift; + my $args = ref $_[0] ? $_[0] : \$_[0]; + + if ($self->{_LITPLUS} ne '') { + # server supports LITERAL+: use $args as is + $self->_cmd_extend_($args); + } + else { + # server supports LITERAL+: flush the command before each + # literal + my ($offset, $litlen) = (0, 0); + while ( (my $idx = index($$args, "\n", $offset+$litlen)) >= 0 ) { + my $line = substr($$args, $offset, $idx+1-$offset); + $line =~ s/\{([0-9]+)\+\}$CRLF\z/{$1}$CRLF/ or $self->panic(); + $litlen = $1; + $self->_cmd_flush(\$line); + + my $x = $self->_getline(); + $x =~ /\A\+ / or $self->panic($x); + $offset = $idx+1; + } + my $line = substr($$args, $offset); + $self->_cmd_extend_(\$line); + } } -# $self->_send_cmd($tag, $command) -# Send the given $command to the IMAP server. -# If $command contains literals and the server supportes LITERAL+, -# non-synchronizing literals are sent instead. -# If a compression layer is active, $command is compressed before -# being send. -sub _send_cmd($) { - my ($self, $tag, $command) = @_; - my $litplus = $self->_capable('LITERAL+') ? 1 : 0; +# $self->_cmd_extend_lit($lit, [$lit8]) +# Append the literal $lit to the command buffer. $lit must be a +# scalar reference. If $lit8 is true, a literal8 is sent instead [RFC +# 3516]. +sub _cmd_extend_lit($$;$) { + my ($self, $lit, $lit8) = @_; + my $len = length($$lit); my $d = $self->{_Z_DEFLATE}; - my ($offset, $litlen) = (0, 0); - my $z_flush = 0; # whether to flush the dictionary after processing the next literal + # create a full flush point for long binary literals + my $z_flush = ($len > 4096 and !($self->{'use-binary'} // 1 and !$lit8)) ? 1 : 0; + $lit8 = $lit8 ? '~' : ''; # literal8, RFC 3516 BINARY - while(1) { - my $lit = substr($command, $offset, $litlen) if $litlen > 0; - $offset += $litlen; + my $strlen = $lit8.'{'.$len.$self->{_LITPLUS}.'}'.$CRLF; - my ($line, $z_flush2); - my $idx = index($command, "\n", $offset); - if ($idx < 0) { - $line = substr($command, $offset); - } - else { - $line = substr($command, $offset, $idx-1-$offset); - $litlen = $litplus ? ($line =~ s/\{([0-9]+)\}\z/{$1+}/ ? $1 : $self->panic()) - : ($line =~ /\{([0-9]+)\}\z/ ? $1 : $self->panic()); - $z_flush2 = ($litlen > 4096 and # large literal - ($self->{'use-binary'} // 1 or $line =~ /~\{[0-9]+\}\z/) # literal8, RFC 3516 BINARY - ) ? 1 : 0; + if ($self->{_LITPLUS} ne '') { + $self->_cmd_extend_(\$strlen); + if ($z_flush and defined $d) { + $d->flush(\$self->{_INBUF}, Z_FULL_FLUSH) == Z_OK + or $self->panic("Can't flush deflation stream: ", $d->msg()); } - $self->logger('C: ', ($offset == 0 ? "$tag " : '[...]'), $line) if $self->{debug}; + } + else { + # server doesn't supports LITERAL+ + $self->_cmd_flush(\$strlen, ($z_flush ? Z_FULL_FLUSH : ())); + my $x = $self->_getline(); + $x =~ /\A\+ / or $self->panic($x); + } - my @data = (($offset == 0 ? "$tag " : $lit), $line, "\r\n"); - $self->{_INCOUNT} += length($_) foreach @data; - if (!defined $d) { - $self->_write(@data); - } - else { - for (my $i = 0; $i <= $#data; $i++) { - $self->_z_flush(Z_FULL_FLUSH) if $i == 0 and $z_flush; - $self->_write( $d->deflate($data[$i]) // $self->panic("Deflation failed: ", $d->msg()) ); - $self->_z_flush(Z_FULL_FLUSH) if $i == 0 and $z_flush; - } - } + $self->_cmd_extend_($lit); + if ($z_flush and defined $d) { + $d->flush(\$self->{_INBUF}, Z_FULL_FLUSH) == Z_OK + or $self->panic("Can't flush deflation stream: ", $d->msg()); + } +} - if (!$litplus or $idx < 0) { - $self->_z_flush(Z_SYNC_FLUSH) if defined $d; - $self->{STDIN}->flush() // $self->panic("Can't flush: $!"); - last if $idx < 0; - my $x = $self->_getline(); - $x =~ /\A\+ / or $self->panic($x); +# $self->_cmd_flush([$crlf], [$z_flush]) +# Append $crlf (default: $CRLF) to the command buffer, flush the +# deflation stream by creating a flush point of type $z_flush +# (default: Z_SYNC_FLUSH) if there is a compression layer, and finally +# send the command to the server. +sub _cmd_flush($;$$) { + my $self = shift; + $self->_cmd_extend_( $_[0] // \$CRLF ); + my $z_flush = $_[1] // Z_SYNC_FLUSH; # the flush point type to use + my ($stdin, $ssl) = @$self{qw/STDIN _SSL/}; + + if ($self->{debug}) { + # remove $CRLF and literals + my ($offset, $litlen) = (0, $self->{_INBUFDBGLEN} // 0); + while ( (my $idx = index($self->{_INBUFDBG}, "\n", $offset+$litlen)) >= 0) { + my $line = substr($self->{_INBUFDBG}, $offset+$litlen, $idx+1-$offset-$litlen); + $line =~ s/$CRLF\z// or $self->panic(); + $self->logger('C: ', ($litlen > 0) ? '[...]' : '', $line); + $litlen = $line =~ /\{([0-9]+)(\+)?\}\z/ ? $1 : 0; + $offset = $idx+1; } + $self->panic() if $offset+$litlen < length($self->{_INBUFDBG}); + undef $self->{_INBUFDBG}; + $self->{_INBUFDBGLEN} = $litlen; + } + + if (defined (my $d = $self->{_Z_DEFLATE})) { + $d->flush(\$self->{_INBUF}, $z_flush) == Z_OK + or $self->panic("Can't flush deflation stream: ", $d->msg()); + } + + my ($offset, $length) = (0, length($self->{_INBUF})); + while ($length > 0) { + my $written = defined $ssl ? + Net::SSLeay::write_partial($ssl, $offset, $length, $self->{_INBUF}) : + $stdin->syswrite($self->{_INBUF}, $length, $offset); + $self->_ssl_error("Can't write: $!") unless defined $written and $written > 0; - $z_flush = $z_flush2; - $offset = $idx+1; + $offset += $written; + $length -= $written; + $self->{_INRAWCOUNT} += $written; + } + undef $self->{_INBUF}; +} + + +# $self->_cmd_extend_($args) +# Append the scalar reference $args to the command buffer. Usually +# one should use the higher-level method _cmd_extend as it takes care +# of literals if the server doesn't support LITERAL+. +sub _cmd_extend_($$) { + my ($self, $args) = @_; + $self->{_INCOUNT} += length($$args); # count IMAP traffic + $self->{_INBUFDBG} .= $$args if $self->{debug}; + if (defined (my $d = $self->{_Z_DEFLATE})) { + $d->deflate($args, \$self->{_INBUF}) == Z_OK or $self->panic("Deflation failed: ", $d->msg()); + } + else { + $self->{_INBUF} .= $$args; } } @@ -1427,15 +1533,31 @@ sub _send_cmd($) { # In void context, croak unless the server answers with a tagged 'OK' # response. Otherwise, return the condition status ('OK'/'NO'/'BAD'). sub _send($$;&) { - my ($self, $command, $callback) = @_; - my $cmd = $command =~ /\AUID ($RE_ATOM_CHAR+) / ? $1 : $command =~ /\A($RE_ATOM_CHAR+) / ? $1 : $command; - my $set = $command =~ /\AUID (?:FETCH|STORE) ([0-9:,*]+)/ ? $1 : undef; + my $self = shift; + my $command = \$_[0]; + my $callback = $_[1]; - # send the command; for servers supporting non-synchronizing - # literals, mark literals as such and then the whole command in one - # go, otherwise send literals one at a time - my $tag = sprintf '%06d', $self->{_TAG}++; - $self->_send_cmd($tag, $command); + my $tag = $self->_cmd_init($command); + $self->_cmd_flush(); + + if (!defined $callback) { + $self->_recv($tag); + } + else { + my $cmd = $$command =~ /\AUID ($RE_ATOM_CHAR+) / ? $1 : $$command =~ /\A($RE_ATOM_CHAR+) / ? $1 : $$command; + my $set = $$command =~ /\AUID (?:FETCH|STORE) ([0-9:,*]+)/ ? $1 : undef; + $self->_recv($tag, $callback, $cmd, $set); + } +} + + +# $self->_recv($tag, [$callback, $command, $set]) +# Wait for a tagged response with the given $tag. The $callback, if +# provided, is used to process each untagged response. $command and +# $set can further limit the set of responses to apply the callback +# to. +sub _recv($$;$&$) { + my ($self, $tag, $callback, $cmd, $set) = @_; my $r; # wait for the answer @@ -1588,6 +1710,7 @@ sub _resp_text($$) { } elsif (/\A\[CAPABILITY((?: $RE_ATOM_CHAR+)+)\] $RE_TEXT_CHAR+\z/) { $self->{_CAPABILITIES} = [ split / /, ($1 =~ s/^ //r) ]; + $self->{_LITPLUS} = (grep { uc $_ eq 'LITERAL+' } @{$self->{_CAPABILITIES}}) ? '+' : ''; } elsif (/\A\[PERMANENTFLAGS \(((?:(?:\\?$RE_ATOM_CHAR+|\\\*)(?: (?:\\?$RE_ATOM_CHAR+|\\\*))*))\)\] $RE_TEXT_CHAR+\z/) { $self->_update_cache( PERMANENTFLAGS => [ split / /, $1 ] ); @@ -1648,7 +1771,7 @@ sub _string($$) { elsif ($$stream =~ s/\A\{([0-9]+)\}\z//) { # literal (my $lit, $$stream) = $self->_getline($1); - return $lit; + return $$lit; } else { $self->panic($$stream); @@ -1802,14 +1925,14 @@ sub _resp($$;$$$) { $mail{INTERNALDATE} = $1; } elsif (s/\A(?:RFC822|BODY\[\]) //) { - $mail{RFC822} = $self->_nstring(\$_); + $mail{RFC822} = \$self->_nstring(\$_); } elsif (s/\ABINARY\[\] //) { if (s/\A~\{([0-9]+)\}\z//) { # literal8, RFC 3516 BINARY (my $lit, $_) = $self->_getline($1); $mail{BINARY} = $lit; } else { - $mail{RFC822} = $self->_nstring(\$_); + $mail{RFC822} = \$self->_nstring(\$_); } } elsif (s/\AFLAGS \((\\?$RE_ATOM_CHAR+(?: \\?$RE_ATOM_CHAR+)*)?\)//) { @@ -1856,11 +1979,8 @@ sub _resp($$;$$$) { elsif (s/\A\+ //) { if (defined $callback and $cmd eq 'AUTHENTICATE') { my $x = $callback->($_); - $self->logger("C: ", $x) if $self->{debug}; - $x .= "\r\n"; - $self->{_INCOUNT} += length($x); - $self->_write($x); - $self->{STDIN}->flush() // $self->panic("Can't flush: $!"); + $self->_cmd_extend(\$x); + $self->_cmd_flush(); } } else { |