From 64dc8a1ed4e15ce456a699184a4fff263f2c902f Mon Sep 17 00:00:00 2001
From: Guilhem Moulin <guilhem@fripost.org>
Date: Wed, 9 Sep 2015 00:44:05 +0200
Subject: Add support for the IMAP COMPRESS extension [RFC4978].

Also, add traffic statistics after closing the connection to the IMAP
server.
---
 Changelog                 |   9 ++
 INSTALL                   |   2 +
 README                    |   7 ++
 interimap                 |  12 +--
 interimap.1               |   7 ++
 interimap.sample          |   1 +
 lib/Net/IMAP/InterIMAP.pm | 224 +++++++++++++++++++++++++++++++++++++++-------
 7 files changed, 226 insertions(+), 36 deletions(-)

diff --git a/Changelog b/Changelog
index acd02d2..0d56bac 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,12 @@
+interimap (0.2) upstream
+
+  * Add support for the IMAP COMPRESS extension [RFC4978].  By default
+    enabled for the remote server, and disabled for the local server.
+  * Add traffic statistics after closing the connection to the IMAP
+    server.
+
+ -- Guilhem Moulin <guilhem@guilhem.org>  Wed, 09 Sep 2015 00:44:35 +0200
+
 interimap (0.1) upstream;
 
   * Initial public release.  Development was started in July 2015.
diff --git a/INSTALL b/INSTALL
index 7bc3eef..b3f9ebc 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,8 +1,10 @@
 InterIMAP depends on the following Perl modules:
 
+  - Compress::Raw::Zlib (core module)
   - Config::Tiny
   - DBI
   - DBD::SQLite
+  - Errno (core module)
   - Getopt::Long (core module)
   - MIME::Base64 (core module) if authentication is required
   - IO::Select (core module)
diff --git a/README b/README
index 44190f3..0e93fe9 100644
--- a/README
+++ b/README
@@ -71,6 +71,13 @@ type=imaps.
     remote: ~user/.ssh/authorized_keys:
       command="/usr/lib/dovecot/imap",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-... id-interimap
 
+However for long-lived connections (using the --watch command-line
+option), the TLS overhead becomes negligible hence the advantage offered
+by the OpenSSH ControlPersist feature is not obvious.  Furthermore if
+the remote server supports the IMAP COMPRESS extension [RFC4978], adding
+compress=DEFLATE to the configuration can also greatly reduce bandwidth
+usage with regular INET sockets (type=imaps or type=imap).
+
 
 #######################################################################
 
diff --git a/interimap b/interimap
index af8b7fd..af2c510 100755
--- a/interimap
+++ b/interimap
@@ -109,7 +109,9 @@ my ($DBFILE, $LOCKFILE, $LOGGER_FD);
 my $DBH;
 
 # Clean after us
