sub mic_new ($$$$) {
my ($self, $mic_arg, $sec, $uri) = @_;
- my %socks;
+ my %mic_arg = %$mic_arg;
my $sa = $self->{imap_opt}->{$sec}->{-proxy_cfg} || $self->{-proxy_cli};
if ($sa) {
my %opt = %$sa;
- $opt{ConnectAddr} = delete $mic_arg->{Server};
- $opt{ConnectPort} = delete $mic_arg->{Port};
- $socks{Socket} = IO::Socket::Socks->new(%opt) or die
+ $opt{ConnectAddr} = delete $mic_arg{Server};
+ $opt{ConnectPort} = delete $mic_arg{Port};
+ $mic_arg{Socket} = IO::Socket::Socks->new(%opt) or die
"E: <$$uri> ".eval('$IO::Socket::Socks::SOCKS_ERROR');
}
- PublicInbox::IMAPClient->new(%$mic_arg, %socks);
+ PublicInbox::IMAPClient->new(%mic_arg, Keepalive => 1);
}
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,
Port => $uri->port,
Server => $host,
Ssl => $uri->scheme eq 'imaps',
- Keepalive => 1, # SO_KEEPALIVE
%$common, # may set Starttls, Compress, Debug ....
};
+ $mic_arg->{Ssl} = 1 if $uri->scheme eq 'imaps';
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?
$cred = undef;
}
if ($cred) {
- $cred->check_netrc unless defined $cred->{password};
- $cred->fill($lei); # may prompt user here
+ my $p = $cred->{password} // $cred->check_netrc;
+ $cred->fill($lei) unless defined($p); # may prompt user here
$mic->User($mic_arg->{User} = $cred->{username});
$mic->Password($mic_arg->{Password} = $cred->{password});
} else { # AUTH=ANONYMOUS
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;
}
$mic = undef;
}
- $cred->run($mic ? 'approve' : 'reject') if $cred;
+ $cred->run($mic ? 'approve' : 'reject') if $cred && $cred->{filled};
if ($err) {
$lei ? $lei->fail($err) : warn($err);
}
}
sub nn_new ($$$) {
- my ($nn_arg, $nntp_opt, $uri) = @_;
+ my ($nn_arg, $nntp_cfg, $uri) = @_;
my $nn;
if (defined $nn_arg->{ProxyAddr}) {
require PublicInbox::NetNNTPSocks;
# default to using STARTTLS if it's available, but allow
# it to be disabled for localhost/VPN users
if (!$nn_arg->{SSL} && $nn->can('starttls')) {
- if (!defined($nntp_opt->{starttls}) &&
+ if (!defined($nntp_cfg->{starttls}) &&
try_starttls($nn_arg->{Host})) {
# soft fail by default
$nn->starttls or warn <<"";
W: <$uri> STARTTLS tried and failed (not requested)
- } elsif ($nntp_opt->{starttls}) {
+ } elsif ($nntp_cfg->{starttls}) {
# hard fail if explicitly configured
$nn->starttls or die <<"";
E: <$uri> STARTTLS requested and failed
}
- } elsif ($nntp_opt->{starttls}) {
+ } elsif ($nntp_cfg->{starttls}) {
$nn->can('starttls') or
die "E: <$uri> Net::NNTP too old for STARTTLS\n";
$nn->starttls or die <<"";
sub nn_for ($$$$) { # nn = Net::NNTP
my ($self, $uri, $nn_args, $lei) = @_;
my $sec = uri_section($uri);
- my $nntp_opt = $self->{nntp_opt}->{$sec} //= {};
+ my $nntp_cfg = $self->{cfg_opt}->{$sec} //= {};
my $host = $uri->host;
# Net::NNTP and Net::Netrc both mishandle `0', so we pass `127.0.0.1'
$host = '127.0.0.1' if $host eq '0';
}, 'PublicInbox::GitCredential';
($u, $p) = split(/:/, $ui, 2);
($cred->{username}, $cred->{password}) = ($u, $p);
- $cred->check_netrc unless defined $p;
+ $p //= $cred->check_netrc;
}
my $common = $nn_args->{$sec} // {};
my $nn_arg = {
Port => $uri->port,
Host => $host,
- SSL => $uri->secure, # snews == nntps
%$common, # may Debug ....
};
+ $nn_arg->{SSL} = 1 if $uri->secure; # snews == nntps
my $sa = $self->{-proxy_cli};
%$nn_arg = (%$nn_arg, %$sa) if $sa;
- my $nn = nn_new($nn_arg, $nntp_opt, $uri);
+ my $nn = nn_new($nn_arg, $nntp_cfg, $uri);
if ($cred) {
- $cred->fill($lei); # may prompt user here
+ $cred->fill($lei) unless defined($p); # may prompt user here
if ($nn->authinfo($u, $p)) {
- push @{$nntp_opt->{-postconn}}, [ 'authinfo', $u, $p ];
+ push @{$nntp_cfg->{-postconn}}, [ 'authinfo', $u, $p ];
} else {
warn "E: <$uri> AUTHINFO $u XXXX failed\n";
$nn = undef;
}
}
- if ($nntp_opt->{compress}) {
+ if ($nntp_cfg->{compress}) {
# https://rt.cpan.org/Ticket/Display.html?id=129967
if ($nn->can('compress')) {
if ($nn->compress) {
- push @{$nntp_opt->{-postconn}}, [ 'compress' ];
+ push @{$nntp_cfg->{-postconn}}, [ 'compress' ];
} else {
warn "W: <$uri> COMPRESS failed\n";
}
} else {
- delete $nntp_opt->{compress};
+ delete $nntp_cfg->{compress};
warn <<"";
W: <$uri> COMPRESS not supported by Net::NNTP
W: see https://rt.cpan.org/Ticket/Display.html?id=129967 for updates
}
$self->{nn_arg}->{$sec} = $nn_arg;
- $cred->run($nn ? 'approve' : 'reject') if $cred;
+ $cred->run($nn ? 'approve' : 'reject') if $cred && $cred->{filled};
$nn;
}
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 ($$$) {
# 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;
}
# Net::NNTP post-connect commands
for my $k (qw(starttls compress)) {
$v = cfg_bool($cfg, "nntp.$k", $$uri) // next;
- $self->{nntp_opt}->{$sec}->{$k} = $v;
+ $self->{cfg_opt}->{$sec}->{$k} = $v;
}
# -watch internal option
for my $k (qw(pollInterval)) {
$to = cfg_intvl($cfg, "nntp.$k", $$uri) // next;
- $self->{nntp_opt}->{$sec}->{$k} = $to;
+ $self->{cfg_opt}->{$sec}->{$k} = $to;
}
}
# make sure we can connect and cache the credentials in memory
}
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;
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}) {
} 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 ($) {
$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;
($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;
+}
+
+# may be overridden in NetWriter or Watch
+sub folder_select { $_[0]->{each_old} ? 'select' : 'examine' }
+
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->folder_select;
+ $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";
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
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} // []};
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: $!";
}
# 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};
}
}
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;
}
$nn = delete($cached->{$sec}) and return $nn;
my $nn_arg = $self->{nn_arg}->{$sec} or
die "BUG: no Net::NNTP->new arg for $sec";
- my $nntp_opt = $self->{nntp_opt}->{$sec};
- $nn = nn_new($nn_arg, $nntp_opt, $uri) or return;
- if (my $postconn = $nntp_opt->{-postconn}) {
+ my $nntp_cfg = $self->{cfg_opt}->{$sec};
+ $nn = nn_new($nn_arg, $nntp_cfg, $uri) or return;
+ if (my $postconn = $nntp_cfg->{-postconn}) {
for my $m_arg (@$postconn) {
my ($method, @args) = @$m_arg;
$nn->$method(@args) and next;
# 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
warn "# $uri fetching ARTICLE $beg..$end\n";
}
my $n = $self->{max_batch};
- my $url = $$uri;
for ($beg..$end) {
last if $self->{quit};
$art = $_;
$raw = join('', @$raw);
$raw =~ s/\r\n/\n/sg;
my ($eml_cb, @args) = @{$self->{eml_each}};
- $eml_cb->($url, $art, $kw, PublicInbox::Eml->new(\$raw), @args);
+ $eml_cb->($uri, $art, $kw, PublicInbox::Eml->new(\$raw), @args);
$last_art = $art;
}
run_commit_cb($self);
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;