+sub imap_idle_fork ($$) {
+ my ($self, $url_intvl) = @_;
+ my ($url, $intvl) = @$url_intvl;
+ defined(my $pid = fork) or die "fork: $!";
+ if ($pid == 0) {
+ watch_atfork_child($self);
+ watch_imap_idle_1($self, $url, $intvl);
+ _exit(0);
+ }
+ $self->{idle_pids}->{$pid} = $url_intvl;
+ PublicInbox::DS::dwaitpid($pid, \&imap_idle_reap, $self);
+}
+
+sub event_step {
+ my ($self) = @_;
+ return if $self->{quit};
+ my $idle_todo = $self->{idle_todo};
+ if ($idle_todo && @$idle_todo) {
+ watch_atfork_parent($self);
+ while (my $url_intvl = shift(@$idle_todo)) {
+ imap_idle_fork($self, $url_intvl);
+ }
+ }
+ goto(&fs_scan_step) if $self->{mdre};
+}
+
+sub watch_imap_fetch_all ($$) {
+ my ($self, $urls) = @_;
+ for my $url (@$urls) {
+ my $uri = PublicInbox::URIimap->new($url);
+ my $sec = uri_section($uri);
+ my $mic_arg = $self->{mic_arg}->{$sec} or
+ die "BUG: no Mail::IMAPClient->new arg for $sec";
+ my $mic = PublicInbox::IMAPClient->new(%$mic_arg) or next;
+ my $err = imap_fetch_all($self, $mic, $url);
+ last if $self->{quit};
+ warn $err, "\n" if $err;
+ }
+}
+
+sub watch_nntp_fetch_all ($$) {
+ my ($self, $urls) = @_;
+ for my $url (@$urls) {
+ my $uri = uri_new($url);
+ my $sec = uri_section($uri);
+ my $nn_arg = $self->{nn_arg}->{$sec} or
+ die "BUG: no Net::NNTP->new arg for $sec";
+ my $nntp_opt = $self->{nntp_opt}->{$sec};
+ my $nn = nn_new($nn_arg, $nntp_opt, $url);
+ unless ($nn) {
+ warn "E: $url: \$!=$!\n";
+ next;
+ }
+ last if $self->{quit};
+ if (my $postconn = $nntp_opt->{-postconn}) {
+ for my $m_arg (@$postconn) {
+ my ($method, @args) = @$m_arg;
+ $nn->$method(@args) and next;
+ warn "E: <$url> $method failed\n";
+ $nn = undef;
+ last;
+ }
+ }
+ last if $self->{quit};
+ if ($nn) {
+ my $err = nntp_fetch_all($self, $nn, $url);
+ warn $err, "\n" if $err;
+ }
+ }
+}
+
+sub poll_fetch_fork ($) { # DS::add_timer callback
+ my ($self, $intvl, $urls) = @{$_[0]};
+ return if $self->{quit};
+ watch_atfork_parent($self);
+ defined(my $pid = fork) or die "fork: $!";
+ if ($pid == 0) {
+ watch_atfork_child($self);
+ if ($urls->[0] =~ m!\Aimaps?://!i) {
+ watch_imap_fetch_all($self, $urls);
+ } else {
+ watch_nntp_fetch_all($self, $urls);
+ }
+ _exit(0);
+ }
+ $self->{poll_pids}->{$pid} = [ $intvl, $urls ];
+ PublicInbox::DS::dwaitpid($pid, \&poll_fetch_reap, $self);
+}
+
+sub poll_fetch_reap { # PublicInbox::DS::dwaitpid callback
+ my ($self, $pid) = @_;
+ my $intvl_urls = delete $self->{poll_pids}->{$pid} or
+ die "BUG: PID=$pid (unknown) reaped: \$?=$?\n";
+ return if $self->{quit};
+ my ($intvl, $urls) = @$intvl_urls;
+ if ($?) {
+ warn "W: PID=$pid died: \$?=$?\n", map { "$_\n" } @$urls;
+ }
+ warn("I: will check $_ in ${intvl}s\n") for @$urls;
+ PublicInbox::DS::add_timer($intvl, \&poll_fetch_fork,
+ [$self, $intvl, $urls]);
+}
+
+sub watch_imap_init ($$) {
+ my ($self, $poll) = @_;
+ eval { require PublicInbox::IMAPClient } or
+ die "Mail::IMAPClient is required for IMAP:\n$@\n";
+ eval { require PublicInbox::IMAPTracker } or
+ die "DBD::SQLite is required for IMAP\n:$@\n";
+
+ my $mic_args = imap_common_init($self); # read args from config
+
+ # 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 $url (sort keys %{$self->{imap}}) {
+ my $uri = PublicInbox::URIimap->new($url);
+ $mics->{uri_section($uri)} //= mic_for($self, $url, $mic_args);
+ }
+
+ my $idle = []; # [ [ url1, intvl1 ], [url2, intvl2] ]
+ for my $url (keys %{$self->{imap}}) {
+ my $uri = PublicInbox::URIimap->new($url);
+ my $sec = uri_section($uri);
+ my $mic = $mics->{$sec};
+ my $intvl = $self->{imap_opt}->{$sec}->{pollInterval};
+ if ($mic->has_capability('IDLE') && !$intvl) {
+ $intvl = $self->{imap_opt}->{$sec}->{idleInterval};
+ push @$idle, [ $url, $intvl // () ];
+ } else {
+ push @{$poll->{$intvl || 120}}, $url;
+ }
+ }
+ if (scalar @$idle) {
+ $self->{idle_todo} = $idle;
+ PublicInbox::DS::requeue($self); # ->event_step to fork
+ }
+}
+
+# flesh out common NNTP-specific data structures
+sub nntp_common_init ($) {
+ my ($self) = @_;
+ my $cfg = $self->{config};
+ my $nn_args = {}; # scheme://authority => Net::NNTP->new arg
+ for my $url (sort keys %{$self->{nntp}}) {
+ my $sec = uri_section(uri_new($url));
+
+ # Debug and Timeout are passed to Net::NNTP->new
+ my $v = cfg_bool($cfg, 'nntp.Debug', $url);
+ $nn_args->{$sec}->{Debug} = $v if defined $v;
+ my $to = cfg_intvl($cfg, 'nntp.Timeout', $url);
+ $nn_args->{$sec}->{Timeout} = $to if $to;
+
+ # Net::NNTP post-connect commands
+ for my $k (qw(starttls compress)) {
+ $v = cfg_bool($cfg, "nntp.$k", $url) // next;
+ $self->{nntp_opt}->{$sec}->{$k} = $v;
+ }
+
+ # internal option
+ for my $k (qw(pollInterval)) {
+ $to = cfg_intvl($cfg, "nntp.$k", $url) // next;
+ $self->{nntp_opt}->{$sec}->{$k} = $to;
+ }
+ }
+ $nn_args;
+}
+
+# Net::NNTP doesn't support CAPABILITIES, yet
+sub try_starttls ($) {
+ my ($host) = @_;
+ return if $host =~ /\.onion\z/s;
+ return if $host =~ /\A127\.[0-9]+\.[0-9]+\.[0-9]+\z/s;
+ return if $host eq '::1';
+ 1;
+}
+
+sub nn_new ($$$) {
+ my ($nn_arg, $nntp_opt, $url) = @_;
+ my $nn = Net::NNTP->new(%$nn_arg) or die "E: <$url> new: $!\n";
+
+ # 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}) &&
+ try_starttls($nn_arg->{Host})) {
+ # soft fail by default
+ $nn->starttls or warn <<"";
+W: <$url> STARTTLS tried and failed (not requested)
+
+ } elsif ($nntp_opt->{starttls}) {
+ # hard fail if explicitly configured
+ $nn->starttls or die <<"";
+E: <$url> STARTTLS requested and failed
+
+ }
+ } elsif ($nntp_opt->{starttls}) {
+ $nn->can('starttls') or
+ die "E: <$url> Net::NNTP too old for STARTTLS\n";
+ $nn->starttls or die <<"";
+E: <$url> STARTTLS requested and failed
+
+ }
+ $nn;
+}
+
+sub nn_for ($$$) { # nn = Net::NNTP
+ my ($self, $url, $nn_args) = @_;
+ my $uri = uri_new($url);
+ my $sec = uri_section($uri);
+ my $nntp_opt = $self->{nntp_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';
+ my $cred;
+ my ($u, $p);
+ if (defined(my $ui = $uri->userinfo)) {
+ require PublicInbox::GitCredential;
+ $cred = bless {
+ url => $sec,
+ protocol => uri_scheme($uri),
+ host => $host,
+ }, 'PublicInbox::GitCredential';
+ ($u, $p) = split(/:/, $ui, 2);
+ ($cred->{username}, $cred->{password}) = ($u, $p);
+ $cred->check_netrc unless defined $p;
+ }
+ my $common = $nn_args->{$sec} // {};
+ my $nn_arg = {
+ Port => $uri->port,
+ Host => $host,
+ SSL => $uri->secure, # snews == nntps
+ %$common, # may Debug ....
+ };
+ my $nn = nn_new($nn_arg, $nntp_opt, $url);
+
+ if ($cred) {
+ $cred->fill; # may prompt user here
+ if ($nn->authinfo($u, $p)) {
+ push @{$nntp_opt->{-postconn}}, [ 'authinfo', $u, $p ];
+ } else {
+ warn "E: <$url> AUTHINFO $u XXXX failed\n";
+ $nn = undef;
+ }