+my ($IMAP, $lIMAP, $rIMAP);
 sub cleanup() {
+    undef $_ foreach grep defined, ($IMAP, $lIMAP, $rIMAP);
     logger(undef, "Cleaning up...") if $CONFIG{debug};
     unlink $LOCKFILE if defined $LOCKFILE and -f $LOCKFILE;
     close $LOGGER_FD if defined $LOGGER_FD;
@@ -239,13 +241,13 @@ $LIST .= $#ARGV == 0 ? Net::IMAP::InterIMAP::quote($ARGV[0])
        : ('('.join(' ',map {Net::IMAP::InterIMAP::quote($_)} @ARGV).')') if @ARGV;
 
 
-my $IMAP;
 foreach my $name (qw/local remote/) {
     my %config = %{$CONF->{$name}};
     $config{$_} = $CONFIG{$_} foreach grep {defined $CONFIG{$_}} qw/quiet debug/;
     $config{enable} = 'QRESYNC';
     $config{name} = $name;
     $config{'logger-fd'} = $LOGGER_FD if defined $LOGGER_FD;
+    $config{'compress'} //= ($name eq 'local' ? 'NO' : 'YES');
 
     $IMAP->{$name} = { client => Net::IMAP::InterIMAP::->new(%config) };
     my $client = $IMAP->{$name}->{client};
@@ -268,6 +270,7 @@ foreach my $name (qw/local remote/) {
 
 @{$IMAP->{$_}}{qw/mailboxes delims/} = $IMAP->{$_}->{client}->list($LIST, @LIST_PARAMS) for qw/local remote/;
 
+
 ##############################################################################
 #
 
@@ -507,7 +510,7 @@ sub sync_mailbox_list() {
 }
 
 sync_mailbox_list();
-my ($lIMAP, $rIMAP) = map {$IMAP->{$_}->{client}} qw/local remote/;
+($lIMAP, $rIMAP) = map {$IMAP->{$_}->{client}} qw/local remote/;
 
 
 #############################################################################
@@ -1215,7 +1218,4 @@ while(1) {
     sync_mailbox_list();
 }
 
-END {
-    $_->logout() foreach grep defined, ($lIMAP, $rIMAP);
-    cleanup();
-}
+END { cleanup(); }
diff --git a/interimap.1 b/interimap.1
index 44235fc..bb97cf4 100644
--- a/interimap.1
+++ b/interimap.1
@@ -311,6 +311,13 @@ rely on Certificate Authorities.
 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.
+(Default: \(lqNO\(rq for the \(lq[local]\(rq section, \(lqYES\(rq for
+the \(lq[remote]\(rq section.)
+
 .SH KNOWN BUGS AND LIMITATIONS
 
 .IP \[bu]
diff --git a/interimap.sample b/interimap.sample
index 296f766..e469c98 100644
--- a/interimap.sample
+++ b/interimap.sample
@@ -13,6 +13,7 @@ host = imap.guilhem.org
 # port = 993
 username = guilhem
 password = xxxxxxxxxxxxxxxx
+#compress = YES
 
 # SSL options
 #SSL_cipher_list = EECDH+AES:EDH+AES:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1
diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm
index 97756f4..966b965 100644
--- a/lib/Net/IMAP/InterIMAP.pm
+++ b/lib/Net/IMAP/InterIMAP.pm
@@ -20,11 +20,13 @@ package Net::IMAP::InterIMAP v0.0.1;
 use warnings;
 use strict;
 
+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 List::Util 'first';
-use Socket 'SO_KEEPALIVE';
 use POSIX ':signal_h';
+use Socket 'SO_KEEPALIVE';
 
 use Exporter 'import';
 BEGIN {
@@ -47,6 +49,7 @@ my %OPTIONS = (
     password => qr/\A([\x01-\x7F]+)\z/,
     auth => qr/\A($RE_ATOM_CHAR+(?: $RE_ATOM_CHAR+)*)\z/,
     command => qr/\A(\/\P{Control}+)\z/,
+    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,
@@ -225,6 +228,11 @@ sub new($%) {
     # (cf RFC 3501 section 3)
     $self->{_STATE} = '';
 
+    # in/out buffer counts and output stream
+    $self->{_INCOUNT}  = $self->{_INRAWCOUNT}  = 0;
+    $self->{_OUTCOUNT} = $self->{_OUTRAWCOUNT} = 0;
+    $self->{_OUTBUF} = '';
+
     if ($self->{type} eq 'tunnel') {
         my $command = $self->{command} // $self->fail("Missing tunnel command");
 
@@ -232,7 +240,6 @@ sub new($%) {
         pipe my $rd, $self->{STDIN}  or $self->panic("Can't pipe: $!");
 
         my $pid = fork // $self->panic("Can't fork: $!");
-
         unless ($pid) {
             # children
             foreach (\*STDIN, \*STDOUT, $self->{STDIN}, $self->{STDOUT}) {
@@ -243,7 +250,6 @@ sub new($%) {
 
             my $sigset = POSIX::SigSet::->new(SIGINT);
             my $oldsigset = POSIX::SigSet::->new();
-
             sigprocmask(SIG_BLOCK, $sigset, $oldsigset) // $self->panic("Can't block SIGINT: $!");
 
             exec $command or $self->panic("Can't exec: $!");
@@ -282,6 +288,7 @@ sub new($%) {
         $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
     $self->{_TAG} = 0;
@@ -391,8 +398,41 @@ sub new($%) {
             $self->capabilities();
         }
     }
-
     $self->{_STATE} = 'AUTH';
+
+    # Don't send the COMPRESS command before STARTTLS or AUTH, as per RFC 4978
+    if (uc ($self->{compress} // 'NO') eq 'YES') {
+        my @supported = qw/DEFLATE/; # supported compression algorithms
+        my @algos = grep defined, map { /^COMPRESS=(.+)/ ? uc $1 : undef } @{$self->{_CAPABILITIES}};
+        my $algo = first { my $x = $_; grep {$_ eq $x} @algos } @supported;
+        if (!defined $algo) {
+            $self->warn("Couldn't find a suitable compression algorithm. Not enabling compression.");
+        }
+        else {
+            my ($d, $i);
+            my $r = $self->_send("COMPRESS $algo");
+            unless ($r eq 'NO' and $IMAP_text =~ /\ANO \[COMPRESSIONACTIVE\] /) {
+                $self->panic($IMAP_text) unless $r eq 'OK';
+
+                if ($algo eq 'DEFLATE') {
+                    my ($status, $d, $i);
+                    my %args = ( -WindowBits => 0 - MAX_WBITS );
+                    ($d, $status) = Compress::Raw::Zlib::Deflate::->new(%args);
+                    $self->panic("Can't create deflation stream: ", $d->msg())
+                        unless defined $d and $status == Z_OK;
+
+                    ($i, $status) = Compress::Raw::Zlib::Inflate::->new(%args);
+                    $self->panic("Can't create inflation stream: ", $i->msg())
+                        unless defined $i and $status == Z_OK;
+                    @$self{qw/_Z_DEFLATE _Z_INFLATE/} = ($d, $i);
+                }
+                else {
+                    $self->fail("Unsupported compression algorithm: $algo");
+                }
+            }
+        }
+    }
+
     my @extensions = !defined $self->{enable} ? ()
                    : ref $self->{enable} eq 'ARRAY' ? @{$self->{enable}}
                    : ($self->{enable});
@@ -411,9 +451,22 @@ sub new($%) {
 # Log out when the Net::IMAP::InterIMAP object is destroyed.
 sub DESTROY($) {
     my $self = shift;
+    $self->{_STATE} = 'LOGOUT';
+
     foreach (qw/STDIN STDOUT/) {
         $self->{$_}->close() if defined $self->{$_} and $self->{$_}->opened();
     }
+
+    unless ($self->{quiet}) {
+        my $msg = "Connection closed";
+        $msg .= " in=$self->{_INCOUNT}";
+        $msg .= " (raw=$self->{_INRAWCOUNT}, ratio ".sprintf('%.2f', $self->{_INRAWCOUNT}/$self->{_INCOUNT}).")"
+          if defined $self->{_INRAWCOUNT} and $self->{_INCOUNT} > 0 and $self->{_INCOUNT} != $self->{_INRAWCOUNT};
+        $msg .= ", out=$self->{_OUTCOUNT}";
+        $msg .= " (raw=$self->{_OUTRAWCOUNT}, ratio ".sprintf('%.2f', $self->{_OUTRAWCOUNT}/$self->{_OUTCOUNT}).")"
+          if defined $self->{_OUTRAWCOUNT} and $self->{_OUTCOUNT} > 0 and $self->{_OUTCOUNT} != $self->{_OUTRAWCOUNT};
+        $self->log($msg);
+    }
 }
 
 
@@ -1153,21 +1206,74 @@ sub _fingerprint_match($$$) {
 }
 
 
-# $self->_getline([$msg])
-#   Read a line from the handle and strip the trailing CRLF.
+# $self->_getline([$length])
+#   Read a line from the handle and strip the trailing CRLF, optionally
+#   after reading a literal of the given $length (default: 0).
+#   In list context, return a pair ($literal, $line); otherwise only
+#   return the $line.
 #   /!\ Don't use this method with non-blocking IO!
 sub _getline($;$) {
     my $self = shift;
-    my $msg = shift // '';
+    my $len = shift // 0;
 
-    if ($self->{STDOUT}->opened()) {
-        my $x = $self->{STDOUT}->getline() // $self->panic("Can't read: $!");
-        $x =~ s/\r\n\z// or $self->panic($x);
-        $self->logger("S: $msg", $x) if $self->{debug};
-        return $x;
-    }
-    else {
-        undef $self;
+    my $stdout = $self->{STDOUT};
+    $self->fail("Lost connection") unless $stdout->opened();
+
+    my (@lit, @line);
+    while(1) {
+        if ($self->{_OUTBUF} eq '') {
+            # 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);
+            unless (defined $n) {
+                next unless $! == EWOULDBLOCK and
+                    (ref $stdout ne 'IO::Socket::SSL' or
+                        # sysread might fail if must finish a SSL handshake first
+                        ($IO::Socket::SSL::SSL_ERROR == Net::SSLeay::ERROR_WANT_READ() or
+                         $IO::Socket::SSL::SSL_ERROR == Net::SSLeay::ERROR_WANT_WRITE()));
+                $self->panic("Can't read: $!")
+            }
+            $self->fail("0 bytes read (got EOF)") unless $n > 0; # EOF
+            $self->{_OUTRAWCOUNT} += $n;
+
+            if (defined (my $i = $self->{_Z_INFLATE})) {
+                my $status = $i->inflate($buf, my $data);
+                $self->panic("Inflation failed: ", $i->msg()) unless $status == Z_OK;
+                $buf = $data;
+            }
+            $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) {
+                # found the EOL, we're done
+                my $lit = join '', @lit;
+                my $line = join '', @line, substr($self->{_OUTBUF}, 0, $idx);
+                $self->{_OUTBUF} = substr($self->{_OUTBUF}, $idx);
+
+                $self->{_OUTCOUNT} += length($lit) + length($line);
+                $line =~ s/\r\n\z// or $self->panic($line);
+                $self->logger('S: '.(@lit ? '[...]' : ''), $line) if $self->{debug};
+
+                return (wantarray ? ($lit, $line) : $line);
+            }
+            else {
+                push @line, $self->{_OUTBUF};
+                $self->{_OUTBUF} = '';
+            }
+        }
+        elsif ($len > 0) { # $len bytes of literal bytes to read
+            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} = '';
+            }
+            next;
+        }
     }
 }
 
@@ -1203,6 +1309,56 @@ sub _update_cache_for($$%) {
 }
 
 
+# $self->_write(@data)
+#   Send the given @data to the IMAP server and flush the buffer.  If a
+#   compression layer is active, flush the deflation stream first.
+#   Update the interal raw byte count, but the regular byte count must
+#   have been updated earlier.
+sub _write($@) {
+    my $self = shift;
+    my @data = @_;
+
+    if (defined (my $d = $self->{_Z_DEFLATE})) {
+        my $status = $d->flush(my $buf, Z_SYNC_FLUSH);
+        $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
+        push @data, $buf if $buf ne '';
+    }
+
+    my $data = join '', @data;
+    $self->{STDIN}->write($data) // $self->panic("Can't write: $!");
+    $self->{STDIN}->flush() // $self->panic("Can't flush: $!");
+    $self->{_INRAWCOUNT} += length($data);
+}
+
+
+# $self->_z_deflate(@data)
+#   Add the given @data to the deflation stream, and return the
+#   compressed data.
+#   This method is a noop if no compression layer is active.
+sub _z_deflate($@) {
+    my $self = shift;
+    my $data = join '', @_;
+    $self->{_INCOUNT} += length($data);
+    my $d = $self->{_Z_DEFLATE} // return @_;
+
+    my $status = $d->deflate($data, my $buf);
+    $self->panic("Deflation failed: ", $d->msg()) unless $status == Z_OK;
+    return ($buf) if $buf ne '';
+}
+
+
+# $self->_z_flush([$type])
+#   Flush the deflation stream, and return the compressed data.
+#   This method is a noop if no compression layer is active.
+sub _z_flush($;$) {
+    my $self = shift;
+    my $d = $self->{_Z_DEFLATE} // return;
+    my $status = $d->flush(my $buf, @_);
+    $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
+    return ($buf) if $buf ne '';
+}
+
+
 # $self->_send($command, [$callback])
 #   Send the given $command to the server, then wait for the response.
 #   (The status condition and response text are respectively placed in
@@ -1222,31 +1378,40 @@ sub _send($$;&) {
     # go, otherwise send literals one at a time
     my $tag = sprintf '%06d', $self->{_TAG}++;
     my $litplus;
-    my @command = ("$tag ");
-    my $dbg_cmd = "C: $command[0]";
+
+    my @command = $self->_z_deflate("$tag ");
+    my $dbg_cmd = "C: $tag ";
+
     while ($command =~ s/\A(.*?)\{([0-9]+)\}\r\n//) {
         my ($str, $len) = ($1, $2);
         my $lit = substr $command, 0, $len, ''; # consume the literal
 
         $litplus //= $self->_capable('LITERAL+') ? '+' : '';
-        push @command,       $str, "{$len$litplus}", "\r\n";
+        push @command, $self->_z_deflate($str, "{$len$litplus}", "\r\n");
+
         $self->logger($dbg_cmd, $str, "{$len$litplus}") if $self->{debug};
         $dbg_cmd = 'C: [...]';
 
         unless ($litplus) {
-            $self->{STDIN}->write(join('',@command)) // $self->panic("Can't write: $!");
-            $self->{STDIN}->flush();
+            $self->_write(@command);
             my $x = $self->_getline();
             $x =~ /\A\+ / or $self->panic($x);
             @command = ();
         }
-        push @command, $lit;
+        if ($len <= 4096) {
+            push @command, $self->_z_deflate($lit);
+        } else {
+            # send a Z_FULL_FLUSH at the start and end of large literals,
+            # as hinted at in RFC 4978 section 4
+            # TODO only do that for non-text literals
+            push @command, $self->_z_flush(Z_FULL_FLUSH);
+            push @command, $self->_z_deflate($lit);
+            push @command, $self->_z_flush(Z_FULL_FLUSH);
+        }
     }
-    push @command, $command, "\r\n";
+    push @command, $self->_z_deflate($command, "\r\n");
     $self->logger($dbg_cmd, $command) if $self->{debug};
-    $self->{STDIN}->write(join('',@command)) // $self->panic("Can't write: $!");
-    $self->{STDIN}->flush();
-
+    $self->_write(@command);
 
     my $r;
     # wait for the answer
@@ -1443,9 +1608,7 @@ sub _string($$) {
     }
     elsif ($$stream =~ s/\A\{([0-9]+)\}\z//) {
         # literal
-        $self->{STDOUT}->read(my $lit, $1) // $self->panic("Can't read: $!");
-        # read a the rest of the response
-        $$stream = $self->_getline('[...]');
+        (my $lit, $$stream) = $self->_getline($1);
         return $lit;
     }
     else {
@@ -1647,8 +1810,9 @@ sub _resp($$;$$$) {
         if (defined $callback and $cmd eq 'AUTHENTICATE') {
             my $x = $callback->($_);
             $self->logger("C: ", $x) if $self->{debug};
-            $self->{STDIN}->write($x."\r\n") // $self->panic("Can't write: $!");
-            $self->{STDIN}->flush();
+            $x .= "\r\n";
+            $self->{_INCOUNT} += length($x);
+            $self->_write($x);
         }
     }
     else {
-- 
cgit v1.2.3


From 9fb0576765624cc27a1c06aebc9f4ef5df31ba30 Mon Sep 17 00:00:00 2001
From: Guilhem Moulin <guilhem@fripost.org>
Date: Wed, 9 Sep 2015 01:18:14 +0200
Subject: Add a configuration option 'null-stderr=YES'.

To send STDERR to /dev/null for type=tunnel.
---
 Changelog                 |  2 ++
 interimap.1               |  6 ++++++
 interimap.sample          |  1 +
 lib/Net/IMAP/InterIMAP.pm | 16 +++++++++++++++-
 4 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/Changelog b/Changelog
index 0d56bac..23d2a17 100644
--- a/Changelog
+++ b/Changelog
@@ -4,6 +4,8 @@ interimap (0.2) upstream
     enabled for the remote server, and disabled for the local server.
   * Add traffic statistics after closing the connection to the IMAP
     server.
+  * Add a configuration option 'null-stderr=YES' to send STDERR to
+    /dev/null for type=tunnel.
 
  -- Guilhem Moulin <guilhem@guilhem.org>  Wed, 09 Sep 2015 00:44:35 +0200
 
diff --git a/interimap.1 b/interimap.1
index bb97cf4..dc3b49b 100644
--- a/interimap.1
+++ b/interimap.1
@@ -318,6 +318,12 @@ advertizing it.
 (Default: \(lqNO\(rq for the \(lq[local]\(rq section, \(lqYES\(rq for
 the \(lq[remote]\(rq section.)
 
+.TP
+.I null-stderr
+Whether to redirect \fIcommand\fR's standard error to \(lq/dev/null\(rq
+for type \fItype\fR=tunnel.
+(Default: \(lqNO\(rq.)
+
 .SH KNOWN BUGS AND LIMITATIONS
 
 .IP \[bu]
diff --git a/interimap.sample b/interimap.sample
index e469c98..86d41dd 100644
--- a/interimap.sample
+++ b/interimap.sample
@@ -6,6 +6,7 @@ ignore-mailbox = ^virtual/
 [local]
 type = tunnel
 command = /usr/lib/dovecot/imap
+null-stderr = YES
 
 [remote]
 # type = imaps
diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm
index 966b965..db6f484 100644
--- a/lib/Net/IMAP/InterIMAP.pm
+++ b/lib/Net/IMAP/InterIMAP.pm
@@ -49,6 +49,7 @@ my %OPTIONS = (
     password => qr/\A([\x01-\x7F]+)\z/,
     auth => qr/\A($RE_ATOM_CHAR+(?: $RE_ATOM_CHAR+)*)\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/,
@@ -248,11 +249,24 @@ sub new($%) {
             open STDIN,  '<&', $rd or $self->panic("Can't dup: $!");
             open STDOUT, '>&', $wd or $self->panic("Can't dup: $!");
 
+            my $stderr2;
+            if (uc ($self->{'null-stderr'} // 'NO') eq 'YES') {
+                open $stderr2, '>&', *STDERR;
+                open STDERR, '>', '/dev/null' or $self->panic("Can't open /dev/null: $!");
+            }
+
             my $sigset = POSIX::SigSet::->new(SIGINT);
             my $oldsigset = POSIX::SigSet::->new();
             sigprocmask(SIG_BLOCK, $sigset, $oldsigset) // $self->panic("Can't block SIGINT: $!");
 
-            exec $command or $self->panic("Can't exec: $!");
+            unless (exec $command) {
+                my $err = $!;
+                if (defined $stderr2) {
+                    close STDERR;
+                    open STDERR, '>&', $stderr2;
+                }
+                $self->panic("Can't exec: $err");
+            }
         }
 
         # parent
-- 
cgit v1.2.3


From 1abb196660516f85b2ec13673aa28e6cf8a24b41 Mon Sep 17 00:00:00 2001
From: Guilhem Moulin <guilhem@fripost.org>
Date: Wed, 9 Sep 2015 16:05:36 +0200
Subject: Add support for the Binary Content extension [RFC3516].

Unfortunately as of Debian Wheezy it doesn't work for Dovecot with
COMPRESS=DEFLATE [RFC4978] and non-synchronizing literals.

    perl -e 'use Compress::Raw::Zlib;
             print "a COMPRESS DEFLATE\r\n";
             sleep 1;
             my $d = new Compress::Raw::Zlib::Deflate( -WindowBits => -15 );
             $d->deflate("b APPEND TRASH ~{1+}\r\nx\r\n", my $buf);
             print $buf;
             $d->flush($buf, Z_SYNC_FLUSH);
             print $buf;
             sleep 1;
    ' | /usr/lib/dovecot/imap
    imap(guilhem): Panic: stream doesn't support seeking backwards

Interestingly, it works just fine for non-binary literals:

    perl -e 'use Compress::Raw::Zlib;
             print "a COMPRESS DEFLATE\r\n";
             sleep 1;
             my $d = new Compress::Raw::Zlib::Deflate( -WindowBits => -15 );
             $d->deflate("b APPEND TRASH {1+}\r\nx\r\n", my $buf);
             print $buf;
             $d->flush($buf, Z_SYNC_FLUSH);
             print $buf;
             sleep 1;
    ' | /usr/lib/dovecot/imap

However I can't reproduce the problem Dovecot 2.2.18 and Debian Sid (but
it doesn't help to install Dovecot from testing to my Wheezy box.)
---
 Changelog                 |  4 +++
 interimap                 | 16 ++++++----
 interimap.1               | 12 ++++++++
 interimap.sample          |  1 +
 lib/Net/IMAP/InterIMAP.pm | 76 ++++++++++++++++++++++++++++++-----------------
 5 files changed, 77 insertions(+), 32 deletions(-)

diff --git a/Changelog b/Changelog
index 23d2a17..5b010d2 100644
--- a/Changelog
+++ b/Changelog
@@ -6,6 +6,10 @@ interimap (0.2) upstream
     server.
   * Add a configuration option 'null-stderr=YES' to send STDERR to
     /dev/null for type=tunnel.
+  * Add support for the Binary Content extension [RFC3516].  Enabled by
+    default if both the local and remote servers advertize "BINARY".
+	Can be disabled by adding 'use-binary=NO' to the default section in
+	the configuration file.
 
  -- Guilhem Moulin <guilhem@guilhem.org>  Wed, 09 Sep 2015 00:44:35 +0200
 
diff --git a/interimap b/interimap
index af2c510..81582f2 100755
--- a/interimap
+++ b/interimap
@@ -74,6 +74,7 @@ my $CONF = read_config( delete $CONFIG{config} // $NAME
                       , 'list-mailbox' => qr/\A([\x01-\x09\x0B\x0C\x0E-\x7F]+)\z/
                       , 'list-select-opts' => qr/\A([\x21\x23\x24\x26\x27\x2B-\x5B\x5E-\x7A\x7C-\x7E]+)\z/
                       , 'ignore-mailbox' => qr/\A([\x01-\x09\x0B\x0C\x0E-\x7F]+)\z/
+                      , 'use-binary' => qr/\A(YES|NO)\z/i,
                       );
 my ($DBFILE, $LOCKFILE, $LOGGER_FD);
 
@@ -511,6 +512,10 @@ 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
+             !$lIMAP->incapable('BINARY') and !$rIMAP->incapable('BINARY'))
+                ? 'BINARY' : 'BODY').'.PEEK[]';
 
 
 #############################################################################
@@ -592,10 +597,10 @@ sub download_missing($$$@) {
     my ($buff, $bufflen) = ([], 0);
     undef $buff if ($target eq 'local' ? $lIMAP : $rIMAP)->incapable('MULTIAPPEND');
 
-    my $attrs = join ' ', qw/MODSEQ FLAGS INTERNALDATE ENVELOPE BODY.PEEK[]/;
+    my $attrs = $ATTRS.' ENVELOPE';
     ($source eq 'local' ? $lIMAP : $rIMAP)->fetch(compact_set(@set), "($attrs)", sub($) {
         my $mail = shift;
-        return unless exists $mail->{RFC822}; # not for us
+        return unless exists $mail->{RFC822} or exists $mail->{BINARY}; # not for us
 
         my $uid = $mail->{UID};
         my $from = first { defined $_ and @$_ } @{$mail->{ENVELOPE}}[2,3,4];
@@ -964,9 +969,10 @@ sub sync_known_messages($$) {
 # after the FETCH.
 sub callback_new_message($$$$;$$$) {
     my ($idx, $mailbox, $name, $mail, $UIDs, $buff, $bufflen) = @_;
-    return unless exists $mail->{RFC822}; # not for us
 
-    my $length = length $mail->{RFC822};
+    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})");
         return;
@@ -1030,7 +1036,7 @@ sub sync_messages($$;$$) {
             my $bufflen = 0;
             my @tUIDs;
 
-            ($source eq 'remote' ? $rIMAP : $lIMAP)->pull_new_messages(sub($) {
+            ($source eq 'remote' ? $rIMAP : $lIMAP)->pull_new_messages($ATTRS, sub($) {
                 callback_new_message($idx, $mailbox, $source, shift, \@tUIDs, $buff, \$bufflen)
             }, @{$ignore{$source}});
 
diff --git a/interimap.1 b/interimap.1
index dc3b49b..e552351 100644
--- a/interimap.1
+++ b/interimap.1
@@ -324,6 +324,18 @@ Whether to redirect \fIcommand\fR's standard error to \(lq/dev/null\(rq
 for type \fItype\fR=tunnel.
 (Default: \(lqNO\(rq.)
 
+.TP
+.I use-binary
+Whether to use the Binary Content extension [RFC3516] in FETCH and
+APPEND commands.
+This is useful for binary attachments for instance, as it avoids the
+overhead caused by base64 encodings.  Moreover if the IMAP COMPRESS
+extension is enabled, full flush points are placed around large non-text
+literals to empty the compression dictionary.
+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.)
+
 .SH KNOWN BUGS AND LIMITATIONS
 
 .IP \[bu]
diff --git a/interimap.sample b/interimap.sample
index 86d41dd..b0f619a 100644
--- a/interimap.sample
+++ b/interimap.sample
@@ -2,6 +2,7 @@
 #list-mailbox = "*"
 list-select-opts = SUBSCRIBED
 ignore-mailbox = ^virtual/
+#use-binary = YES
 
 [local]
 type = tunnel
diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm
index db6f484..2821f98 100644
--- a/lib/Net/IMAP/InterIMAP.pm
+++ b/lib/Net/IMAP/InterIMAP.pm
@@ -214,10 +214,6 @@ our $IMAP_text;
 #
 #   - 'name': An optional instance name to include in log messages.
 #
-#   - 'extra-attrs': An attribute or list of extra attributes to FETCH
-#     when getting new mails, in addition to (MODSEQ FLAGS INTERNALDATE
-#     BODY.PEEK[]).
-#
 #   - 'logger-fd': An optional filehandle to use for debug output.
 #
 sub new($%) {
@@ -753,7 +749,7 @@ sub remove_message($@) {
     my $self = shift;
     my @set = @_;
     $self->fail("Server did not advertise UIDPLUS (RFC 4315) capability.")
-        if $self->incapable('UIDPLUS');
+        unless $self->_capable('UIDPLUS');
 
     my $set = compact_set(@set);
     $self->_send("UID STORE $set +FLAGS.SILENT (\\Deleted)");
@@ -785,15 +781,19 @@ sub remove_message($@) {
 # $self->append($mailbox, $mail, [...])
 #   Issue an APPEND command with the given mails.  Croak if the server
 #   did not advertise "UIDPLUS" (RFC 4315) in its CAPABILITY list.
-#   Providing multiple mails is only allowed for servers advertising
-#   "MULTIAPPEND" (RFC 3502) in their CAPABILITY list.
+#   Each $mail is a hash reference with key 'RFC822' and optionally
+#   'FLAGS' and 'INTERNALDATE'.  If the server supports the "BINARY"
+#   extension (RFC 3516), the key 'RFC822' can be replaced with 'BINARY'
+#   to send the mail body as a binary literal.
+#   Providing multiple mails is only allowed for servers supporting
+#   "MULTIAPPEND" (RFC 3502).
 #   Return the list of UIDs allocated for the new messages.
 sub append($$@) {
     my $self = shift;
     my $mailbox = shift;
     return unless @_;
     $self->fail("Server did not advertise UIDPLUS (RFC 4315) capability.")
-        if $self->incapable('UIDPLUS');
+        unless $self->_capable('UIDPLUS');
 
     my @appends;
     foreach my $mail (@_) {
@@ -801,11 +801,14 @@ sub append($$@) {
         $append .= '('.join(' ', grep {lc $_ ne '\recent'} @{$mail->{FLAGS}}).') '
             if defined $mail->{FLAGS};
         $append .= '"'.$mail->{INTERNALDATE}.'" ' if defined $mail->{INTERNALDATE};
-        $append .= "{".length($mail->{RFC822})."}\r\n".$mail->{RFC822};
+        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.")
-        if $#appends > 0 and $self->incapable('MULTIAPPEND');
+        unless $#appends == 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);
@@ -870,7 +873,7 @@ sub fetch($$$$) {
 sub notify($@) {
     my $self = shift;
     $self->fail("Server did not advertise NOTIFY (RFC 5465) capability.")
-        if $self->incapable('NOTIFY');
+        unless $self->_capable('NOTIFY');
     my $events = join ' ', qw/MessageNew MessageExpunge FlagChange MailboxName SubscriptionChange/;
     # Be notified of new messages with EXISTS/RECENT responses, but
     # don't receive unsolicited FETCH responses with a RFC822/BODY[].
@@ -1075,23 +1078,22 @@ sub pull_updates($;$) {
 }
 
 
-# $self->pull_new_messages($callback, @ignore)
+# $self->pull_new_messages($callback, $attrs, @ignore)
 #   FETCH new messages since the UIDNEXT found in the persistent cache
 #   (or 1 in no such UIDNEXT is found), and process each response on the
 #   fly with the callback.
+#   The list of attributes to FETCH, $attr, much contain either BODY or
+#   BINARY.
 #   If an @ignore list is supplied, then these messages are ignored from
 #   the UID FETCH range.
 #   Finally, update the UIDNEXT from the persistent cache to the value
 #   found in the internal cache.
 #   /!\ Use pull_updates afterwards to udpate the HIGHESTMODSEQ!
-sub pull_new_messages($$@) {
+sub pull_new_messages($$$@) {
     my $self = shift;
+    my $attrs = shift;
     my $callback = shift;
     my @ignore = sort { $a <=> $b } @_;
-    my @attrs = !defined $self->{'extra-attrs'} ? ()
-                   : ref $self->{'extra-attrs'} eq 'ARRAY' ? @{$self->{'extra-attrs'}}
-                   : ($self->{'extra-attrs'});
-    my $attrs = join ' ', qw/MODSEQ FLAGS INTERNALDATE/, @attrs, 'BODY.PEEK[]';
 
     my $mailbox = $self->{_SELECTED} // $self->panic();
 
@@ -1398,7 +1400,8 @@ sub _send($$;&) {
 
     while ($command =~ s/\A(.*?)\{([0-9]+)\}\r\n//) {
         my ($str, $len) = ($1, $2);
-        my $lit = substr $command, 0, $len, ''; # consume the literal
+        my $lit = substr $command, 0, $len, '';   # consume the literal
+        my $bin = substr($str,-1) eq '~' ? 1 : 0; # literal8, RFC 3516 BINARY
 
         $litplus //= $self->_capable('LITERAL+') ? '+' : '';
         push @command, $self->_z_deflate($str, "{$len$litplus}", "\r\n");
@@ -1412,15 +1415,26 @@ sub _send($$;&) {
             $x =~ /\A\+ / or $self->panic($x);
             @command = ();
         }
-        if ($len <= 4096) {
-            push @command, $self->_z_deflate($lit);
-        } else {
-            # send a Z_FULL_FLUSH at the start and end of large literals,
-            # as hinted at in RFC 4978 section 4
-            # TODO only do that for non-text literals
-            push @command, $self->_z_flush(Z_FULL_FLUSH);
+        if ($len > 4096 and (!$self->{'use-binary'} or $bin) and defined (my $d = $self->{_Z_DEFLATE})) {
+            my ($status, $buf);
+            # send a Z_FULL_FLUSH at the start and end of large non-text
+            # literals, as hinted at in RFC 4978 section 4
+            $status = $d->flush($buf, Z_FULL_FLUSH);
+            $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
+            push @command, $buf if $buf ne '';
+
+            undef $buf;
+            $status = $d->deflate($lit, $buf);
+            $self->panic("Deflation failed: ", $d->msg()) unless $status == Z_OK;
+            push @command, $buf if $buf ne '';
+
+            undef $buf;
+            $status = $d->flush($buf, Z_FULL_FLUSH);
+            $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
+            push @command, $buf if $buf ne '';
+        }
+        else {
             push @command, $self->_z_deflate($lit);
-            push @command, $self->_z_flush(Z_FULL_FLUSH);
         }
     }
     push @command, $self->_z_deflate($command, "\r\n");
@@ -1779,6 +1793,14 @@ sub _resp($$;$$$) {
                 elsif (s/\A(?:RFC822|BODY\[\]) //) {
                     $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(\$_);
+                    }
+                }
                 elsif (s/\AFLAGS \((\\?$RE_ATOM_CHAR+(?: \\?$RE_ATOM_CHAR+)*)?\)//) {
                     $mail{FLAGS} = defined $1 ? [ split / /, $1 ] : [];
                 }
@@ -1788,7 +1810,7 @@ sub _resp($$;$$$) {
             my $uid = $mail{UID} // $self->panic(); # sanity check
             $self->panic() unless defined $mail{MODSEQ} or !$self->_enabled('QRESYNC'); # sanity check
 
-            if (!exists $mail{RFC822} and !exists $mail{ENVELOPE} and # ignore new mails
+            if (!exists $mail{RFC822} and !exists $mail{BINARY} and !exists $mail{ENVELOPE} and # ignore new mails
                 (!exists $self->{_MODIFIED}->{$uid} or $self->{_MODIFIED}->{$uid}->[0] < $mail{MODSEQ} or
                     ($self->{_MODIFIED}->{$uid}->[0] == $mail{MODSEQ} and !defined $self->{_MODIFIED}->{$uid}->[1]))) {
                 my $flags = join ' ', sort(grep {lc $_ ne '\recent'} @{$mail{FLAGS}}) if defined $mail{FLAGS};
-- 
cgit v1.2.3


From ae69332edcf916e0e2af806e4969ef6402040816 Mon Sep 17 00:00:00 2001
From: Guilhem Moulin <guilhem@fripost.org>
Date: Wed, 9 Sep 2015 21:37:35 +0200
Subject: Refactoring.

---
 INSTALL                   |   2 +-
 lib/Net/IMAP/InterIMAP.pm | 169 +++++++++++++++++++++++-----------------------
 2 files changed, 84 insertions(+), 87 deletions(-)

diff --git a/INSTALL b/INSTALL
index b3f9ebc..f27952b 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,6 +1,6 @@
 InterIMAP depends on the following Perl modules:
 
-  - Compress::Raw::Zlib (core module)
+  - Compress::Zlib (core module)
   - Config::Tiny
   - DBI
   - DBD::SQLite
diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm
index 2821f98..6012049 100644
--- a/lib/Net/IMAP/InterIMAP.pm
+++ b/lib/Net/IMAP/InterIMAP.pm
@@ -20,7 +20,7 @@ package Net::IMAP::InterIMAP v0.0.1;
 use warnings;
 use strict;
 
-use Compress::Raw::Zlib qw/Z_OK Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/;
+use Compress::Zlib qw/Z_OK Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/;
 use Config::Tiny ();
 use Errno 'EWOULDBLOCK';
 use IO::Select ();
@@ -427,11 +427,11 @@ sub new($%) {
                 if ($algo eq 'DEFLATE') {
                     my ($status, $d, $i);
                     my %args = ( -WindowBits => 0 - MAX_WBITS );
-                    ($d, $status) = Compress::Raw::Zlib::Deflate::->new(%args);
+                    ($d, $status) = Compress::Zlib::deflateInit(%args);
                     $self->panic("Can't create deflation stream: ", $d->msg())
                         unless defined $d and $status == Z_OK;
 
-                    ($i, $status) = Compress::Raw::Zlib::Inflate::->new(%args);
+                    ($i, $status) = Compress::Zlib::inflateInit(%args);
                     $self->panic("Can't create inflation stream: ", $i->msg())
                         unless defined $i and $status == Z_OK;
                     @$self{qw/_Z_DEFLATE _Z_INFLATE/} = ($d, $i);
@@ -1254,9 +1254,9 @@ sub _getline($;$) {
             $self->{_OUTRAWCOUNT} += $n;
 
             if (defined (my $i = $self->{_Z_INFLATE})) {
-                my $status = $i->inflate($buf, my $data);
+                my ($out, $status) = $i->inflate($buf);
                 $self->panic("Inflation failed: ", $i->msg()) unless $status == Z_OK;
-                $buf = $data;
+                $buf = $out;
             }
             $self->{_OUTBUF} = $buf;
         }
@@ -1326,52 +1326,94 @@ sub _update_cache_for($$%) {
 
 
 # $self->_write(@data)
-#   Send the given @data to the IMAP server and flush the buffer.  If a
-#   compression layer is active, flush the deflation stream first.
+#   Send the given @data to the IMAP server.
 #   Update the interal raw byte count, but the regular byte count must
-#   have been updated earlier.
+#   have been updated earlier (eg, by _send_cmd).
 sub _write($@) {
     my $self = shift;
-    my @data = @_;
-
-    if (defined (my $d = $self->{_Z_DEFLATE})) {
-        my $status = $d->flush(my $buf, Z_SYNC_FLUSH);
-        $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
-        push @data, $buf if $buf ne '';
+    foreach (@_) {
+        next if $_ eq '';
+        $self->{STDIN}->write($_) // $self->panic("Can't write: $!");
+        $self->{_INRAWCOUNT} += length($_);
     }
-
-    my $data = join '', @data;
-    $self->{STDIN}->write($data) // $self->panic("Can't write: $!");
-    $self->{STDIN}->flush() // $self->panic("Can't flush: $!");
-    $self->{_INRAWCOUNT} += length($data);
-}
-
-
-# $self->_z_deflate(@data)
-#   Add the given @data to the deflation stream, and return the
-#   compressed data.
-#   This method is a noop if no compression layer is active.
-sub _z_deflate($@) {
-    my $self = shift;
-    my $data = join '', @_;
-    $self->{_INCOUNT} += length($data);
-    my $d = $self->{_Z_DEFLATE} // return @_;
-
-    my $status = $d->deflate($data, my $buf);
-    $self->panic("Deflation failed: ", $d->msg()) unless $status == Z_OK;
-    return ($buf) if $buf ne '';
 }
 
 
 # $self->_z_flush([$type])
-#   Flush the deflation stream, and return the compressed data.
+#   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 = shift;
+    my ($self,$t) = @_;
     my $d = $self->{_Z_DEFLATE} // return;
-    my $status = $d->flush(my $buf, @_);
+    my ($out, $status) = $d->flush($t);
     $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
-    return ($buf) if $buf ne '';
+    $self->_write($out);
+}
+
+
+# $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;
+    my $d = $self->{_Z_DEFLATE};
+
+    my ($offset, $litlen) = (0, 0);
+    my $z_flush = 0; # whether to flush the dictionary after processing the next literal
+
+    while(1) {
+        my $lit = substr($command, $offset, $litlen) if $litlen > 0;
+        $offset += $litlen;
+
+        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
+                (uc ($self->{'use-binary'} // 'YES') eq 'NO'
+                        or $line =~ /~\{[0-9]+\}\z/)            # literal8, RFC 3516 BINARY
+            ) ? 1 : 0;
+        }
+        $self->logger('C: ', ($offset == 0 ? "$tag " : '[...]'), $line) if $self->{debug};
+
+        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;
+
+                my ($out, $status) = $d->deflate($data[$i]);
+                $self->panic("Deflation failed: ", $d->msg()) unless $status == Z_OK;
+                $self->_write($out);
+
+                $self->_z_flush(Z_FULL_FLUSH) if $i == 0 and $z_flush;
+            }
+        }
+
+        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);
+        }
+
+        $z_flush = $z_flush2;
+        $offset = $idx+1;
+    }
 }
 
 
@@ -1393,53 +1435,7 @@ sub _send($$;&) {
     # 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}++;
-    my $litplus;
-
-    my @command = $self->_z_deflate("$tag ");
-    my $dbg_cmd = "C: $tag ";
-
-    while ($command =~ s/\A(.*?)\{([0-9]+)\}\r\n//) {
-        my ($str, $len) = ($1, $2);
-        my $lit = substr $command, 0, $len, '';   # consume the literal
-        my $bin = substr($str,-1) eq '~' ? 1 : 0; # literal8, RFC 3516 BINARY
-
-        $litplus //= $self->_capable('LITERAL+') ? '+' : '';
-        push @command, $self->_z_deflate($str, "{$len$litplus}", "\r\n");
-
-        $self->logger($dbg_cmd, $str, "{$len$litplus}") if $self->{debug};
-        $dbg_cmd = 'C: [...]';
-
-        unless ($litplus) {
-            $self->_write(@command);
-            my $x = $self->_getline();
-            $x =~ /\A\+ / or $self->panic($x);
-            @command = ();
-        }
-        if ($len > 4096 and (!$self->{'use-binary'} or $bin) and defined (my $d = $self->{_Z_DEFLATE})) {
-            my ($status, $buf);
-            # send a Z_FULL_FLUSH at the start and end of large non-text
-            # literals, as hinted at in RFC 4978 section 4
-            $status = $d->flush($buf, Z_FULL_FLUSH);
-            $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
-            push @command, $buf if $buf ne '';
-
-            undef $buf;
-            $status = $d->deflate($lit, $buf);
-            $self->panic("Deflation failed: ", $d->msg()) unless $status == Z_OK;
-            push @command, $buf if $buf ne '';
-
-            undef $buf;
-            $status = $d->flush($buf, Z_FULL_FLUSH);
-            $self->panic("Can't flush deflation stream: ", $d->msg()) unless $status == Z_OK;
-            push @command, $buf if $buf ne '';
-        }
-        else {
-            push @command, $self->_z_deflate($lit);
-        }
-    }
-    push @command, $self->_z_deflate($command, "\r\n");
-    $self->logger($dbg_cmd, $command) if $self->{debug};
-    $self->_write(@command);
+    $self->_send_cmd($tag, $command);
 
     my $r;
     # wait for the answer
@@ -1849,6 +1845,7 @@ sub _resp($$;$$$) {
             $x .= "\r\n";
             $self->{_INCOUNT} += length($x);
             $self->_write($x);
+            $self->{STDIN}->flush() // $self->panic("Can't flush: $!");
         }
     }
     else {
-- 
cgit v1.2.3


From 47fec06f75d365259c6bf1da78dfc4b5548ce5fe Mon Sep 17 00:00:00 2001
From: Guilhem Moulin <guilhem@fripost.org>
Date: Wed, 9 Sep 2015 22:30:00 +0200
Subject: Add a list of supported extensions.

---
 README      | 13 ++++++++-----
 interimap.1 | 29 ++++++++++++++++++++++++-----
 2 files changed, 32 insertions(+), 10 deletions(-)

diff --git a/README b/README
index 0e93fe9..2809ccb 100644
--- a/README
+++ b/README
@@ -23,11 +23,14 @@ the last synchronization only gives a phenomenal performance boost and
 drastically reduces the network traffic.
 
 For convenience reasons servers must also support LIST-EXTENDED
-[RFC5258], LIST-STATUS [RFC5819] and UIDPLUS [RFC4315].  Furthermore,
-while InterIMAP can work with servers lacking support for LITERAL+
-[RFC2088] and MULTIAPPEND [RFC3502], these extensions greatly improve
-performance by reducing the number of required round trips hence are
-recommended.
+[RFC5258], LIST-STATUS [RFC5819] and UIDPLUS [RFC4315].  Other supported
+extensions are:
+  * LITERAL+ [RFC2088] non-synchronizing literals (recommended),
+  * MULTIAPPEND [RFC3502] (recommended),
+  * COMPRESS=DEFLATE [RFC4978] (recommended),
+  * SASL-IR [RFC4959] SASL Initial Client Response,
+  * UNSELECT [RFC3691], and
+  * BINARY [RFC3516].
 
 
 #######################################################################
diff --git a/interimap.1 b/interimap.1
index e552351..988fa16 100644
--- a/interimap.1
+++ b/interimap.1
@@ -15,10 +15,7 @@ servers.
 Such synchronization is made possible by the QRESYNC extension from
 [RFC7162]; for convenience reasons servers must also support
 LIST\-EXTENDED [RFC5258], LIST\-STATUS [RFC5819] and UIDPLUS [RFC4315].
-Furthermore, while \fBInterIMAP\fR can work with servers lacking support
-for LITERAL+ [RFC2088] and MULTIAPPEND [RFC3502], these extensions
-greatly improve performance by reducing the number of required round
-trips hence are recommended.
+See also the \fBSUPPORTED EXTENSIONS\fR section.
 
 .PP
 Stateful synchronization is only possible for mailboxes supporting
@@ -336,6 +333,24 @@ 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.)
 
+.SH SUPPORTED EXTENSIONS
+
+Performance is better for servers supporting the following extensions to
+the IMAP4rev1 [RFC3501] protocol:
+
+.IP \[bu]
+LITERAL+ [RFC2088] non-synchronizing literals (recommended),
+.IP \[bu]
+MULTIAPPEND [RFC3502] (recommended),
+.IP \[bu]
+COMPRESS=DEFLATE [RFC4978] (recommended),
+.IP \[bu]
+SASL-IR [RFC4959] SASL Initial Client Response,
+.IP \[bu]
+UNSELECT [RFC3691], and
+.IP \[bu]
+BINARY [RFC3516].
+
 .SH KNOWN BUGS AND LIMITATIONS
 
 .IP \[bu]
@@ -354,10 +369,14 @@ a message is moved to another mailbox (using the MOVE command from
 [RFC6851] or COPY + STORE + EXPUNGE), moving a messages causes
 \fBInterIMAP\fR to believe that it was deleted while another one (which
 is replicated again) was added to the other mailbox in the meantime.
-
 .IP \[bu]
 \(lqPLAIN\(rq and \(lqLOGIN\(rq are the only authentication mechanisms
 currently supported.
+.IP \[bu]
+\fBInterIMAP\fR will probably not work with non RFC-compliant servers.
+In particular, no work-around are currently implemented beside the
+tunable in the configuration file.  Morever, few IMAP servers have been
+tested so far.
 
 .SH AUTHOR
 Written by Guilhem Moulin
-- 
cgit v1.2.3