]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/NetReader.pm
lei import: store IMAP user+auth in mail_sync folder URI
[public-inbox.git] / lib / PublicInbox / NetReader.pm
index b9365c05e37ee9126e39f5e96f3fafe37463477d..a532b2183654a233ed7cce33f761c971c3d6c466 100644 (file)
@@ -7,6 +7,7 @@ use strict;
 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
 
@@ -24,22 +25,50 @@ sub uri_section ($) {
        $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';
@@ -51,15 +80,14 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient
                %$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?
@@ -80,9 +108,14 @@ sub mic_for ($$$$) { # mic = Mail::IMAPClient
        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;
                }
@@ -106,7 +139,14 @@ sub try_starttls ($) {
 
 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
@@ -160,6 +200,8 @@ sub nn_for ($$$$) { # nn = Net::NNTP
                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
@@ -248,6 +290,8 @@ sub imap_common_init ($;$) {
                }
                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;
@@ -263,15 +307,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;
 }
@@ -289,12 +334,15 @@ sub nntp_common_init ($;$) {
        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)) {
@@ -302,7 +350,7 @@ sub nntp_common_init ($;$) {
                        $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;
@@ -331,7 +379,7 @@ sub add_url {
 }
 
 sub errors {
-       my ($self) = @_;
+       my ($self, $lei) = @_;
        if (my $u = $self->{unsupported_url}) {
                return "Unsupported URL(s): @$u";
        }
@@ -343,6 +391,8 @@ sub errors {
                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;
 }
 
@@ -373,12 +423,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;
@@ -413,7 +469,7 @@ 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 ($itrk, $l_uid, $l_uidval) = itrk_last($self, $uri, $r_uidval, $mic);
        return <<EOF if $l_uidval != $r_uidval;
 E: $uri UIDVALIDITY mismatch
 E: local=$l_uidval != remote=$r_uidval
@@ -507,7 +563,7 @@ sub mic_get {
                        $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;
 }
@@ -566,7 +622,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
@@ -582,6 +638,7 @@ sub _nntp_fetch_all ($$$) {
                warn "# $uri fetching ARTICLE $beg..$end\n";
        }
        my $n = $self->{max_batch};
+       my $url = $$uri;
        for ($beg..$end) {
                last if $self->{quit};
                $art = $_;
@@ -604,7 +661,7 @@ sub _nntp_fetch_all ($$$) {
                $raw = join('', @$raw);
                $raw =~ s/\r\n/\n/sg;
                my ($eml_cb, @args) = @{$self->{eml_each}};
-               $eml_cb->($uri, $art, $kw, PublicInbox::Eml->new(\$raw), @args);
+               $eml_cb->($url, $art, $kw, PublicInbox::Eml->new(\$raw), @args);
                $last_art = $art;
        }
        run_commit_cb($self);