diff options
-rw-r--r-- | debian/patches/0001-Ignore-custom-lib-PATH.patch | 2 | ||||
-rwxr-xr-x | imapsync | 201 | ||||
-rw-r--r-- | imapsync.sample | 20 | ||||
-rw-r--r-- | imapsync.service | 9 | ||||
-rw-r--r-- | lib/Net/IMAP/Sync.pm | 123 |
5 files changed, 272 insertions, 83 deletions
diff --git a/debian/patches/0001-Ignore-custom-lib-PATH.patch b/debian/patches/0001-Ignore-custom-lib-PATH.patch index 0786ffa..8199295 100644 --- a/debian/patches/0001-Ignore-custom-lib-PATH.patch +++ b/debian/patches/0001-Ignore-custom-lib-PATH.patch @@ -12,8 +12,8 @@ index 4ef47b3..f932686 100755 --- a/imapsync +++ b/imapsync @@ -30,7 +30,6 @@ use List::Util 'first'; + use List::Util 'first'; use DBI (); - use POSIX 'strftime'; -use lib 'lib'; use Net::IMAP::Sync qw/read_config compact_set $IMAP_text $IMAP_cond/; @@ -28,7 +28,6 @@ use Getopt::Long qw/:config posix_default no_ignore_case gnu_compat use List::Util 'first'; use DBI (); -use POSIX 'strftime'; use lib 'lib'; use Net::IMAP::Sync qw/read_config compact_set $IMAP_text $IMAP_cond/; @@ -43,14 +42,33 @@ sub usage(;$) { print STDERR "TODO $NAME usage\n"; exit $rv; } -usage(1) unless GetOptions(\%CONFIG, qw/debug help|h quiet|q oneshot|1/); +usage(1) unless GetOptions(\%CONFIG, qw/debug help|h config=s quiet|q oneshot|1 check/); usage(0) if $CONFIG{help}; -my $CONFFILE = 'sync.ini'; -my $CACHEDIR = './imapsync.cache'; # XXX use a config option -my $DBFILE = "$CACHEDIR/imap.guilhem.org.db"; -my $LOCKFILE = "$CACHEDIR/.imap.guilhem.org.lck"; +my $CONF = read_config( delete $CONFIG{config} // $NAME + , [qw/_ local remote/] + , database => qr/\A(\P{Control}+)\z/ ); +my ($DBFILE, $LOCKFILE); + +{ + $DBFILE = $CONF->{_}->{database} if defined $CONF->{_}; + $DBFILE //= $CONF->{remote}->{host}.'.db' if defined $CONF->{remote}; + $DBFILE //= $CONF->{local}->{host}. '.db' if defined $CONF->{local}; + die "Missing option database" unless defined $DBFILE; + + unless ($DBFILE =~ /\A\//) { + my $dir = ($ENV{XDG_DATA_HOME} // "$ENV{HOME}/.local/share") .'/'. $NAME; + $dir =~ /\A(\/\p{Print}+)\z/ or die "Insecure $dir"; + $dir = $1; + $DBFILE = $dir .'/'. $DBFILE; + unless (-d $dir) { + mkdir $dir, 0700 or die "Cannot mkdir $dir: $!\n"; + } + } + + $LOCKFILE = $DBFILE =~ s/([^\/]+)\z/.$1.lck/r; +} my ($DBH, $IMAP); @@ -67,10 +85,7 @@ $SIG{$_} = sub { clean(); die "$!\n"; } foreach qw/INT TERM/; ############################################################################# # Lock the database { - if (!-d $CACHEDIR) { - mkdir $CACHEDIR, 0700 or die "Cannot mkdir $CACHEDIR: $!\n"; - } - elsif (-f $LOCKFILE) { + if (-f $LOCKFILE) { open my $lock, '<', $LOCKFILE or die "Cannot open $LOCKFILE: $!\n"; my $pid = <$lock>; close $lock; @@ -153,9 +168,7 @@ $DBH->do('PRAGMA foreign_keys = ON'); sub msg($@) { my $name = shift; return unless @_; - my $prefix = strftime "%b %e %H:%M:%S", localtime; - $prefix .= " $name" if defined $name; - $prefix .= ': '; + my $prefix = defined $name ? "$name: " : ''; print STDERR $prefix, @_, "\n"; } @@ -164,7 +177,7 @@ sub msg($@) { # Connect to the local and remote IMAP servers foreach my $name (qw/local remote/) { - my %config = Net::IMAP::Sync::read_config($CONFFILE, $name); + my %config = %{$CONF->{$name}}; $config{$_} = $CONFIG{$_} foreach keys %CONFIG; $config{enable} = 'QRESYNC'; $config{name} = $name; @@ -502,18 +515,25 @@ my $STH_GET_CACHE = $DBH->prepare(q{ r.UIDVALIDITY as rUIDVALIDITY, r.UIDNEXT as rUIDNEXT, r.HIGHESTMODSEQ as rHIGHESTMODSEQ FROM mailboxes m JOIN local l ON m.idx = l.idx JOIN remote r ON m.idx = r.idx }); +my $STH_GET_CACHE_BY_IDX = $DBH->prepare(q{ + SELECT mailbox, + l.UIDVALIDITY as lUIDVALIDITY, l.UIDNEXT as lUIDNEXT, l.HIGHESTMODSEQ as lHIGHESTMODSEQ, + r.UIDVALIDITY as rUIDVALIDITY, r.UIDNEXT as rUIDNEXT, r.HIGHESTMODSEQ as rHIGHESTMODSEQ + FROM mailboxes m JOIN local l ON m.idx = l.idx JOIN remote r ON m.idx = r.idx + WHERE m.idx = ? +}); # Get the index associated with a mailbox. my $STH_GET_INDEX = $DBH->prepare(q{SELECT idx FROM mailboxes WHERE mailbox = ?}); # Find local/remote UID from the map. -my $STH_GET_LOCAL_UID = $DBH->prepare("SELECT lUID FROM mapping WHERE idx = ? and rUID = ?"); -my $STH_GET_REMOTE_UID = $DBH->prepare("SELECT rUID FROM mapping WHERE idx = ? and lUID = ?"); +my $STH_GET_LOCAL_UID = $DBH->prepare(q{SELECT lUID FROM mapping WHERE idx = ? and rUID = ?}); +my $STH_GET_REMOTE_UID = $DBH->prepare(q{SELECT rUID FROM mapping WHERE idx = ? and lUID = ?}); # Delete a (idx,lUID,rUID) association. # /!\ Don't commit before the messages have actually been EXPUNGEd on # both sides! -my $STH_DELETE_MAPPING = $DBH->prepare("DELETE FROM mapping WHERE idx = ? and lUID = ?"); +my $STH_DELETE_MAPPING = $DBH->prepare(q{DELETE FROM mapping WHERE idx = ? and lUID = ?}); # Update the HIGHESTMODSEQ. my $STH_UPDATE_LOCAL_HIGHESTMODSEQ = $DBH->prepare(q{UPDATE local SET HIGHESTMODSEQ = ? WHERE idx = ?}); @@ -528,8 +548,9 @@ my $STH_NEWMAILBOX = $DBH->prepare(q{INSERT INTO mailboxes (mailbox,subscribed) my $STH_INSERT_LOCAL = $DBH->prepare(q{INSERT INTO local (idx,UIDVALIDITY,UIDNEXT,HIGHESTMODSEQ) VALUES (?,?,?,?)}); my $STH_INSERT_REMOTE = $DBH->prepare(q{INSERT INTO remote (idx,UIDVALIDITY,UIDNEXT,HIGHESTMODSEQ) VALUES (?,?,?,?)}); -# Insert a (idx,lUID,rUID) association. -my $STH_INSERT_MAPPING = $DBH->prepare("INSERT INTO mapping (idx,lUID,rUID) VALUES (?,?,?)"); +# Insert or retrieve a (idx,lUID,rUID) association. +my $STH_INSERT_MAPPING = $DBH->prepare(q{INSERT INTO mapping (idx,lUID,rUID) VALUES (?,?,?)}); +my $STH_GET_MAPPING = $DBH->prepare(q{SELECT lUID,rUID FROM mapping WHERE idx = ?}); # Initialize $lIMAP and $rIMAP states to detect mailbox dirtyness. @@ -538,31 +559,136 @@ while (defined (my $row = $STH_GET_CACHE->fetchrow_hashref())) { $lIMAP->set_cache($row->{mailbox}, UIDVALIDITY => $row->{lUIDVALIDITY}, UIDNEXT => $row->{lUIDNEXT}, - HIGHESTMODSEQ => $row->{lHIGHESTMODSEQ} + HIGHESTMODSEQ => ($CONFIG{check} ? 0 : $row->{lHIGHESTMODSEQ}) ); $rIMAP->set_cache($row->{mailbox}, UIDVALIDITY => $row->{rUIDVALIDITY}, UIDNEXT => $row->{rUIDNEXT}, - HIGHESTMODSEQ => $row->{rHIGHESTMODSEQ} + HIGHESTMODSEQ => ($CONFIG{check} ? 0 : $row->{rHIGHESTMODSEQ}) ); } +# Download some missing UIDs. +sub fix_missing($$$@) { + my $idx = shift; + my $mailbox = shift; + my $name = shift; + my @set = @_; + + my $source = $name eq 'local' ? $lIMAP : $rIMAP; + my $target = $name eq 'local' ? $rIMAP : $lIMAP; + + my $attrs = join ' ', qw/MODSEQ FLAGS INTERNALDATE ENVELOPE BODY.PEEK[]/; + $source->fetch(compact_set(@set), "($attrs)", sub(%) { + my %mail = @_; + return unless exists $mail{RFC822}; # not for us + + my $from = first { defined $_ and @$_ } @{$mail{ENVELOPE}}[2,3,4]; + $from = (defined $from and @$from) ? $from->[0]->[2].'@'.$from->[0]->[3] : ''; + print STDERR "$name($mailbox): UID $mail{UID} from <$from> ($mail{INTERNALDATE})\n" unless $CONFIG{quiet}; + + # don't bother checking for MULTIAPPEND, @set is probably rather small + my @mail = ($mail{RFC822}, [ grep {lc $_ ne '\recent'} @{$mail{FLAGS}} ], $mail{INTERNALDATE}); + my ($uid) = $target->append($mailbox, @mail); + + my ($lUID, $rUID) = $name eq 'local' ? ($mail{UID}, $uid) : ($uid, $mail{UID}); + print STDERR "$name($mailbox): Adding mapping (lUID,rUID) = ($lUID,$rUID)\n"; + $STH_INSERT_MAPPING->execute($idx, $lUID, $rUID); + }); +} + +# Check synchronization of a mailbox between the two servers (in a very crude way) +my @CHECKED; +sub check($$$$$) { + my ($idx, $lVanished, $lList, $rVanished, $rList) = @_; + + my %lVanished = map {$_ => 1} @$lVanished; + my %rVanished = map {$_ => 1} @$rVanished; + + $STH_GET_CACHE_BY_IDX->execute($idx); + my $cache = $STH_GET_CACHE_BY_IDX->fetchrow_hashref() // die "Missing cache for index $idx"; + die if defined $STH_GET_CACHE_BY_IDX->fetch(); # sanity check + my $mailbox = $cache->{mailbox}; + + $STH_GET_MAPPING->execute($idx); + my %missing = ( local => [], remote => [] ); + while (defined (my $row = $STH_GET_MAPPING->fetch())) { + my ($lUID, $rUID) = @$row; + if (defined $lList->{$lUID} and defined $rList->{$rUID}) { + # both $lUID and $rUID are known + if ($lList->{$lUID}->[0] <= $cache->{lHIGHESTMODSEQ} and + $rList->{$rUID}->[0] <= $cache->{rHIGHESTMODSEQ}) { + # old stuff + if ($lList->{$lUID}->[1] ne $rList->{$rUID}->[1]) { + warn "WARNING: Missed flag update in $mailbox for (lUID,rUID) = ($lUID,$rUID). Fixing...\n"; + # keep it in the hash references so we fix it automatically + } + else { + # no conflict, remove it from the hashes + delete $lList->{$lUID}; + delete $rList->{$rUID}; + } + } + else { + # delete the old stuff + delete $lList->{$lUID} if $lList->{$lUID}->[0] <= $cache->{lHIGHESTMODSEQ}; + delete $rList->{$rUID} if $rList->{$rUID}->[0] <= $cache->{rHIGHESTMODSEQ}; + } + } + elsif (!defined $lList->{$lUID} and !defined $rList->{$rUID}) { + unless ($lVanished{$lUID} and $rVanished{$rUID}) { + # will be deleted from the database later + warn "WARNING: Pair (lUID,rUID) = ($lUID,$rUID) vanished from $mailbox\n"; + $lVanished{$lUID} = 1; + $rVanished{$rUID} = 1; + } + } + elsif (!defined $lList->{$lUID}) { + unless ($lVanished{$lUID}) { + warn "WARNING: local($mailbox): No match for remote UID $rUID. Downloading again...\n"; + push @{$missing{remote}}, $rUID; + delete $rList->{$rUID}; + } + } + elsif (!defined $rList->{$rUID}) { + unless ($rVanished{$rUID}) { + warn "WARNING: remote($mailbox): No match for local UID $lUID. Downloading again...\n"; + push @{$missing{local}}, $lUID; + delete $lList->{$lUID}; + } + } + $lList->{$lUID} = $lList->{$lUID}->[1] if defined $lList->{$lUID}; + $rList->{$rUID} = $rList->{$rUID}->[1] if defined $rList->{$rUID}; + } + + # we'll complain later for modified UIDs without an entry in the database + + @$lVanished = keys %lVanished; + @$rVanished = keys %rVanished; + push @CHECKED, $idx; + return %missing; +} + # Sync known messages. Since pull_updates is the last method call on # $lIMAP and $rIMAP, it is safe to call get_cache on either object after # this function, in order to update the HIGHESTMODSEQ. # Return true if an update was detected, and false otherwise -sub sync_known_messages($) { - my $idx = shift; +sub sync_known_messages($$) { + my ($idx, $mailbox) = @_; my $update = 0; # loop since processing might produce VANISHED or unsollicited FETCH responses while (1) { - my ($lVanished, $lModified) = $lIMAP->pull_updates(); - my ($rVanished, $rModified) = $rIMAP->pull_updates(); + my ($lVanished, $lModified, $rVanished, $rModified, %missing); + + my $check = ($CONFIG{check} and !grep { $idx == $_} @CHECKED) ? 1 : 0; + ($lVanished, $lModified) = $lIMAP->pull_updates($check); + ($rVanished, $rModified) = $rIMAP->pull_updates($check); + %missing = check($idx, $lVanished, $lModified, $rVanished, $rModified) if $check; # repeat until we have nothing pending - return $update unless %$lModified or %$rModified or @$lVanished or @$rVanished; + return $update unless %$lModified or %$rModified or @$lVanished or @$rVanished or %missing; $update = 1; # process VANISHED messages @@ -581,7 +707,7 @@ sub sync_known_messages($) { my ($rUID) = $STH_GET_REMOTE_UID->fetchrow_array(); die if defined $STH_GET_REMOTE_UID->fetchrow_arrayref(); # sanity check if (!defined $rUID) { - warn "WARNING: Couldn't find a matching rUID for (idx,lUID) = ($idx,$lUID)\n"; + warn "WARNING: remote($mailbox): No match for local vanished UID $lUID. Ignoring...\n"; } elsif (!exists $rVanished{$rUID}) { push @rToRemove, $rUID; @@ -592,7 +718,7 @@ sub sync_known_messages($) { my ($lUID) = $STH_GET_LOCAL_UID->fetchrow_array(); die if defined $STH_GET_LOCAL_UID->fetchrow_arrayref(); # sanity check if (!defined $lUID) { - warn "WARNING: Couldn't find a matching lUID for (idx,rUID) = ($idx,$rUID)\n"; + warn "WARNING: local($mailbox): No match for remote vanished UID $rUID. Ignoring...\n"; } elsif (!exists $lVanished{$lUID}) { push @lToRemove, $lUID; @@ -606,7 +732,7 @@ sub sync_known_messages($) { foreach my $lUID (@$lVanished, @lToRemove) { my $r = $STH_DELETE_MAPPING->execute($idx, $lUID); die if $r > 1; # sanity check - warn "WARNING: Couldn't delete (idx,lUID) pair ($idx,$lUID)\n" if $r == 0; + warn "WARNING: Can't delete (idx,lUID) = ($idx,$lUID) from the database\n" if $r == 0; } } @@ -629,7 +755,8 @@ sub sync_known_messages($) { my ($rUID) = $STH_GET_REMOTE_UID->fetchrow_array(); die if defined $STH_GET_REMOTE_UID->fetchrow_arrayref(); # sanity check if (!defined $rUID) { - warn "WARNING: Couldn't find a matching rUID for (idx,lUID) = ($idx,$lUID)\n"; + warn "WARNING: remote($mailbox): No match for local updated UID $lUID. Downloading again...\n"; + push @{$missing{local}}, $lUID; } elsif (defined (my $rFlags = $rModified->{$rUID})) { unless ($lFlags eq $rFlags) { @@ -653,7 +780,8 @@ sub sync_known_messages($) { my ($lUID) = $STH_GET_LOCAL_UID->fetchrow_array(); die if defined $STH_GET_LOCAL_UID->fetchrow_arrayref(); # sanity check if (!defined $lUID) { - warn "WARNING: Couldn't find a matching rUID for (idx,rUID) = ($idx,$rUID)\n"; + warn "WARNING: local($mailbox): No match for remote updated UID $rUID. Downloading again...\n"; + push @{$missing{remote}}, $rUID; } elsif (!exists $lModified->{$lUID}) { # conflicts are taken care of above @@ -662,6 +790,9 @@ sub sync_known_messages($) { } } + fix_missing($idx, $mailbox, 'local', @{$missing{local}}) if @{$missing{local} // []}; + fix_missing($idx, $mailbox, 'remote', @{$missing{remote}}) if @{$missing{remote} // []}; + while (my ($lFlags,$lUIDs) = each %lToUpdate) { $lIMAP->push_flag_updates($lFlags, @$lUIDs); } @@ -734,13 +865,13 @@ sub sync_messages($$) { die if !defined $$idx or defined $STH_GET_INDEX->fetchrow_arrayref(); # sanity check # there might be flag updates pending - sync_known_messages($$idx); + sync_known_messages($$idx, $mailbox); $STH_INSERT_LOCAL->execute($$idx, $lIMAP->get_cache(qw/UIDVALIDITY UIDNEXT HIGHESTMODSEQ/)); $STH_INSERT_REMOTE->execute($$idx, $rIMAP->get_cache(qw/UIDVALIDITY UIDNEXT HIGHESTMODSEQ/)); } else { # update known mailbox - sync_known_messages($$idx); + sync_known_messages($$idx, $mailbox); $STH_UPDATE_LOCAL->execute($lIMAP->get_cache( qw/UIDNEXT HIGHESTMODSEQ/), $$idx); $STH_UPDATE_REMOTE->execute($rIMAP->get_cache(qw/UIDNEXT HIGHESTMODSEQ/), $$idx); } @@ -793,7 +924,7 @@ while(1) { $rIMAP->select($mailbox); # sync updates to known messages before fetching new messages - if (defined $idx and sync_known_messages($idx)) { + if (defined $idx and sync_known_messages($idx, $mailbox)) { # get_cache is safe after pull_update $STH_UPDATE_LOCAL_HIGHESTMODSEQ->execute( $lIMAP->get_cache('HIGHESTMODSEQ'), $idx); $STH_UPDATE_REMOTE_HIGHESTMODSEQ->execute($rIMAP->get_cache('HIGHESTMODSEQ'), $idx); @@ -803,7 +934,7 @@ while(1) { } } # clean state! - exit 0 if $CONFIG{oneshot}; + exit 0 if $CONFIG{oneshot} or $CONFIG{check}; wait_notifications(900); } diff --git a/imapsync.sample b/imapsync.sample new file mode 100644 index 0000000..51958aa --- /dev/null +++ b/imapsync.sample @@ -0,0 +1,20 @@ +; database = imap.guilhem.org.db + +[local] +type = preauth +command = /usr/lib/dovecot/imap + +[remote] +; type = imaps +host = imap.guilhem.org +; port = 993 +username = guilhem +password = xxxxxxxxxxxxxxxx + +; SSL options +;SSL_verify_peer = TRUE +SSL_ca_path = /etc/ssl/certs +;SSL_cipher_list = EECDH+AES:EDH+AES:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL:!SSLv2:!SSLv3:!TLSv1:!TLSv1.1 +;SSL_fingerprint = sha256$62E436BB329C46A628314C49BDA7C2A2E86C57B2021B9A964B8FABB6540D3605 + +; vim:ft=dosini diff --git a/imapsync.service b/imapsync.service new file mode 100644 index 0000000..e3a47e4 --- /dev/null +++ b/imapsync.service @@ -0,0 +1,9 @@ +[Unit] +Description=IMAP-to-IMAP Syncronization service +After=network.target + +[Service] +ExecStart=/usr/bin/imapsync + +[Install] +WantedBy=multi-user.target diff --git a/lib/Net/IMAP/Sync.pm b/lib/Net/IMAP/Sync.pm index 0b95276..bb99dcb 100644 --- a/lib/Net/IMAP/Sync.pm +++ b/lib/Net/IMAP/Sync.pm @@ -23,7 +23,6 @@ use strict; use Config::Tiny (); use List::Util 'first'; use Socket 'SO_KEEPALIVE'; -use POSIX 'strftime'; use Exporter 'import'; BEGIN { @@ -50,45 +49,55 @@ my %OPTIONS = ( SSL_ca_path => qr/\A(\P{Control}+)\z/, SSL_cipher_list => qr/\A(\P{Control}+)\z/, SSL_fingerprint => qr/\A([A-Za-z0-9]+\$\p{AHex}+)\z/, + SSL_verify_peer => qr/\A(TRUE|FALSE)\z/i, ); ############################################################################# # Utilities -# read_config($conffile, $section, %opts) -# Read $conffile's default section, then $section (which takes -# precedence). %opts extends %OPTIONS and maps each option to a -# regexp validating its values. +# read_config($conffile, $sections, %opts) +# Read $conffile's default section, then each section in the array +# reference $section (which takes precedence). %opts extends %OPTIONS +# and maps each option to a regexp validating its values. sub read_config($$%) { my $conffile = shift; - my $section = shift; + my $sections = shift; my %opts = (%OPTIONS, @_); + $conffile = ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") .'/'. $conffile + unless $conffile =~ /\A\//; # relative path + die "No such config file $conffile\n" unless defined $conffile and -f $conffile and -r $conffile; my $h = Config::Tiny::->read($conffile); - die "No such section $section\n" unless defined $h->{$section}; - - my $conf = $h->{_}; # default section - $conf->{$_} = $h->{$section}->{$_} foreach keys %{$h->{$section}}; - - # default values - $conf->{type} //= 'imaps'; - $conf->{host} //= 'localhost'; - $conf->{port} //= $conf->{type} eq 'imaps' ? 993 : $conf->{type} eq 'imap' ? 143 : undef; - $conf->{auth} //= 'PLAIN LOGIN'; - $conf->{STARTTLS} //= 'TRUE'; - - # untaint and validate the config - foreach my $k (keys %$conf) { - 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; + + my %configs; + foreach my $section (@$sections) { + my $conf = { %{$h->{_}} }; # default section + $configs{$section} = $conf; + next unless defined $section and $section ne '_'; + + die "No such section $section\n" unless defined $h->{$section}; + $conf->{$_} = $h->{$section}->{$_} foreach keys %{$h->{$section}}; + + # default values + $conf->{type} //= 'imaps'; + $conf->{host} //= 'localhost'; + $conf->{port} //= $conf->{type} eq 'imaps' ? 993 : $conf->{type} eq 'imap' ? 143 : undef; + $conf->{auth} //= 'PLAIN LOGIN'; + $conf->{STARTTLS} //= 'TRUE'; + + # untaint and validate the config + foreach my $k (keys %$conf) { + 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; + } } - return %$conf; + return \%configs; } @@ -215,6 +224,16 @@ sub new($%) { my $self = { @_ }; bless $self, $class; + # whether we're allowed to to use read-write command + $self->{'read-only'} = uc ($self->{'read-only'} // 'FALSE') ne 'TRUE' ? 0 : 1; + + # where to log + $self->{STDERR} //= \*STDERR; + + # the IMAP state: one of 'UNAUTH', 'AUTH', 'SELECTED' or 'LOGOUT' + # (cf RFC 3501 section 3) + $self->{_STATE} = ''; + if ($self->{type} eq 'preauth') { require 'IPC/Open2.pm'; my $command = $self->{command} // $self->fail("Missing preauth command"); @@ -232,9 +251,12 @@ sub new($%) { $socket = IO::Socket::INET->new(%args) or $self->fail("Cannot bind: $@"); } else { + require 'IO/Socket/SSL.pm'; + if (defined (my $vrfy = delete $self->{SSL_verify_peer})) { + $args{SSL_verify_mode} = 0 if uc $vrfy eq 'FALSE'; + } my $fpr = delete $self->{SSL_fingerprint}; $args{$_} = $self->{$_} foreach grep /^SSL_/, keys %$self; - require 'IO/Socket/SSL.pm'; $socket = IO::Socket::SSL->new(%args) or $self->fail("Failed connect or SSL handshake: $!\n$IO::Socket::SSL::SSL_ERROR"); @@ -275,16 +297,6 @@ sub new($%) { # are considered. $self->{_MODIFIED} = {}; - # whether we're allowed to to use read-write command - $self->{'read-only'} = uc ($self->{'read-only'} // 'FALSE') ne 'TRUE' ? 0 : 1; - - # where to log - $self->{STDERR} //= \*STDERR; - - # the IMAP state: one of 'UNAUTH', 'AUTH', 'SELECTED' or 'LOGOUT' - # (cf RFC 3501 section 3) - $self->{_STATE} = ''; - # wait for the greeting my $x = $self->_getline(); $x =~ s/\A\* (OK|PREAUTH) // or $self->panic($x); @@ -306,8 +318,12 @@ sub new($%) { require 'IO/Socket/SSL.pm'; $self->_send('STARTTLS'); + my %sslargs; + if (defined (my $vrfy = delete $self->{SSL_verify_peer})) { + $sslargs{SSL_verify_mode} = 0 if uc $vrfy eq 'FALSE'; + } my $fpr = delete $self->{SSL_fingerprint}; - my %sslargs = %$self{ grep /^SSL_/, keys %$self }; + $sslargs{$_} = $self->{$_} foreach grep /^SSL_/, keys %$self; IO::Socket::SSL->start_SSL($self->{STDIN}, %sslargs) or $self->fail("Failed SSL handshake: $!\n$IO::Socket::SSL::SSL_ERROR"); @@ -384,12 +400,10 @@ sub DESTROY($) { sub log($@) { my $self = shift; return unless @_; - my $prefix = strftime "%b %e %H:%M:%S", localtime; - $prefix .= " $self->{name}" if defined $self->{name}; + my $prefix = defined $self->{name} ? $self->{name} : ''; $prefix .= "($self->{_SELECTED})" if $self->{_STATE} eq 'SELECTED'; $prefix .= ': '; - my $stderr = $self->{STDERR}; - print $stderr $prefix, @_, "\n"; + $self->{STDERR}->say($prefix, @_); } @@ -665,6 +679,15 @@ sub append($$$@) { } +# $self->fetch($set, $flags, [$callback]) +# Issue an UID FETCH command with the given UID $set, $flags, and +# optional $callback. +sub fetch($$$$) { + my ($self, $set, $flags, $callback) = @_; + $self->_send("UID FETCH $set $flags", $callback); +} + + # $self->notify(@specifications) # Issue a NOTIFY command with the given mailbox @specifications (cf RFC # 5465 section 6) to be monitored. Croak if the server did not @@ -815,13 +838,16 @@ sub next_dirty_mailbox($@) { } -# $self->pull_updates() +# $self->pull_updates([$full]) +# If $full is set, FETCH FLAGS and MODSEQ for each UID up to +# UIDNEXT-1. # Get pending updates (unprocessed VANISHED responses and FLAG # updates), and empty these lists from the cache. # Finally, update the HIGHESTMODSEQ from the persistent cache to the # value found in the internal cache. -sub pull_updates($) { +sub pull_updates($;$) { my $self = shift; + my $full = shift // 0; my $mailbox = $self->{_SELECTED} // $self->panic(); my $pcache = $self->{_PCACHE}->{$mailbox}; @@ -831,6 +857,9 @@ sub pull_updates($) { $self->{_VANISHED} = []; } else { + $self->_send("UID FETCH 1:".($pcache->{UIDNEXT}-1)." (MODSEQ FLAGS)") + if $full and $pcache->{UIDNEXT} > 1; + my @missing; while (%{$self->{_MODIFIED}}) { while (my ($uid,$v) = each %{$self->{_MODIFIED}}) { @@ -838,9 +867,9 @@ sub pull_updates($) { # FLAG updates can arrive while processing pull_new_messages # for instance if (defined $v->[1] and $v->[0] > 0) { # setting the MODSEQ to 0 forces a FETCH - next unless $uid < $pcache->{UIDNEXT} # out of bounds - and $v->[0] > $pcache->{HIGHESTMODSEQ}; # already seen - $modified{$uid} = $v->[1]; + next unless $uid < $pcache->{UIDNEXT} # out of bounds + and ($full or $v->[0] > $pcache->{HIGHESTMODSEQ}); # already seen + $modified{$uid} = $full ? $v : $v->[1]; } else { push @missing, $uid; } @@ -979,7 +1008,7 @@ sub push_flag_updates($$@) { } unless ($self->{quiet}) { - $self->log("Updated flags ($flags) for UID ".compact_set(@ok)); + $self->log("Updated flags ($flags) for UID ".compact_set(@ok)) if @ok; $self->log("Couldn't update flags ($flags) for UID ".compact_set(keys %failed).', '. "trying again later") if %failed; } |