]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/LeiMailSync.pm
lei_mail_sync: show non-matching SHA
[public-inbox.git] / lib / PublicInbox / LeiMailSync.pm
index f185b585f5e240b02cda3e01f69340741220f50b..e70cb5de2b6b5046736ba72feeeb51a05a6bdf7b 100644 (file)
@@ -47,7 +47,8 @@ sub lms_write_prepare { ($_[0]->{dbh} //= dbh_new($_[0], 1)); $_[0] }
 sub lms_pause {
        my ($self) = @_;
        $self->{fmap} = {};
-       delete $self->{dbh};
+       my $dbh = delete $self->{dbh};
+       eval { $dbh->do('PRAGMA optimize') } if $dbh;
 }
 
 sub create_tables {
@@ -66,6 +67,7 @@ CREATE TABLE IF NOT EXISTS blob2num (
        oidbin VARBINARY NOT NULL,
        fid INTEGER NOT NULL, /* folder ID */
        uid INTEGER NOT NULL, /* NNTP article number, IMAP UID, MH number */
+       /* not UNIQUE(fid, uid), since we may have broken servers */
        UNIQUE (oidbin, fid, uid)
 )
 
@@ -78,6 +80,7 @@ CREATE TABLE IF NOT EXISTS blob2name (
        oidbin VARBINARY NOT NULL,
        fid INTEGER NOT NULL, /* folder ID */
        name VARBINARY NOT NULL, /* Maildir basename, JMAP blobId */
+       /* not UNIQUE(fid, name), since we may have broken software */
        UNIQUE (oidbin, fid, name)
 )
 
@@ -195,9 +198,12 @@ INSERT OR IGNORE INTO blob2name (oidbin, fid, name) VALUES (?, ?, ?)
 sub each_src {
        my ($self, $folder, $cb, @args) = @_;
        my $dbh = $self->{dbh} //= dbh_new($self);
-       my $fid;
+       my ($fid, @rng);
+       my $and_ge_le = '';
        if (ref($folder) eq 'HASH') {
                $fid = $folder->{fid} // die "BUG: no `fid'";
+               @rng = grep(defined, @$folder{qw(min max)});
+               $and_ge_le = 'AND uid >= ? AND uid <= ?' if @rng;
        } else {
                $fid = $self->{fmap}->{$folder} //=
                        fid_for($self, $folder) // return;
@@ -206,16 +212,17 @@ sub each_src {
        # minimize implicit txn time to avoid blocking writers by
        # batching SELECTs.  This looks wonky but is necessary since
        # $cb-> may access the DB on its own.
-       my $ary = $dbh->selectall_arrayref(<<'', undef, $fid);
-SELECT _rowid_,oidbin,uid FROM blob2num WHERE fid = ?
+       my $ary = $dbh->selectall_arrayref(<<"", undef, $fid, @rng);
+SELECT _rowid_,oidbin,uid FROM blob2num WHERE fid = ? $and_ge_le
 ORDER BY _rowid_ ASC LIMIT 1000
 
        my $min = @$ary ? $ary->[-1]->[0] : undef;
        while (defined $min) {
                for my $row (@$ary) { $cb->($row->[1], $row->[2], @args) }
 
-               $ary = $dbh->selectall_arrayref(<<'', undef, $fid, $min);
-SELECT _rowid_,oidbin,uid FROM blob2num WHERE fid = ? AND _rowid_ > ?
+               $ary = $dbh->selectall_arrayref(<<"", undef, $fid, @rng, $min);
+SELECT _rowid_,oidbin,uid FROM blob2num
+WHERE fid = ? $and_ge_le AND _rowid_ > ?
 ORDER BY _rowid_ ASC LIMIT 1000
 
                $min = @$ary ? $ary->[-1]->[0] : undef;
@@ -247,12 +254,14 @@ sub location_stats {
 SELECT COUNT(name) FROM blob2name WHERE fid = ?
 
        $ret->{'name.count'} = $row if $row;
+       my $ntype = ($folder =~ m!\A(?:nntps?|s?news)://!i) ? 'article' :
+               (($folder =~ m!\Aimaps?://!i) ? 'uid' : "TODO<$folder>");
        for my $op (qw(count min max)) {
                ($row) = $dbh->selectrow_array(<<"", undef, $fid);
 SELECT $op(uid) FROM blob2num WHERE fid = ?
 
                $row or last;
-               $ret->{"uid.$op"} = $row;
+               $ret->{"$ntype.$op"} = $row;
        }
        $ret;
 }
@@ -291,16 +300,19 @@ sub locations_for {
 
 # returns a list of folders used for completion
 sub folders {
-       my ($self, $pfx) = @_;
-       my $dbh = $self->{dbh} //= dbh_new($self);
+       my ($self, @pfx) = @_;
        my $sql = 'SELECT loc FROM folders';
-       my @pfx;
-       if (defined $pfx) {
+       if (defined($pfx[0])) {
                $sql .= ' WHERE loc LIKE ? ESCAPE ?';
-               @pfx = ($pfx, '\\');
+               my $anywhere = !!$pfx[1];
+               $pfx[1] = '\\';
                $pfx[0] =~ s/([%_\\])/\\$1/g; # glob chars
                $pfx[0] .= '%';
+               substr($pfx[0], 0, 0, '%') if $anywhere;
+       } else {
+               @pfx = (); # [0] may've been undef
        }
+       my $dbh = $self->{dbh} //= dbh_new($self);
        map { $_->[0] } @{$dbh->selectall_arrayref($sql, undef, @pfx)};
 }
 
@@ -327,9 +339,12 @@ WHERE b.oidbin = ?
                        next unless -s $fh;
                        local $/;
                        my $raw = <$fh>;
-                       if ($vrfy && git_sha(1, \$raw)->hexdigest ne $oidhex) {
-                               warn "$f changed $oidhex\n";
-                               next;
+                       if ($vrfy) {
+                               my $got = git_sha(1, \$raw)->hexdigest;
+                               if ($got ne $oidhex) {
+                                       warn "$f changed $oidhex => $got\n";
+                                       next;
+                               }
                        }
                        return \$raw;
                }
@@ -369,6 +384,30 @@ sub match_imap_url {
                        "E: `$url' is ambiguous:\n\t".join("\n\t", @match)."\n";
 }
 
+sub match_nntp_url ($$$) {
+       my ($self, $url, $all) = @_; # $all = [ $lms->folders ];
+       $all //= [ $self->folders ];
+       require PublicInbox::URInntps;
+       my $want = PublicInbox::URInntps->new($url)->canonical;
+       my ($s, $h, $p) = ($want->scheme, $want->host, $want->port);
+       my $ng = $want->group; # force scalar (no article ranges)
+       my @uri = map { PublicInbox::URInntps->new($_)->canonical }
+               grep(m!\A\Q$s\E://.*?\Q$h\E\b.*?/\Q$ng\E\b!, @$all);
+       my @match;
+       for my $x (@uri) {
+               next if $x->group ne $ng || $x->host ne $h || $x->port != $p;
+               # maybe user was forgotten on CLI:
+               if (defined($x->userinfo) && !defined($want->userinfo)) {
+                       push @match, $x;
+               } elsif (($x->userinfo//"\0") eq ($want->userinfo//"\0")) {
+                       push @match, $x;
+               }
+       }
+       return @match if wantarray;
+       scalar(@match) <= 1 ? $match[0] :
+                       "E: `$url' is ambiguous:\n\t".join("\n\t", @match)."\n";
+}
+
 # returns undef on failure, number on success
 sub group2folders {
        my ($self, $lei, $all, $folders) = @_;
@@ -409,7 +448,7 @@ sub arg2folder {
        my ($self, $lei, $folders) = @_;
        my @all = $self->folders;
        my %all = map { $_ => 1 } @all;
-       my ($err, @no);
+       my @no;
        for (@$folders) {
                next if $all{$_}; # ok
                if (m!\A(maildir|mh):(.+)!i) {
@@ -426,11 +465,23 @@ sub arg2folder {
                        my $res = match_imap_url($self, $orig, \@all);
                        if (ref $res) {
                                $_ = $$res;
-                               push(@{$err->{qerr}}, <<EOM);
+                               $lei->qerr(<<EOM);
+# using `$res' instead of `$orig'
+EOM
+                       } else {
+                               warn($res, "\n") if defined $res;
+                               push @no, $orig;
+                       }
+               } elsif (m!\A(?:nntps?|s?news)://!i) {
+                       my $orig = $_;
+                       my $res = match_nntp_url($self, $orig, \@all);
+                       if (ref $res) {
+                               $_ = $$res;
+                               $lei->qerr(<<EOM);
 # using `$res' instead of `$orig'
 EOM
                        } else {
-                               $lei->err($res) if defined $res;
+                               warn($res, "\n") if defined $res;
                                push @no, $orig;
                        }
                } else {
@@ -439,12 +490,11 @@ EOM
        }
        if (@no) {
                my $no = join("\n\t", @no);
-               $err->{fail} = <<EOF;
+               die <<EOF;
 No sync information for: $no
 Run `lei ls-mail-sync' to display valid choices
 EOF
        }
-       $err;
 }
 
 sub forget_folders {
@@ -484,14 +534,14 @@ EOM
        }
 }
 
-sub imap_oidbin ($$$) {
-       my ($self, $url, $uid) = @_; # $url MUST have UIDVALIDITY
-       my $fid = $self->{fmap}->{$url} //= fid_for($self, $url) // return;
+sub num_oidbin ($$$) {
+       my ($self, $url, $uid) = @_; # $url MUST have UIDVALIDITY if IMAP
+       my $fid = $self->{fmap}->{$url} //= fid_for($self, $url) // return ();
        my $sth = $self->{dbh}->prepare_cached(<<EOM, undef, 1);
-SELECT oidbin FROM blob2num WHERE fid = ? AND uid = ?
+SELECT oidbin FROM blob2num WHERE fid = ? AND uid = ? ORDER BY _rowid_
 EOM
        $sth->execute($fid, $uid);
-       $sth->fetchrow_array;
+       map { $_->[0] } @{$sth->fetchall_arrayref};
 }
 
 sub name_oidbin ($$$) {
@@ -501,23 +551,17 @@ sub name_oidbin ($$$) {
 SELECT oidbin FROM blob2name WHERE fid = ? AND name = ?
 EOM
        $sth->execute($fid, $nm);
-       $sth->fetchrow_array;
+       map { $_->[0] } @{$sth->fetchall_arrayref};
 }
 
-sub imap_oid {
+sub imap_oidhex {
        my ($self, $lei, $uid_uri) = @_;
        my $mailbox_uri = $uid_uri->clone;
        $mailbox_uri->uid(undef);
        my $folders = [ $$mailbox_uri ];
-       if (my $err = $self->arg2folder($lei, $folders)) {
-               if ($err->{fail}) {
-                       $lei->qerr("# no sync information for $mailbox_uri");
-                       return;
-               }
-               $lei->qerr(@{$err->{qerr}}) if $err->{qerr};
-       }
-       my $oidbin = imap_oidbin($self, $folders->[0], $uid_uri->uid);
-       $oidbin ? unpack('H*', $oidbin) : undef;
+       eval { $self->arg2folder($lei, $folders) };
+       $lei->qerr("# no sync information for $mailbox_uri") if $@;
+       map { unpack('H*',$_) } num_oidbin($self, $folders->[0], $uid_uri->uid)
 }
 
 1;