diff options
author | Guilhem Moulin <guilhem@fripost.org> | 2016-03-07 18:21:46 +0100 |
---|---|---|
committer | Guilhem Moulin <guilhem@fripost.org> | 2016-03-07 18:21:46 +0100 |
commit | 58d6bd5be5ce08c55448a82ea20039808c1e214e (patch) | |
tree | 3c45eaf0aafd07fcad082595a6296b477d8573e5 | |
parent | 5f97601949f10e085a0df740c628cbfbfdf1d49f (diff) | |
parent | feb047ee7fcc1f93bc1e76626e16651761e0db55 (diff) |
Merge branch 'master' into debian
-rw-r--r-- | Changelog | 11 | ||||
-rw-r--r-- | INSTALL | 1 | ||||
-rw-r--r-- | README | 2 | ||||
-rwxr-xr-x | interimap | 12 | ||||
-rw-r--r-- | interimap.1 | 12 | ||||
-rw-r--r-- | interimap.sample | 3 | ||||
-rw-r--r-- | interimap.service | 2 | ||||
-rw-r--r-- | lib/Net/IMAP/InterIMAP.pm | 219 | ||||
-rwxr-xr-x | pullimap | 331 | ||||
-rw-r--r-- | pullimap.1 | 236 | ||||
-rw-r--r-- | pullimap.sample | 32 |
11 files changed, 802 insertions, 59 deletions
@@ -1,3 +1,14 @@ +interimap (0.3) upstream; + + * Fix byte count for compression streams. + * Add an option 'SSL_protocols' to list SSL protocols to enable or + disable. The default value, "!SSLv2 !SSLv3", enables only TLSv1 + and above. + * New script 'pullimap', to pull mails from an IMAP mailbox and + deliver them to a SMTP session. + + -- Guilhem Moulin <guilhem@guilhem.org> Mon, 28 Sep 2015 01:16:47 +0200 + interimap (0.2) upstream; * Add support for the IMAP COMPRESS extension [RFC4978]. By default @@ -7,7 +7,6 @@ InterIMAP depends on the following Perl modules: - Errno (core module) - Getopt::Long (core module) - MIME::Base64 (core module) if authentication is required - - IO::Select (core module) - List::Util (core module) - Net::SSLeay - POSIX (core module) @@ -1,4 +1,4 @@ -InterIMAP is a fast two-way synchronization program for QRESYNC-capable +InterIMAP is a fast bidirectional synchronization program for QRESYNC-capable IMAP4rev1 servers. Consult the manual for more information. @@ -1,8 +1,8 @@ #!/usr/bin/perl -T #---------------------------------------------------------------------- -# Fast two-way synchronization program for QRESYNC-capable IMAP servers -# Copyright © 2015 Guilhem Moulin <guilhem@fripost.org> +# Fast bidirectional synchronization for QRESYNC-capable IMAP servers +# Copyright © 2015,2016 Guilhem Moulin <guilhem@fripost.org> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,15 +21,16 @@ use strict; use warnings; -our $VERSION = '0.2'; +our $VERSION = '0.3'; my $NAME = 'interimap'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_compat bundling auto_version/; use DBI (); +use Fcntl qw/F_GETFL F_SETFL FD_CLOEXEC/; use List::Util 'first'; use lib 'lib'; -use Net::IMAP::InterIMAP qw/read_config compact_set $IMAP_text $IMAP_cond/; +use Net::IMAP::InterIMAP qw/read_config compact_set/; # Clean up PATH $ENV{PATH} = join ':', qw{/usr/local/bin /usr/bin /bin}; @@ -65,6 +66,7 @@ usage(1) if defined $COMMAND and defined $CONFIG{watch}; usage(1) if $CONFIG{target} and !(defined $COMMAND and ($COMMAND eq 'delete'or $COMMAND eq 'rename')); $CONFIG{watch} = 60 if defined $CONFIG{watch} and $CONFIG{watch} == 0; @ARGV = map {uc $_ eq 'INBOX' ? 'INBOX' : $_ } @ARGV; # INBOX is case-insensitive +die "Invalid mailbox name $_" foreach grep !/\A([\x01-\x7F]+)\z/, @ARGV; my $CONF = read_config( delete $CONFIG{config} // $NAME @@ -101,6 +103,8 @@ my ($DBFILE, $LOCKFILE, $LOGGER_FD); open $LOGGER_FD, '>>', $CONF->{_}->{logfile} or die "Can't open $CONF->{_}->{logfile}: $!\n"; $LOGGER_FD->autoflush(1); + my $flags = fcntl($LOGGER_FD, F_GETFL, 0) or die "fcntl F_GETFL: $!"; + fcntl($LOGGER_FD, F_SETFL, $flags | FD_CLOEXEC) or die "fcntl F_SETFL: $!"; } elsif ($CONFIG{debug}) { $LOGGER_FD = \*STDERR; diff --git a/interimap.1 b/interimap.1 index 60493f3..7058f62 100644 --- a/interimap.1 +++ b/interimap.1 @@ -1,8 +1,7 @@ .TH INTERIMAP "1" "JULY 2015" "InterIMAP" "User Commands" .SH NAME -InterIMAP \- Fast two-way synchronization program for QRESYNC-capable -IMAP servers +InterIMAP \- Fast bidirectional synchronization for QRESYNC-capable IMAP servers .SH SYNOPSIS .B interimap\fR [\fIOPTION\fR ...] [\fICOMMAND\fR] [\fIMAILBOX\fR ...] @@ -305,6 +304,15 @@ for type \fItype\fR=tunnel. (Default: \(lqNO\(rq.) .TP +.I SSL_protocols +A space-separated list of SSL protocols to enable or disable (if +prefixed with an exclamation mark \(oq!\(cq). Known protocols are +\(lqSSLv2\(rq, \(lqSSLv3\(rq, \(lqTLSv1\(rq, \(lqTLSv1.1\(rq, and +\(lqTLSv1.2\(rq. Enabling a protocol is a short-hand for disabling all +other protocols. +(Default: \(lq!SSLv2 !SSLv3\(rq, i.e., only enable TLSv1 and above.) + +.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 diff --git a/interimap.sample b/interimap.sample index 6d52f91..c3919ce 100644 --- a/interimap.sample +++ b/interimap.sample @@ -20,7 +20,8 @@ password = xxxxxxxxxxxxxxxx # SSL options 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_protocols = !SSLv2 !SSLv3 !TLSv1 !TLSv1.1 +#SSL_cipherlist = EECDH+AESGCM:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL #SSL_fingerprint = sha256$62E436BB329C46A628314C49BDA7C2A2E86C57B2021B9A964B8FABB6540D3605 # vim:ft=dosini diff --git a/interimap.service b/interimap.service index 2dc1506..8c685d9 100644 --- a/interimap.service +++ b/interimap.service @@ -1,5 +1,5 @@ [Unit] -Description=Fast two-way synchronization program for QRESYNC-capable IMAP servers +Description=Fast bidirectional synchronization for QRESYNC-capable IMAP servers Wants=network-online.target After=network-online.target diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index 3a6481e..be62a9d 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -16,13 +16,14 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. #---------------------------------------------------------------------- -package Net::IMAP::InterIMAP v0.0.2; +package Net::IMAP::InterIMAP v0.0.3; use warnings; use strict; use Compress::Raw::Zlib qw/Z_OK Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/; use Config::Tiny (); -use IO::Select (); +use Errno 'EINTR'; +use Fcntl qw/F_GETFL F_SETFL FD_CLOEXEC/; use Net::SSLeay (); use List::Util qw/all first/; use POSIX ':signal_h'; @@ -43,6 +44,8 @@ my $RE_ATOM_CHAR = qr/[\x21\x23\x24\x26\x27\x2B-\x5B\x5E-\x7A\x7C-\x7E]/; my $RE_ASTRING_CHAR = qr/[\x21\x23\x24\x26\x27\x2B-\x5B\x5D-\x7A\x7C-\x7E]/; my $RE_TEXT_CHAR = qr/[\x01-\x09\x0B\x0C\x0E-\x7F]/; +my $RE_SSL_PROTO = qr/(?:SSLv[23]|TLSv1|TLSv1\.[0-2])/; + # Map each option to a regexp validating its values. my %OPTIONS = ( host => qr/\A(\P{Control}+)\z/, @@ -56,6 +59,7 @@ my %OPTIONS = ( 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_protocols => qr/\A(!?$RE_SSL_PROTO(?: !?$RE_SSL_PROTO)*)\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, @@ -485,11 +489,11 @@ sub stats($) { $msg .= ' recv '._kibi($self->{_OUTCOUNT}); $msg .= ' (compr. '._kibi($self->{_OUTRAWCOUNT}). ', factor '.sprintf('%.2f', $self->{_OUTRAWCOUNT}/$self->{_OUTCOUNT}).')' - if defined $self->{_Z_DEFLATE} and $self->{_OUTCOUNT} > 0; + if exists $self->{_Z_DEFLATE} and $self->{_OUTCOUNT} > 0; $msg .= ' sent '._kibi($self->{_INCOUNT}); $msg .= ' (compr. '._kibi($self->{_INRAWCOUNT}). ', factor '.sprintf('%.2f', $self->{_INRAWCOUNT}/$self->{_INCOUNT}).')' - if defined $self->{_Z_DEFLATE} and $self->{_INCOUNT} > 0; + if exists $self->{_Z_DEFLATE} and $self->{_INCOUNT} > 0; $self->log($msg); } @@ -520,9 +524,10 @@ sub log($@) { return unless @_; $self->logger(@_) if defined $self->{'logger-fd'} and defined $self->{'logger-fd'}->fileno and $self->{'logger-fd'}->fileno != fileno STDERR; - my $prefix = defined $self->{name} ? $self->{name} : ''; + my $prefix = $self->{name} // ''; $prefix .= "($self->{_SELECTED})" if $self->{_STATE} eq 'SELECTED'; - print STDERR $prefix, ': ', @_, "\n"; + $prefix .= ': ' unless $prefix eq ''; + print STDERR $prefix, @_, "\n"; } sub logger($@) { my $self = shift; @@ -531,11 +536,13 @@ sub logger($@) { if (defined $self->{'logger-fd'}->fileno and defined $self->{'logger-fd'}->fileno and $self->{'logger-fd'}->fileno != fileno STDERR) { my ($s, $us) = Time::HiRes::gettimeofday(); - $prefix = POSIX::strftime("%b %e %H:%M:%S", localtime($s)).".$us "; + $prefix = POSIX::strftime("%b %e %H:%M:%S", localtime($s)).".$us"; + $prefix .= ' ' if defined $self->{name} or $self->{_STATE} eq 'SELECTED'; } - $prefix .= defined "$self->{name}" ? $self->{name} : ''; + $prefix .= $self->{name} if defined $self->{name}; $prefix .= "($self->{_SELECTED})" if $self->{_STATE} eq 'SELECTED'; - $self->{'logger-fd'}->say($prefix, ': ', @_); + $prefix .= ': ' unless $prefix eq ''; + $self->{'logger-fd'}->say($prefix, @_); } @@ -731,6 +738,7 @@ sub rename($$$;$) { # If $try is set, print a warning but don't crash if the command fails. sub subscribe($$;$) { my ($self, $mailbox, $try) = @_; + $mailbox = 'INBOX' if uc $mailbox eq 'INBOX'; # INBOX is case-insensitive my $r = $self->_send("SUBSCRIBE ".quote($mailbox)); if ($IMAP_cond eq 'OK') { $self->log("Subscribe to ".$mailbox) unless $self->{quiet}; @@ -743,6 +751,7 @@ sub subscribe($$;$) { } sub unsubscribe($$;$) { my ($self, $mailbox, $try) = @_; + $mailbox = 'INBOX' if uc $mailbox eq 'INBOX'; # INBOX is case-insensitive my $r = $self->_send("UNSUBSCRIBE ".quote($mailbox)); if ($IMAP_cond eq 'OK') { $self->log("Unsubscribe to ".$mailbox) unless $self->{quiet}; @@ -831,6 +840,7 @@ sub append($$@) { # dump the cache before issuing the command if we're appending to the current mailbox my ($UIDNEXT, $EXISTS, $cache, %vanished); + $mailbox = 'INBOX' if uc $mailbox eq 'INBOX'; # INBOX is case-insensitive if (defined $self->{_SELECTED} and $mailbox eq $self->{_SELECTED}) { $cache = $self->{_CACHE}->{$mailbox}; $UIDNEXT = $cache->{UIDNEXT} // $self->panic(); @@ -925,36 +935,68 @@ sub notify($@) { my $command = 'NOTIFY '; $command .= @_ ? ('SET '. join(' ', map {"($_ ($events))"} @_)) : 'NONE'; $self->_send($command); - $self->{_SEL_OUT} = IO::Select::->new($self->{STDOUT}); } -# $self->slurp() +# $self->slurp([$cmd, $callback]) # See if the server has sent some unprocessed data; try to as many # lines as possible, process them, and return the number of lines # read. # This is mostly useful when waiting for notifications while no -# command is progress, cf. RFC 5465 (NOTIFY). -sub slurp($) { - my $self = shift; - +# command is progress, cf. RFC 2177 (IDLE) or RFC 5465 (NOTIFY). +sub slurp($;$$) { + my ($self, $cmd, $callback) = @_; my $ssl = $self->{_SSL}; my $read = 0; + vec(my $rin, fileno($self->{STDOUT}), 1) = 1; while (1) { - # Unprocessed data within the current TLS record would cause - # select(2) to block/timeout due to the raw socket not being - # ready. - 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() ); + unless ((defined $self->{_OUTBUF} and $self->{_OUTBUF} ne '') or + # Unprocessed data within the current TLS record would + # cause select(2) to block/timeout due to the raw socket + # not being ready. + (defined $ssl and Net::SSLeay::pending($ssl) > 0)) { + my $r = CORE::select($rin, undef, undef, 0); + next if $r == -1 and $! == EINTR; # select(2) was interrupted + $self->panic("Can't select: $!") if $r == -1; + return $read if $r == 0; # nothing more to read + } + my $x = $self->_getline(); + $self->_resp($x, $cmd, undef, $callback); $read++; } } +# $self->idle([$timeout, $stopwhen]) +# Enter IDLE (RFC 2177) for $timout seconds (by default 29 mins), or +# when the callback $stopwhen returns true. +# Return false if the timeout was reached, and true if IDLE was +# stopped due the callback. +sub idle($$$) { + my ($self, $timeout, $stopwhen) = @_; + $timeout //= 1740; # 29 mins + + $self->fail("Server did not advertise IDLE (RFC 2177) capability.") + unless $self->_capable('IDLE'); + + my $tag = $self->_cmd_init('IDLE'); + $self->_cmd_flush(); + + for (; $timeout > 0; $timeout--) { + $self->slurp('IDLE', sub() {$timeout = -1 if $stopwhen->()}); + sleep 1 if $timeout > 0; + } + + # done idling + $self->_cmd_extend('DONE'); + $self->_cmd_flush(); + $self->_recv($tag); + + return $timeout < 0 ? 1 : 0; +} + + # $self->set_cache( $mailbox, STATE ) # Initialize or update the persistent cache, that is, associate a # known $mailbox with the last known (synced) state: @@ -970,6 +1012,7 @@ sub slurp($) { sub set_cache($$%) { my $self = shift; my $mailbox = shift // $self->panic(); + $mailbox = 'INBOX' if uc $mailbox eq 'INBOX'; # INBOX is case-insensitive my $cache = $self->{_PCACHE}->{$mailbox} //= {}; my %status = @_; @@ -996,6 +1039,7 @@ sub uidvalidity($;$) { my $self = shift; my $mailbox = shift; if (defined $mailbox) { + $mailbox = 'INBOX' if uc $mailbox eq 'INBOX'; # INBOX is case-insensitive my $cache = $self->{_CACHE}->{$mailbox} // return; return $cache->{UIDVALIDITY}; } @@ -1030,21 +1074,21 @@ sub get_cache($@) { # $self->is_dirty($mailbox) -# Return true if there are pending updates for $mailbox, i.e., its -# internal cache is newer than its persistent cache. +# Return true if there are pending updates for $mailbox, i.e., if its +# internal cache's HIGHESTMODSEQ or UIDNEXT values differ from its +# persistent cache's values. sub is_dirty($$) { my ($self, $mailbox) = @_; - my $cache = $self->{_CACHE}->{$mailbox} // return 1; - my $pcache = $self->{_PCACHE}->{$mailbox} // return 1; + $self->_updated_cache($mailbox, qw/HIGHESTMODSEQ UIDNEXT/); +} - if (defined $pcache->{HIGHESTMODSEQ} and defined $cache->{HIGHESTMODSEQ} - and $pcache->{HIGHESTMODSEQ} == $cache->{HIGHESTMODSEQ} and - defined $pcache->{UIDNEXT} and defined $cache->{UIDNEXT} - and $pcache->{UIDNEXT} == $cache->{UIDNEXT}) { - return 0 - } else { - return 1 - } + +# $self->has_new_mails($mailbox) +# Return true if there are new messages in $mailbox, i.e., if its +# internal cache's UIDNEXT value differs from its persistent cache's. +sub has_new_mails($$) { + my ($self, $mailbox) = @_; + $self->_updated_cache($mailbox, 'UIDNEXT'); } @@ -1242,6 +1286,31 @@ sub push_flag_updates($$@) { } +# $self->silent_store($set, $mod, @flags) +# Set / Add / Remove the flags list on the UID $set, depending on the +# value of $mod ('' / '+' / '-'). +# /!\ There is no guaranty that message flags are successfully updated! +sub silent_store($$$@) { + my $self = shift; + my $set = shift; + my $mod = shift; + $self->_send("UID STORE $set ${mod}FLAGS.SILENT (".join(' ', @_).")"); +} + + +# $self->expunge($set) +# Exunge the given UID $set. +# /!\ There is no guaranty that messages are successfully expunged! +sub expunge($$) { + my $self = shift; + my $set = shift; + + $self->fail("Server did not advertise UIDPLUS (RFC 4315) capability.") + unless $self->_capable('UIDPLUS'); + $self->_send("UID EXPUNGE $set"); +} + + ############################################################################# # Private methods @@ -1299,7 +1368,13 @@ sub _tcp_connect($$$) { foreach my $ai (@res) { socket my $s, $ai->{family}, $ai->{socktype}, $ai->{protocol}; - return $s if defined $s and connect($s, $ai->{addr}); + # TODO: add a connection timeout + # http://devpit.org/wiki/Connect%28%29_with_timeout_%28in_Perl%29 + if (defined $s and connect($s, $ai->{addr})) { + my $flags = fcntl($s, F_GETFL, 0) or $self->panic("fcntl F_GETFL: $!"); + fcntl($s, F_SETFL, $flags | FD_CLOEXEC) or $self->panic("fcntl F_SETFL: $!"); + return $s; + } } $self->fail("Can't connect to $host:$port"); } @@ -1460,20 +1535,42 @@ sub _ssl_verify($$$) { return $ok; # 1=accept cert, 0=reject } +my %SSL_proto = ( + 'SSLv2' => Net::SSLeay::OP_NO_SSLv2(), + 'SSLv3' => Net::SSLeay::OP_NO_SSLv3(), + 'TLSv1' => Net::SSLeay::OP_NO_TLSv1(), + 'TLSv1.1' => Net::SSLeay::OP_NO_TLSv1_1(), + 'TLSv1.2' => Net::SSLeay::OP_NO_TLSv1_2() +); # $self->_start_ssl($socket) # Upgrade the $socket to SSL/TLS. sub _start_ssl($$) { my ($self, $socket) = @_; my $ctx = Net::SSLeay::CTX_new() or $self->panic("Failed to create SSL_CTX $!"); + my $ssl_options = Net::SSLeay::OP_SINGLE_DH_USE() | Net::SSLeay::OP_SINGLE_ECDH_USE(); + + $self->{SSL_protocols} //= q{!SSLv2 !SSLv3}; + my ($proto_include, $proto_exclude) = (0, 0); + foreach (split /\s+/, $self->{SSL_protocols}) { + my $neg = s/^!// ? 1 : 0; + s/\.0$//; + ($neg ? $proto_exclude : $proto_include) |= $SSL_proto{$_} // $self->panic("Unknown SSL protocol: $_"); + } + if ($proto_include != 0) { + # exclude all protocols except those explictly included + my $x = 0; + $x |= $_ foreach values %SSL_proto; + $x &= ~ $proto_include; + $proto_exclude |= $x; + } + my @proto_exclude = grep { ($proto_exclude & $SSL_proto{$_}) != 0 } keys %SSL_proto; + $self->log("Disabling SSL protocol: ".join(', ', sort @proto_exclude)) if $self->{debug}; + $ssl_options |= $SSL_proto{$_} foreach @proto_exclude; + $ssl_options |= Net::SSLeay::OP_NO_COMPRESSION(); # 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() ); + Net::SSLeay::CTX_set_options($ctx, $ssl_options); # https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_mode.html Net::SSLeay::CTX_set_mode($ctx, @@ -1625,6 +1722,24 @@ sub _update_cache_for($$%) { } +# $self->_updated_cache($mailbox) +# Return true if there are pending updates for $mailbox, i.e., if one +# of its internal cache's @attrs value differs from the persistent +# cache's value. +sub _updated_cache($$@) { + my ($self, $mailbox, @attrs) = @_; + $mailbox = 'INBOX' if uc $mailbox eq 'INBOX'; # INBOX is case-insensitive + my $cache = $self->{_CACHE}->{$mailbox} // return 1; + my $pcache = $self->{_PCACHE}->{$mailbox} // return 1; + + foreach (@attrs) { + return 1 unless $pcache->{$_} and defined $cache->{$_} and + $pcache->{$_} == $cache->{$_}; + } + return 0; +} + + # $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 @@ -1892,11 +2007,11 @@ sub _select_or_examine($$$;$$) { my $mailbox = shift; my ($seqs, $uids) = @_; + $mailbox = uc $mailbox eq 'INBOX' ? 'INBOX' : $mailbox; # INBOX is case-insensitive my $pcache = $self->{_PCACHE}->{$mailbox} //= {}; my $cache = $self->{_CACHE}->{$mailbox} //= {}; $cache->{UIDVALIDITY} = $pcache->{UIDVALIDITY} if defined $pcache->{UIDVALIDITY}; - $mailbox = uc $mailbox eq 'INBOX' ? 'INBOX' : $mailbox; # INBOX is case-insensitive $command .= ' '.quote($mailbox); if ($self->_enabled('QRESYNC') and ($pcache->{HIGHESTMODSEQ} // 0) > 0 and ($pcache->{UIDNEXT} // 1) > 1) { $command .= " (QRESYNC ($pcache->{UIDVALIDITY} $pcache->{HIGHESTMODSEQ} " @@ -2078,7 +2193,7 @@ sub _envelope($$) { return \@envelope; } -# $self->_resp($buf, [$cmd, $callback] ) +# $self->_resp($buf, [$cmd, $set, $callback] ) # Parse an untagged response line or a continuation request line. # (The trailing CRLF must be removed.) The internal cache is # automatically updated when needed. @@ -2119,8 +2234,10 @@ sub _resp($$;$$$) { } elsif (/\A([0-9]+) EXPUNGE\z/) { # /!\ No bookkeeping since there is no internal cache mapping sequence numbers to UIDs - $self->panic("$1 <= $cache->{EXISTS}") if $1 <= $cache->{EXISTS}; # sanity check - $self->fail("RFC 7162 violation! Got an EXPUNGE response with QRESYNC enabled.") if $self->_enabled('QRESYNC'); + if ($self->_enabled('QRESYNC')) { + $self->panic("$1 <= $cache->{EXISTS}") if $1 <= $cache->{EXISTS}; # sanity check + $self->fail("RFC 7162 violation! Got an EXPUNGE response with QRESYNC enabled."); + } $cache->{EXISTS}--; # explicit EXISTS responses are optional } elsif (/\ASEARCH((?: [0-9]+)*)\z/) { @@ -2180,16 +2297,18 @@ sub _resp($$;$$$) { undef $first; } - my $uid = $mail{UID} // $self->panic(); # sanity check $self->panic() unless defined $mail{MODSEQ} or !$self->_enabled('QRESYNC'); # sanity check + my $uid = $mail{UID}; if (!exists $mail{RFC822} and !exists $mail{ENVELOPE} and # ignore new mails + defined $uid and # /!\ ignore unsolicited FETCH responses without UID, cf RFC 7162 section 3.2.4 (!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}; $self->{_MODIFIED}->{$uid} = [ $mail{MODSEQ}, $flags ]; } - $callback->(\%mail) if defined $callback and ($cmd eq 'FETCH' or $cmd eq 'STORE') and in_set($uid, $set); + $callback->(\%mail) if defined $callback and ($cmd eq 'FETCH' or $cmd eq 'STORE') and + defined $uid and in_set($uid, $set); } elsif (/\AENABLED((?: $RE_ATOM_CHAR+)+)\z/) { # RFC 5161 ENABLE $self->{_ENABLED} //= []; @@ -2215,7 +2334,8 @@ sub _resp($$;$$$) { } } } - elsif (s/\A\+ //) { + elsif (s/\A\+// and ($_ eq '' or s/\A //)) { + # Microsoft Exchange Server 2010 violates RFC 3501 by skipping the trailing ' ' for empty resp-text if (defined $callback and $cmd eq 'AUTHENTICATE') { my $x = $callback->($_); $self->_cmd_extend(\$x); @@ -2225,6 +2345,7 @@ sub _resp($$;$$$) { else { $self->panic("Unexpected response: ", $_); } + $callback->() if defined $callback and $cmd eq 'IDLE'; } diff --git a/pullimap b/pullimap new file mode 100755 index 0000000..27226d2 --- /dev/null +++ b/pullimap @@ -0,0 +1,331 @@ +#!/usr/bin/perl -T + +#---------------------------------------------------------------------- +# Pull mails from an IMAP mailbox and deliver them to a SMTP session +# Copyright © 2016 Guilhem Moulin <guilhem@fripost.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +#---------------------------------------------------------------------- + +use strict; +use warnings; + +our $VERSION = '0.3'; +my $NAME = 'pullimap'; + +use Errno 'EINTR'; +use Fcntl qw/O_CREAT O_RDWR O_DSYNC LOCK_EX SEEK_SET F_GETFL F_SETFL FD_CLOEXEC/; +use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; +use List::Util 'first'; +use Socket qw/PF_INET PF_INET6 SOCK_STREAM/; + +use lib 'lib'; +use Net::IMAP::InterIMAP qw/read_config compact_set/; + +my %CONFIG; +sub usage(;$) { + my $rv = shift // 0; + if ($rv) { + print STDERR "Usage: $NAME [OPTIONS] SECTION\n" + ."Try '$NAME --help' or consult the manpage for more information.\n"; + } + else { + print STDERR "Usage: $NAME [OPTIONS] SECTION\n" + ."Consult the manpage for more information.\n"; + } + exit $rv; +} + +usage(1) unless GetOptions(\%CONFIG, qw/config=s quiet|q debug help|h idle:i no-delivery/); +usage(0) if $CONFIG{help}; +usage(1) unless $#ARGV == 0 and $ARGV[0] ne '_'; + + +####################################################################### +# Read and validate configuration +# +my $CONF = read_config( delete $CONFIG{config} // $NAME, + , [$ARGV[0]] + , statefile => qr/\A(\P{Control}+)\z/ + , mailbox => qr/\A([\x01-\x7F]+)\z/ + , 'deliver-method' => qr/\A([ls]mtp:\[.*\]:\d+)\z/ + , 'deliver-ehlo' => qr/\A(\P{Control}+)\z/ + , 'deliver-rcpt' => qr/\A(\P{Control}+)\z/ + , 'purge-after' => qr/\A(\d+)\z/ + )->{$ARGV[0]}; + +my ($MAILBOX, $STATE); +do { + $MAILBOX = $CONF->{mailbox} // 'INBOX'; + + my $statefile = $CONF->{statefile} // $ARGV[0]; + die "Missing option statefile" unless defined $statefile; + $statefile = $statefile =~ /\A(\p{Print}+)\z/ ? $1 : die "Insecure $statefile"; + + unless ($statefile =~ /\A\//) { + my $dir = ($ENV{XDG_DATA_HOME} // "$ENV{HOME}/.local/share") .'/'. $NAME; + $dir = $dir =~ /\A(\/\p{Print}+)\z/ ? $1 : die "Insecure $dir"; + $statefile = $dir .'/'. $statefile; + unless (-d $dir) { + mkdir $dir, 0700 or die "Can't mkdir $dir: $!\n"; + } + } + + sysopen($STATE, $statefile, O_CREAT|O_RDWR|O_DSYNC, 0600) or die "Can't open $statefile: $!"; + my $flags = fcntl($STATE, F_GETFL, 0) or die "fcntl F_GETFL: $!"; + fcntl($STATE, F_SETFL, $flags | FD_CLOEXEC) or die "fcntl F_SETFL: $!"; + + flock($STATE, LOCK_EX) or die "Can't flock $statefile: $!"; +}; + + +####################################################################### + +# Read a UID (32-bits integer) from the statefile, or undef if we're at +# the end of the statefile +sub readUID() { + my $n = sysread($STATE, my $buf, 4) // die "Can't sysread: $!"; + return if $n == 0; # EOF + # file length is a multiple of 4 bytes, and we always read 4 bytes at a time + die "Corrupted state file!" if $n != 4; + unpack('N', $buf); +} + +# Write a UID (32-bits integer) to the statefile +sub writeUID($) { + my $uid = pack('N', shift); + my $offset = 0; + for ( my $offset = 0 + ; $offset < 4 + ; $offset += syswrite($STATE, $uid, 4-$offset, $offset) // die "Can't syswrite: $!" + ) {} +} + + +####################################################################### +# SMTP/LMTP part +# +my ($SMTP, $SMTP_PIPELINING); +sub sendmail($$) { + my ($from, $rfc822) = @_; + unless (defined $SMTP) { + # TODO we need to be able to reconnect when the server closes + # the connection due to a timeout (RFC 5321 section 4.5.3.2) + my ($fam, $addr) = (PF_INET, $CONF->{'deliver-method'} // 'smtp:[127.0.0.1]:25'); + $addr =~ s/^([ls]mtp):// or die; + my $ehlo = $1 eq 'lmtp' ? 'LHO' : $1 eq 'smtp' ? 'EHLO' : die; + $ehlo .= ' '. ($CONF->{'deliver-ehlo'} // 'localhost.localdomain'); + + my $port = $addr =~ s/:(\d+)$// ? $1 : die; + $addr =~ s/^\[(.*)\]$/$1/ or die; + $fam = PF_INET6 if $addr =~ /:/; + $addr = Socket::inet_pton($fam, $addr) // die "Invalid address $addr\n"; + my $sockaddr = $fam == PF_INET ? Socket::pack_sockaddr_in($port, $addr) + : $fam == PF_INET6 ? Socket::pack_sockaddr_in6($port, $addr) + : die; + + my $proto = getprotobyname("tcp") // die; + socket($SMTP, $fam, SOCK_STREAM, $proto) or die "socket: $!"; + until (connect($SMTP, $sockaddr)) { + next if $! == EINTR; # try again if connect(2) was interrupted by a signal + die "connect: $!"; + } + + smtp_resp('220'); + my @r = smtp_send($ehlo => '250'); + $SMTP_PIPELINING = grep {$_ eq 'PIPELINING'} @r; # SMTP pipelining (RFC 2920) + } + my $rcpt = $CONF->{'deliver-rcpt'} // getpwuid($>) // die; + + # return codes are from RFC 5321 section 4.3.2 + smtp_send( "MAIL FROM:<$from>" => '250' + , "RCPT TO:<$rcpt>" => '250' + , "DATA" => '354' + ); + + print STDERR "C: [...]\n" if $CONFIG{debug}; + if ($$rfc822 eq '') { + # RFC 5321 section 4.1.1.4: if there was no mail data, the first + # "\r\n" ends the DATA command itself + $SMTP->printflush("\r\n.\r\n"); + } else { + my $offset = 0; + my $length = length($$rfc822); + while ((my $end = index($$rfc822, "\r\n", $offset) + 2) != 1) { + my $line = substr($$rfc822, $offset, $end-$offset); + # RFC 5321 section 4.5.2: the character sequence "\r\n.\r\n" + # ends the mail text and cannot be sent by the user + $SMTP->print($line eq ".\r\n" ? "..\r\n" : $line); + $offset = $end; + } + if ($offset < $length) { + # the last line did not end with "\r\n"; add it in order to + # have the receiving SMTP server recognize the "end of data" + # condition. See RFC 5321 section 4.1.1.4 + my $line = substr($$rfc822, $offset); + $SMTP->print(($line eq "." ? ".." : $line), "\r\n"); + } + $SMTP->printflush(".\r\n"); + } + smtp_resp('250'); +} +sub smtp_resp($) { + my $code = shift; + my @resp; + while(1) { + local $_ = $SMTP->getline() // die; + s/\r\n\z// or die "Invalid SMTP reply: $_"; + print STDERR "S: $_\n" if $CONFIG{debug}; + s/\A\Q$code\E([ -])// or die "SMTP error: Expected $code, got: $_\n"; + push @resp, $_; + return @resp if $1 eq ' '; + } +} +sub smtp_send(@) { + my (@cmd, @code, @r); + while (@_) { + push @cmd, shift // die; + push @code, shift // die; + } + if ($SMTP_PIPELINING) { # SMTP pipelining (RFC 2920) + print STDERR join('', map {"C: $_\n"} @cmd) if $CONFIG{debug}; + $SMTP->printflush(join('', map {"$_\r\n"} @cmd)); + @r = smtp_resp($_) foreach @code; + } + else { + foreach (@cmd) { + print STDERR "C: $_\n" if $CONFIG{debug}; + $SMTP->printflush("$_\r\n"); + @r = smtp_resp(shift(@code)); + } + } + return @r; +} + + +####################################################################### +# Initialize the cache from the statefile, then pull new messages from +# the remote mailbox +# +$CONF->{'logger-fd'} = \*STDERR if $CONFIG{debug}; +my $IMAP = Net::IMAP::InterIMAP::->new( %$CONF, %CONFIG{qw/quiet debug/} ); + +# Remove messages with UID < UIDNEXT and INTERNALDATE at most +# $CONF->{'purge-after'} days ago. +my $LAST_PURGED; +sub purge() { + my $days = $CONF->{'purge-after'} // return; + my ($uidnext) = $IMAP->get_cache('UIDNEXT'); + return unless 1<$uidnext; + my $set = "1:".($uidnext-1); + + my $now = time; + return if defined $LAST_PURGED and $now - $LAST_PURGED < 6*3600; + $LAST_PURGED = $now; + + unless ($days == 0) { + my @now = gmtime($now - $days*86400); + my @m = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/; # RFC 3501's date-month + my $date = sprintf("%02d-%s-%04d", $now[3], $m[$now[4]], $now[5]+1900); + my @uid = $IMAP->search("UID $set BEFORE $date"); + return unless @uid; + + $set = compact_set(@uid); + $IMAP->log("Removing ".($#uid+1)." UID(s) $set") unless $CONFIG{quiet}; + } + $IMAP->silent_store($set, '+', '\Deleted'); + $IMAP->expunge($set); +} + +# Use BODY.PEEK[] so if something gets wrong, unpulled messages +# won't be marked as \Seen in the mailbox +my $ATTRS = "ENVELOPE INTERNALDATE BODY.PEEK[]"; + +# Pull new messages from IMAP and deliver them to SMTP, then update the +# statefile +sub pull(;$) { + my $ignore = shift // []; + my @uid; + + # invariant: we're at pos 8 + 4*(1+$#ignore + 1+$#uids) in the statefile + $IMAP->pull_new_messages($ATTRS, sub($) { + my $mail = shift; + return unless exists $mail->{RFC822}; # not for us + + my $uid = $mail->{UID}; + my $from = first { defined $_ and @$_ } @{$mail->{ENVELOPE}}[2,3,4]; + $from = (defined $from and @$from) ? $from->[0]->[2].'@'.$from->[0]->[3] : ''; + print STDERR "($MAILBOX): UID $uid from <$from> ($mail->{INTERNALDATE})\n" unless $CONFIG{quiet}; + + sendmail($from, $mail->{RFC822}) unless $CONFIG{'no-delivery'}; + + push @uid, $uid; + writeUID($uid); + }, @$ignore); + + # terminate the SMTP transmission channel gracefully, cf RFC 5321 section 4.5.3.2 + smtp_send('QUIT' => '221') if defined $SMTP; + undef $SMTP; + + # now that everything has been deliverd, mark @ignore and @uid as \Seen + $IMAP->silent_store(compact_set(@$ignore, @uid), '+', '\Seen') if @$ignore or @uid; + + # update the statefile + sysseek($STATE, 4, SEEK_SET) // die "Can't seek: $!"; + my ($uidnext) = $IMAP->get_cache('UIDNEXT'); + writeUID($uidnext); + truncate($STATE, 8) // die "Can't truncate"; +} + +do { + my $uidvalidity = readUID(); + my $uidnext = readUID(); + my $ignore = []; + + $IMAP->set_cache($MAILBOX, UIDVALIDITY => $uidvalidity, UIDNEXT => $uidnext); + $IMAP->select($MAILBOX); + + unless (defined $uidvalidity) { + ($uidvalidity) = $IMAP->get_cache('UIDVALIDITY'); + # we were at pos 0 before the write, at pos 4 afterwards + writeUID($uidvalidity); + die if defined $uidnext; # sanity check + } + + if (!defined $uidnext) { + # we were at pos 4 before the write, at pos 8 afterwards + writeUID(1); + } + else { + # put the remaining UIDs in the @ignore list: these messages + # have already been delivered, but the process exited before the + # statefile was updated + while (defined (my $uid = readUID())) { + push @$ignore, $uid; + } + } + pull($ignore); + purge(); +}; +unless (defined $CONFIG{idle}) { + $IMAP->logout(); + exit 0; +} + +$CONFIG{idle} = 1740 if defined $CONFIG{idle} and $CONFIG{idle} == 0; # 29 mins +while(1) { + my $r = $IMAP->idle($CONFIG{idle}, sub() { $IMAP->has_new_mails($MAILBOX) }); + pull() if $r; + purge(); +} diff --git a/pullimap.1 b/pullimap.1 new file mode 100644 index 0000000..e0f1ec3 --- /dev/null +++ b/pullimap.1 @@ -0,0 +1,236 @@ +.TH PULLIMAP "1" "MARCH 2016" "PullIMAP" "User Commands" + +.SH NAME +PullIMAP \- Pull mails from an IMAP mailbox and deliver them to a SMTP session + +.SH SYNOPSIS +.B pullimap\fR [\fB--config=\fIFILE\fR] [\fB--idle\fR[\fB=\fISECONDS\fR]] +[\fB--no-delivery\fR] [\fB--quiet\fR] \fISECTION\fR + + +.SH DESCRIPTION +.PP +.B PullIMAP\fR retrives messages from an IMAP mailbox and deliver them +to a SMTP or LMTP transmission channel. +It can also remove delivered messages after a configurable retention +period. + +.PP +A statefile is used to keep track of the mailbox's UIDVALIDITY and +UIDNEXT values. While \fBPullIMAP\fR is running, the statefile is also +used to keep track of UIDs being delivered, which avoids duplicate +deliveries if the process is interrupted. + +.SH OPTIONS +.TP +.B \-\-config=\fR\fIFILE\fR +Specify an alternate configuration file. Relative paths start from +\fI$XDG_CONFIG_HOME\fR, or \fI~/.config\fR if the XDG_CONFIG_HOME +environment variable is unset. + +.TP +.B \fB\-\-idle\fR[\fB=\fR\fIseconds\fR] +Don't exit after a successful poll; instead, keep the connection open +and issue IDLE commands (requires an IMAP server supporting RFC 2177) to +watch for updates in the mailbox. +Each IDLE is terminated after at most \fIseconds\fR (29 minutes by +default) to avoid being logged out for inactivity. + +.TP +.B \fB\-\-no\-delivery +Update the state file, but skip SMTP/LMTP delivery. This is mostly +useful for initializing the statefile when migrating to \fBPullIMAP\fR +from another equivalent program such as \fIgetmail\fR(1) or +\fIfetchmail\fR(1). + +.TP +.B \-q\fR, \fB\-\-quiet\fR +Try to be quiet. + +.TP +.B \-\-debug +Turn on debug mode. Debug messages are written to the error output. +Note that this include all IMAP traffic (except literals). Depending on +the chosen authentication mechanism, this might include authentication +credentials. + +.TP +.B \-h\fR, \fB\-\-help\fR +Output a brief help and exit. + +.TP +.B \-\-version +Show the version number and exit. + +.SH CONFIGURATION FILE + +Unless told otherwise by the \fB\-\-config=\fR\fIFILE\fR option, +\fBPullIMAP\fR reads its configuration from +\fI$XDG_CONFIG_HOME/pullimap\fR (or \fI~/.config/pullimap\fR if the +XDG_CONFIG_HOME environment variable is unset) as an INI file. +The syntax of the configuration file is a serie of +\fIOPTION\fR=\fIVALUE\fR lines organized under some \fI[SECTION]\fR; +lines starting with a \(oq#\(cq or \(oq;\(cq character are ignored as +comments. +Valid options are: + +.TP +.I statefile +State file to use to keep track of the \fImailbox\fR's UIDVALIDITY and +UIDNEXT values. +Relative paths start from \fI$XDG_DATA_HOME/pullimap\fR, or +\fI~/.local/share/pullimap\fR if the XDG_DATA_HOME environment variable +is unset. +(Default: \(lq\fISECTION\fR\)\(rq, where \fISECTION\fR is the section +name of the option.) + +.TP +.I mailbox +The IMAP mailbox to pull messages from. +Support for persistent message Unique Identifiers (UID) is required. +(Default: \(lqINBOX\)\(rq.) + +.TP +.I deliver\-method +\fR\fIprotocol\fR:\fI[address]\fI\fR:\fIport\fR where to deliver +messages. Both SMTP [RFC 5321] and LMTP [RFC 2030] are supported. +(Default: \(lqsmtp:[127.0.0.1]:25\)\(rq.) + +.TP +.I deliver\-ehlo +Hostname to use in EHLO or LHO commands. +(Default: \(lq\fIlocalhost.localdomain\fR\)\(rq.) + + +.TP +.I deliver\-rcpt +Message recpient. +(Default: the username associated with the effective uid of the +\fBpullimap\fR process.) + +.TP +.I purge\-after +Retention period (in days), after which messages are removed from the +IMAP server. (The value is at best 24h accurate due to IMAP SEARCH +criterion ignoring time and timezone.) + +.TP +.I type +One of \(lqimap\(rq, \(lqimaps\(rq or \(lqtunnel\(rq. +\fItype\fR=imap and \fItype\fR=imaps are respectively used for IMAP and +IMAP over SSL/TLS connections over a INET socket. +\fItype\fR=tunnel causes \fBPullIMAP\fR to open a pipe to a +\fIcommand\fR instead of a raw socket. +Note that specifying \fItype\fR=tunnel in the \(lq[remote]\(rq section +makes the default \fIdatabase\fR to be \(lqlocalhost.db\(rq. +(Default: \(lqimaps\(rq.) + +.TP +.I host +Server hostname, for \fItype\fR=imap and \fItype\fR=imaps. +(Default: \(lqlocalhost\(rq.) + +.TP +.I port +Server port. +(Default: \(lq143\(rq for \fItype\fR=imap, \(lq993\(rq for +\fItype\fR=imaps.) + +.TP +.I proxy +An optional SOCKS proxy to use for TCP connections to the IMAP server +(\fItype\fR=imap and \fItype\fR=imaps only), formatted as +\(lq\fIprotocol\fR://[\fIuser\fR:\fIpassword\fR@]\fIproxyhost\fR[:\fIproxyport\fR]\(rq. +If \fIproxyport\fR is omitted, it is assumed at port 1080. +Only SOCKSv5 is supported, in two flavors: \(lqsocks5://\(rq to resolve +\fIhostname\fR locally, and \(lqsocks5h://\(rq to let the proxy resolve +\fIhostname\fR. + +.TP +.I command +Command to use for \fItype\fR=tunnel. Must speak the IMAP4rev1 protocol +on its standard output, and understand it on its standard input. + +.TP +.I STARTTLS +Whether to use the \(lqSTARTTLS\(rq directive to upgrade to a secure +connection. Setting this to \(lqYES\(rq for a server not advertising +the \(lqSTARTTLS\(rq capability causes \fBPullIMAP\fR to immediately +abort the connection. +(Ignored for \fItype\fRs other than \(lqimap\(rq. Default: \(lqYES\(rq.) + +.TP +.I auth +Space\-separated list of preferred authentication mechanisms. +\fBPullIMAP\fR uses the first mechanism in that list that is also +advertised (prefixed with \(lqAUTH=\(rq) in the server's capability list. +Supported authentication mechanisms are \(lqPLAIN\(rq and \(lqLOGIN\(rq. +(Default: \(lqPLAIN LOGIN\(rq.) + +.TP +.I username\fR, \fIpassword\fR +Username and password to authenticate with. Can be required for non +pre\-authenticated connections, depending on the chosen authentication +mechanism. + +.TP +.I compress +Whether to use the IMAP COMPRESS extension [RFC4978] for servers +advertizing it. +(Default: \(lqYES\(rq.) + +.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.) + +.TP +.I SSL_protocols +A space-separated list of SSL protocols to enable or disable (if +prefixed with an exclamation mark \(oq!\(cq). Known protocols are +\(lqSSLv2\(rq, \(lqSSLv3\(rq, \(lqTLSv1\(rq, \(lqTLSv1.1\(rq, and +\(lqTLSv1.2\(rq. Enabling a protocol is a short-hand for disabling all +other protocols. +(Default: \(lq!SSLv2 !SSLv3\(rq, i.e., only enable TLSv1 and above.) + +.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 \fBPullIMAP\fR to abort the connection during 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 AUTHOR +Written by Guilhem Moulin +.MT guilhem@fripost.org +.ME . diff --git a/pullimap.sample b/pullimap.sample new file mode 100644 index 0000000..63ff9de --- /dev/null +++ b/pullimap.sample @@ -0,0 +1,32 @@ +mailbox = INBOX +deliver-method = smtp:[127.0.0.1]:25 +#deliver-method = smtp:[127.0.0.1]:10024 +purge-after = 90 + +# SSL options +SSL_CApath = /etc/ssl/certs +#SSL_verify = YES +#SSL_protocols = !SSLv2 !SSLv3 !TLSv1 !TLSv1.1 +#SSL_cipherlist = EECDH+AESGCM:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL + +[private] +#type = imaps +host = imap.guilhem.org +#port = 993 +#proxy = socks5h://localhost:9050 +username = guilhem +password = xxxxxxxxxxxxxxxx +#compress = YES +#SSL_fingerprint = sha256$62E436BB329C46A628314C49BDA7C2A2E86C57B2021B9A964B8FABB6540D3605 + +[work] +#type = imaps +host = imap.example.com +#port = 993 +#proxy = socks5h://localhost:9050 +username = guilhem +password = xxxxxxxxxxxxxxxx +#compress = YES +#SSL_fingerprint = sha256$c93677ac6a4ac7d0a2b412c1bfdd83b9191c853aa8685bf5440f154e647caacf + +# vim:ft=dosini |