]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/NetReader.pm
get rid of unnecessary bytes::length usage
[public-inbox.git] / lib / PublicInbox / NetReader.pm
index 64910fe193eeb87275ea6f32c341077d44bf0e43..23445e7a49f2200834ae5571464c97d6c00b560d 100644 (file)
@@ -58,12 +58,10 @@ sub auth_anon_cb { '' }; # for Mail::IMAPClient::Authcallback
 
 # mic_for may prompt the user and store auth info, prepares mic_get
 sub mic_for ($$$$) { # mic = Mail::IMAPClient
-       my ($self, $url, $mic_args, $lei) = @_;
-       require PublicInbox::URIimap;
-       my $uri = PublicInbox::URIimap->new($url);
+       my ($self, $uri, $mic_args, $lei) = @_;
        require PublicInbox::GitCredential;
        my $cred = bless {
-               url => $url,
+               url => "$uri",
                protocol => $uri->scheme,
                host => $uri->host,
                username => $uri->user,
@@ -83,13 +81,13 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient
        };
        require PublicInbox::IMAPClient;
        my $mic = mic_new($self, $mic_arg, $sec, $uri) or
-                       die "E: <$url> new: $@\n";
+                       die "E: <$uri> new: $@\n";
        # default to using STARTTLS if it's available, but allow
        # it to be disabled since I usually connect to localhost
        if (!$mic_arg->{Ssl} && !defined($mic_arg->{Starttls}) &&
                        $mic->has_capability('STARTTLS') &&
                        $mic->can('starttls')) {
-               $mic->starttls or die "E: <$url> STARTTLS: $@\n";
+               $mic->starttls or die "E: <$uri> STARTTLS: $@\n";
        }
 
        # do we even need credentials?
@@ -111,8 +109,13 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient
        if ($mic->login && $mic->IsAuthenticated) {
                # success! keep IMAPClient->new arg in case we get disconnected
                $self->{mic_arg}->{$sec} = $mic_arg;
+               if ($cred) {
+                       $uri->user($cred->{username}) if !defined($uri->user);
+               } elsif ($mic_arg->{Authmechanism} eq 'ANONYMOUS') {
+                       $uri->auth('ANONYMOUS') if !defined($uri->auth);
+               }
        } else {
-               $err = "E: <$url> LOGIN: $@\n";
+               $err = "E: <$uri> LOGIN: $@\n";
                if ($cred && defined($cred->{password})) {
                        $err =~ s/\Q$cred->{password}\E/*******/g;
                }
@@ -233,18 +236,19 @@ W: see https://rt.cpan.org/Ticket/Display.html?id=129967 for updates
 }
 
 sub imap_uri {
-       my ($url) = @_;
+       my ($url, $ls_ok) = @_;
        require PublicInbox::URIimap;
        my $uri = PublicInbox::URIimap->new($url);
-       $uri ? $uri->canonical : undef;
+       $uri && ($ls_ok || $uri->mailbox) ? $uri->canonical : undef;
 }
 
 my %IS_NNTP = (news => 1, snews => 1, nntp => 1, nntps => 1);
 sub nntp_uri {
-       my ($url) = @_;
+       my ($url, $ls_ok) = @_;
        require PublicInbox::URInntps;
        my $uri = PublicInbox::URInntps->new($url);
-       $uri && $IS_NNTP{$uri->scheme} && $uri->group ? $uri->canonical : undef;
+       $uri && $IS_NNTP{$uri->scheme} && ($ls_ok || $uri->group) ?
+               $uri->canonical : undef;
 }
 
 sub cfg_intvl ($$$) {
@@ -304,15 +308,16 @@ sub imap_common_init ($;$) {
        # make sure we can connect and cache the credentials in memory
        $self->{mic_arg} = {}; # schema://authority => IMAPClient->new args
        my $mics = {}; # schema://authority => IMAPClient obj
-       for my $uri (@{$self->{imap_order}}) {
-               my $sec = uri_section($uri);
+       for my $orig_uri (@{$self->{imap_order}}) {
+               my $sec = uri_section($orig_uri);
+               my $uri = PublicInbox::URIimap->new("$sec/");
                my $mic = $mics->{$sec} //=
-                               mic_for($self, "$sec/", $mic_args, $lei) //
+                               mic_for($self, $uri, $mic_args, $lei) //
                                die "Unable to continue\n";
                next unless $self->isa('PublicInbox::NetWriter');
-               my $dst = $uri->mailbox // next;
+               my $dst = $orig_uri->mailbox // next;
                next if $mic->exists($dst); # already exists
-               $mic->create($dst) or die "CREATE $dst failed <$uri>: $@";
+               $mic->create($dst) or die "CREATE $dst failed <$orig_uri>: $@";
        }
        $mics;
 }
@@ -363,11 +368,13 @@ sub nntp_common_init ($;$) {
 }
 
 sub add_url {
-       my ($self, $arg) = @_;
+       my ($self, $arg, $ls_ok) = @_;
        my $uri;
-       if ($uri = imap_uri($arg)) {
+       if ($uri = imap_uri($arg, $ls_ok)) {
+               $_[1] = $$uri; # canonicalized
                push @{$self->{imap_order}}, $uri;
-       } elsif ($uri = nntp_uri($arg)) {
+       } elsif ($uri = nntp_uri($arg, $ls_ok)) {
+               $_[1] = $$uri; # canonicalized
                push @{$self->{nntp_order}}, $uri;
        } else {
                push @{$self->{unsupported_url}}, $arg;
@@ -392,10 +399,8 @@ sub errors {
        undef;
 }
 
-sub _imap_do_msg ($$$$$) {
-       my ($self, $url, $uid, $raw, $flags) = @_;
-       # our target audience expects LF-only, save storage
-       $$raw =~ s/\r\n/\n/sg;
+sub flags2kw ($$$$) {
+       my ($self, $uri, $uid, $flags) = @_;
        my $kw = [];
        for my $f (split(/ /, $flags)) {
                if (my $k = $IMAPflags2kw{$f}) {
@@ -404,12 +409,21 @@ sub _imap_do_msg ($$$$$) {
                } elsif ($f eq "\\Deleted") { # not in JMAP
                        return;
                } elsif ($self->{verbose}) {
-                       warn "# unknown IMAP flag $f <$url/;UID=$uid>\n";
+                       warn "# unknown IMAP flag $f <$uri/;UID=$uid>\n";
                }
        }
-       @$kw = sort @$kw; # for all UI/UX purposes
+       @$kw = sort @$kw; # for LeiSearch->kw_changed and UI/UX purposes
+       $kw;
+}
+
+sub _imap_do_msg ($$$$$) {
+       my ($self, $uri, $uid, $raw, $flags) = @_;
+       # our target audience expects LF-only, save storage
+       $$raw =~ s/\r\n/\n/sg;
+       my $kw = defined($flags) ?
+               (flags2kw($self, $uri, $uid, $flags) // return) : undef;
        my ($eml_cb, @args) = @{$self->{eml_each}};
-       $eml_cb->($url, $uid, $kw, PublicInbox::Eml->new($raw), @args);
+       $eml_cb->($uri, $uid, $kw, PublicInbox::Eml->new($raw), @args);
 }
 
 sub run_commit_cb ($) {
@@ -419,12 +433,18 @@ sub run_commit_cb ($) {
        $cb->(@args);
 }
 
-sub _itrk_last ($$;$) {
-       my ($self, $uri, $r_uidval) = @_;
+sub itrk_last ($$;$$) {
+       my ($self, $uri, $r_uidval, $mic) = @_;
        return (undef, undef, $r_uidval) unless $self->{incremental};
        my ($itrk, $l_uid, $l_uidval);
        if (defined(my $lms = $self->{-lms_ro})) { # LeiMailSync or 0
                $uri->uidvalidity($r_uidval) if defined $r_uidval;
+               if ($mic) {
+                       my $auth = $mic->Authmechanism // '';
+                       $uri->auth($auth) if $auth eq 'ANONYMOUS';
+                       my $user = $mic->User;
+                       $uri->user($user) if defined($user);
+               }
                my $x;
                $l_uid = ($lms && ($x = $lms->location_stats($$uri))) ?
                                $x->{'uid.max'} : undef;
@@ -437,17 +457,58 @@ sub _itrk_last ($$;$) {
        ($itrk, $l_uid, $l_uidval //= $r_uidval);
 }
 
+# import flags of already-seen messages
+sub each_old_flags ($$$$) {
+       my ($self, $mic, $uri, $l_uid) = @_;
+       $l_uid ||= 1;
+       my $sec = uri_section($uri);
+       my $bs = ($self->{imap_opt}->{$sec}->{batch_size} // 1) * 10000;
+       my ($eml_cb, @args) = @{$self->{eml_each}};
+       $self->{quiet} or warn "# $uri syncing flags 1:$l_uid\n";
+       for (my $n = 1; $n <= $l_uid; $n += $bs) {
+               my $end = $n + $bs;
+               $end = $l_uid if $end > $l_uid;
+               my $r = $mic->fetch_hash("$n:$end", 'FLAGS');
+               if (!$r) {
+                       return if $!{EINTR} && $self->{quit};
+                       return "E: $uri UID FETCH $n:$end error: $!";
+               }
+               while (my ($uid, $per_uid) = each %$r) {
+                       my $kw = flags2kw($self, $uri, $uid, $per_uid->{FLAGS})
+                               // next;
+                       # LeiImport->input_net_cb
+                       $eml_cb->($uri, $uid, $kw, undef, @args);
+               }
+       }
+}
+
+# returns true if PERMANENTFLAGS indicates FLAGS of already imported
+# messages are meaningful
+sub perm_fl_ok ($) {
+       my ($perm_fl) = @_;
+       return if !defined($perm_fl);
+       for my $f (split(/[ \t]+/, $perm_fl)) {
+               return 1 if $IMAPflags2kw{$f};
+       }
+       undef;
+}
+
 sub _imap_fetch_all ($$$) {
        my ($self, $mic, $orig_uri) = @_;
        my $sec = uri_section($orig_uri);
        my $mbx = $orig_uri->mailbox;
        $mic->Clear(1); # trim results history
-       $mic->examine($mbx) or return "E: EXAMINE $mbx ($sec) failed: $!";
-       my ($r_uidval, $r_uidnext);
+
+       # we need to check for mailbox writability to see if we care about
+       # FLAGS from already-imported messages.
+       my $cmd = $self->{each_old} ? 'select' : 'examine';
+       $mic->$cmd($mbx) or return "E: \U$cmd\E $mbx ($sec) failed: $!";
+
+       my ($r_uidval, $r_uidnext, $perm_fl);
        for ($mic->Results) {
+               /^\* OK \[PERMANENTFLAGS \(([^\)]*)\)\].*/ and $perm_fl = $1;
                /^\* OK \[UIDVALIDITY ([0-9]+)\].*/ and $r_uidval = $1;
                /^\* OK \[UIDNEXT ([0-9]+)\].*/ and $r_uidnext = $1;
-               last if $r_uidval && $r_uidnext;
        }
        $r_uidval //= $mic->uidvalidity($mbx) //
                return "E: $orig_uri cannot get UIDVALIDITY";
@@ -459,7 +520,12 @@ E: $orig_uri UIDVALIDITY mismatch (got $r_uidval)
 EOF
 
        my $uri = $orig_uri->clone;
-       my ($itrk, $l_uid, $l_uidval) = _itrk_last($self, $uri, $r_uidval);
+       my $single_uid = $uri->uid;
+       my ($itrk, $l_uid, $l_uidval) = itrk_last($self, $uri, $r_uidval, $mic);
+       if (defined($single_uid)) {
+               $itrk = $l_uid = undef;
+               $uri->uid(undef); # for eml_cb
+       }
        return <<EOF if $l_uidval != $r_uidval;
 E: $uri UIDVALIDITY mismatch
 E: local=$l_uidval != remote=$r_uidval
@@ -471,6 +537,13 @@ EOF
 E: $uri local UID exceeds remote ($l_uid > $r_uid)
 E: $uri strangely, UIDVALIDLITY matches ($l_uidval)
 EOF
+       $mic->Uid(1); # the default, we hope
+       my $err;
+       my $use_fl = perm_fl_ok($perm_fl);
+       if (!defined($single_uid) && $self->{each_old} && $use_fl) {
+               $err = each_old_flags($self, $mic, $uri, $l_uid);
+               return $err if $err;
+       }
        return if $l_uid >= $r_uid; # nothing to do
        $l_uid ||= 1;
        my ($mod, $shard) = @{$self->{shard_info} // []};
@@ -478,18 +551,18 @@ EOF
                my $m = $mod ? " [(UID % $mod) == $shard]" : '';
                warn "# $uri fetching UID $l_uid:$r_uid$m\n";
        }
-       $mic->Uid(1); # the default, we hope
        my $bs = $self->{imap_opt}->{$sec}->{batch_size} // 1;
        my $req = $mic->imap4rev1 ? 'BODY.PEEK[]' : 'RFC822.PEEK';
        my $key = $req;
        $key =~ s/\.PEEK//;
        my ($uids, $batch);
-       my $err;
        do {
                # I wish "UID FETCH $START:*" could work, but:
                # 1) servers do not need to return results in any order
                # 2) Mail::IMAPClient doesn't offer a streaming API
-               unless ($uids = $mic->search("UID $l_uid:*")) {
+               if (defined $single_uid) {
+                       $uids = [ $single_uid ];
+               } elsif (!($uids = $mic->search("UID $l_uid:*"))) {
                        return if $!{EINTR} && $self->{quit};
                        return "E: $uri UID SEARCH $l_uid:* error: $!";
                }
@@ -522,8 +595,8 @@ EOF
                                # messages get deleted, so holes appear
                                my $per_uid = delete $r->{$uid} // next;
                                my $raw = delete($per_uid->{$key}) // next;
-                               _imap_do_msg($self, $$uri, $uid, \$raw,
-                                               $per_uid->{FLAGS});
+                               my $fl = $use_fl ? $per_uid->{FLAGS} : undef;
+                               _imap_do_msg($self, $uri, $uid, \$raw, $fl);
                                $last_uid = $uid;
                                last if $self->{quit};
                        }
@@ -531,7 +604,7 @@ EOF
                }
                run_commit_cb($self);
                $itrk->update_last($r_uidval, $last_uid) if $itrk;
-       } until ($err || $self->{quit});
+       } until ($err || $self->{quit} || defined($single_uid));
        $err;
 }
 
@@ -612,7 +685,7 @@ sub _nntp_fetch_all ($$$) {
        # IMAPTracker is also used for tracking NNTP, UID == article number
        # LIST.ACTIVE can get the equivalent of UIDVALIDITY, but that's
        # expensive.  So we assume newsgroups don't change:
-       my ($itrk, $l_art) = _itrk_last($self, $uri);
+       my ($itrk, $l_art) = itrk_last($self, $uri);
 
        # allow users to specify articles to refetch
        # cf. https://tools.ietf.org/id/draft-gilman-news-url-01.txt
@@ -679,4 +752,23 @@ sub nntp_each {
 
 sub new { bless {}, shift };
 
+# updates $uri with UIDVALIDITY
+sub mic_for_folder {
+       my ($self, $uri) = @_;
+       my $mic = $self->mic_get($uri) or die "E: not connected: $@";
+       my $m = $self->isa('PublicInbox::NetWriter') ? 'select' : 'examine';
+       $mic->$m($uri->mailbox) or return;
+       my $uidval;
+       for ($mic->Results) {
+               /^\* OK \[UIDVALIDITY ([0-9]+)\].*/ or next;
+               $uidval = $1;
+               last;
+       }
+       $uidval //= $mic->uidvalidity($uri->mailbox) or
+               die "E: failed to get uidvalidity from <$uri>: $@";
+       $uri->uidvalidity($uidval);
+       $mic;
+}
+
+
 1;