use v5.10.1;
use parent qw(Exporter PublicInbox::IPC);
use PublicInbox::Eml;
+use PublicInbox::Config;
our %IMAPflags2kw = map {; "\\\u$_" => $_ } qw(seen answered flagged draft);
$IMAPflags2kw{'$Forwarded'} = 'forwarded'; # RFC 5550
$uri->scheme . '://' . $uri->authority;
}
+sub socks_args ($) {
+ my ($val) = @_;
+ return if ($val // '') eq '';
+ if ($val =~ m!\Asocks5h:// (?: \[ ([^\]]+) \] | ([^:/]+) )
+ (?::([0-9]+))?/*\z!ix) {
+ my ($h, $p) = ($1 // $2, $3 + 0);
+ $h = '127.0.0.1' if $h eq '0';
+ eval { require IO::Socket::Socks } or die <<EOM;
+IO::Socket::Socks missing for socks5h://$h:$p
+EOM
+ return { ProxyAddr => $h, ProxyPort => $p };
+ }
+ die "$val not understood (only socks5h:// is supported)\n";
+}
+
+sub mic_new ($$$$) {
+ my ($self, $mic_arg, $sec, $uri) = @_;
+ my %socks;
+ 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
+ "E: <$$uri> ".eval('$IO::Socket::Socks::SOCKS_ERROR');
+ }
+ PublicInbox::IMAPClient->new(%$mic_arg, %socks);
+}
+
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,
password => $uri->password,
}, 'PublicInbox::GitCredential';
- my $common = $mic_args->{uri_section($uri)} // {};
+ my $sec = uri_section($uri);
+ my $common = $mic_args->{$sec} // {};
# IMAPClient and Net::Netrc both mishandles `0', so we pass `127.0.0.1'
my $host = $cred->{host};
$host = '127.0.0.1' if $host eq '0';
%$common, # may set Starttls, Compress, Debug ....
};
require PublicInbox::IMAPClient;
- my $mic = PublicInbox::IMAPClient->new(%$mic_arg) or
- die "E: <$url> new: $@\n";
-
+ my $mic = mic_new($self, $mic_arg, $sec, $uri) or
+ 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?
my $err;
if ($mic->login && $mic->IsAuthenticated) {
# success! keep IMAPClient->new arg in case we get disconnected
- $self->{mic_arg}->{uri_section($uri)} = $mic_arg;
+ $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;
}
sub nn_new ($$$) {
my ($nn_arg, $nntp_opt, $uri) = @_;
- my $nn = Net::NNTP->new(%$nn_arg) or die "E: <$uri> new: $!\n";
+ my $nn;
+ if (defined $nn_arg->{ProxyAddr}) {
+ require PublicInbox::NetNNTPSocks;
+ eval { $nn = PublicInbox::NetNNTPSocks->new_socks(%$nn_arg) };
+ die "E: <$uri> $@\n" if $@;
+ } else {
+ $nn = Net::NNTP->new(%$nn_arg) or die "E: <$uri> new: $!\n";
+ }
# default to using STARTTLS if it's available, but allow
# it to be disabled for localhost/VPN users
SSL => $uri->secure, # snews == nntps
%$common, # may Debug ....
};
+ my $sa = $self->{-proxy_cli};
+ %$nn_arg = (%$nn_arg, %$sa) if $sa;
my $nn = nn_new($nn_arg, $nntp_opt, $uri);
if ($cred) {
$cred->fill($lei); # may prompt user here
}
my $to = cfg_intvl($cfg, 'imap.timeout', $$uri);
$mic_args->{$sec}->{Timeout} = $to if $to;
+ my $sa = socks_args($cfg->urlmatch('imap.Proxy', $$uri));
+ $self->{imap_opt}->{$sec}->{-proxy_cfg} = $sa if $sa;
for my $k (qw(pollInterval idleInterval)) {
$to = cfg_intvl($cfg, "imap.$k", $$uri) // next;
$self->{imap_opt}->{$sec}->{$k} = $to;
# 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;
}
my $nn_args = {}; # scheme://authority => Net::NNTP->new arg
for my $uri (@{$self->{nntp_order}}) {
my $sec = uri_section($uri);
+ my $args = $nn_args->{$sec} //= {};
# Debug and Timeout are passed to Net::NNTP->new
my $v = cfg_bool($cfg, 'nntp.Debug', $$uri);
- $nn_args->{$sec}->{Debug} = $v if defined $v;
+ $args->{Debug} = $v if defined $v;
my $to = cfg_intvl($cfg, 'nntp.Timeout', $$uri);
- $nn_args->{$sec}->{Timeout} = $to if $to;
+ $args->{Timeout} = $to if $to;
+ my $sa = socks_args($cfg->urlmatch('nntp.Proxy', $$uri));
+ %$args = (%$args, %$sa) if $sa;
# Net::NNTP post-connect commands
for my $k (qw(starttls compress)) {
$self->{nntp_opt}->{$sec}->{$k} = $v;
}
- # internal option
+ # -watch internal option
for my $k (qw(pollInterval)) {
$to = cfg_intvl($cfg, "nntp.$k", $$uri) // next;
$self->{nntp_opt}->{$sec}->{$k} = $to;
}
sub errors {
- my ($self) = @_;
+ my ($self, $lei) = @_;
if (my $u = $self->{unsupported_url}) {
return "Unsupported URL(s): @$u";
}
eval { require Net::NNTP } or
die "Net::NNTP is required for NNTP:\n$@\n";
}
+ my $sa = socks_args($lei ? $lei->{opt}->{proxy} : undef);
+ $self->{-proxy_cli} = $sa if $sa;
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 = flags2kw($self, $uri, $uid, $flags) // return;
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} // 10000;
+ my ($eml_cb, @args) = @{$self->{eml_each}};
+ 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";
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;
+ if (!defined($single_uid) && $self->{each_old} &&
+ perm_fl_ok($perm_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,
+ _imap_do_msg($self, $uri, $uid, \$raw,
$per_uid->{FLAGS});
$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;
}
$mic_arg->{Authcallback} = $self->can($cb_name);
}
}
- my $mic = PublicInbox::IMAPClient->new(%$mic_arg);
+ my $mic = mic_new($self, $mic_arg, $sec, $uri);
$cached //= {}; # invalid placeholder if no cache enabled
$mic && $mic->IsConnected ? ($cached->{$sec} = $mic) : undef;
}
# 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