about summary refs log tree commit homepage
path: root/lib/PublicInbox/NNTP.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/PublicInbox/NNTP.pm')
-rw-r--r--lib/PublicInbox/NNTP.pm177
1 files changed, 107 insertions, 70 deletions
diff --git a/lib/PublicInbox/NNTP.pm b/lib/PublicInbox/NNTP.pm
index 8740377f..9408ffb9 100644
--- a/lib/PublicInbox/NNTP.pm
+++ b/lib/PublicInbox/NNTP.pm
@@ -9,12 +9,14 @@ use base qw(Danga::Socket);
 use fields qw(nntpd article rbuf ng long_res);
 use PublicInbox::Search;
 use PublicInbox::Msgmap;
+use PublicInbox::MID qw(mid_escape);
 use PublicInbox::Git;
-use PublicInbox::MID qw(mid2path);
-use Email::MIME;
-use Data::Dumper qw(Dumper);
+require PublicInbox::EvCleanup;
+use Email::Simple;
 use POSIX qw(strftime);
 use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
+use Digest::SHA qw(sha1_hex);
+use Time::Local qw(timegm timelocal);
 use constant {
         r501 => '501 command syntax error',
         r221 => '221 Header follows',
@@ -36,21 +38,36 @@ my $LIST_HEADERS = join("\r\n", @OVERVIEW,
 my %DISABLED; # = map { $_ => 1 } qw(xover list_overview_fmt newnews xhdr);
 
 my $EXPMAP; # fd -> [ idle_time, $self ]
-my $EXPTIMER;
+my $expt;
 our $EXPTIME = 180; # 3 minutes
+my $nextt;
+
+my $nextq = [];
+sub next_tick () {
+        $nextt = undef;
+        my $q = $nextq;
+        $nextq = [];
+        foreach my $nntp (@$q) {
+                # for request && response protocols, always finish writing
+                # before finishing reading:
+                if (my $long_cb = $nntp->{long_res}) {
+                        $nntp->write($long_cb);
+                } elsif (&Danga::Socket::POLLIN & $nntp->{event_watch}) {
+                        event_read($nntp);
+                }
+        }
+}
 
 sub update_idle_time ($) {
         my ($self) = @_;
-        my $tmp = $self->{sock} or return;
-        $tmp = fileno($tmp);
-        defined $tmp and $EXPMAP->{$tmp} = [ now(), $self ];
+        my $fd = $self->{fd};
+        defined $fd and $EXPMAP->{$fd} = [ now(), $self ];
 }
 
 sub expire_old () {
         my $now = now();
         my $exp = $EXPTIME;
         my $old = $now - $exp;
-        my $next = $now + $exp;
         my $nr = 0;
         my %new;
         while (my ($fd, $v) = each %$EXPMAP) {
@@ -58,36 +75,31 @@ sub expire_old () {
                 if ($idle_time < $old) {
                         $nntp->close; # idempotent
                 } else {
-                        my $nexp = $idle_time + $exp;
-                        $next = $nexp if ($nexp < $next);
                         ++$nr;
                         $new{$fd} = $v;
                 }
         }
         $EXPMAP = \%new;
         if ($nr) {
-                $next -= $now;
-                $next = 0 if $next < 0;
-                $EXPTIMER = Danga::Socket->AddTimer($next, *expire_old);
+                $expt = PublicInbox::EvCleanup::later(*expire_old);
         } else {
-                $EXPTIMER = undef;
-                # noop to kick outselves out of the loop so descriptors
+                $expt = undef;
+                # noop to kick outselves out of the loop ASAP so descriptors
                 # really get closed
-                Danga::Socket->AddTimer(0, sub {});
+                PublicInbox::EvCleanup::asap(sub {});
         }
 }
 
 sub new ($$$) {
         my ($class, $sock, $nntpd) = @_;
         my $self = fields::new($class);
-        binmode $sock, ':utf8'; # RFC 3977
         $self->SUPER::new($sock);
         $self->{nntpd} = $nntpd;
         res($self, '201 server ready - post via email');
         $self->{rbuf} = '';
         $self->watch_read(1);
         update_idle_time($self);
-        $EXPTIMER ||= Danga::Socket->AddTimer($EXPTIME, *expire_old);
+        $expt ||= PublicInbox::EvCleanup::later(*expire_old);
         $self;
 }
 
@@ -115,7 +127,8 @@ sub process_line ($$) {
         my $res = eval { $req->($self, @args) };
         my $err = $@;
         if ($err && !$self->{closed}) {
-                chomp($l = Dumper(\$l));
+                local $/ = "\n";
+                chomp($l);
                 err($self, 'error from: %s (%s)', $l, $err);
                 $res = '503 program fault - command not performed';
         }
@@ -153,7 +166,7 @@ sub list_active ($;$) {
         my ($self, $wildmat) = @_;
         wildmat2re($wildmat);
         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
-                $ng->{name} =~ $wildmat or next;
+                $ng->{newsgroup} =~ $wildmat or next;
                 group_line($self, $ng);
         }
 }
@@ -162,9 +175,9 @@ sub list_active_times ($;$) {
         my ($self, $wildmat) = @_;
         wildmat2re($wildmat);
         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
-                $ng->{name} =~ $wildmat or next;
+                $ng->{newsgroup} =~ $wildmat or next;
                 my $c = eval { $ng->mm->created_at } || time;
-                more($self, "$ng->{name} $c $ng->{address}");
+                more($self, "$ng->{newsgroup} $c $ng->{-primary_address}");
         }
 }
 
@@ -172,9 +185,9 @@ sub list_newsgroups ($;$) {
         my ($self, $wildmat) = @_;
         wildmat2re($wildmat);
         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
-                $ng->{name} =~ $wildmat or next;
+                $ng->{newsgroup} =~ $wildmat or next;
                 my $d = $ng->description;
-                more($self, "$ng->{name} $d");
+                more($self, "$ng->{newsgroup} $d");
         }
 }
 
@@ -226,7 +239,6 @@ sub cmd_listgroup ($;$) {
 
 sub parse_time ($$;$) {
         my ($date, $time, $gmt) = @_;
-        use Time::Local qw();
         my ($hh, $mm, $ss) = unpack('A2A2A2', $time);
         if (defined $gmt) {
                 $gmt =~ /\A(?:UTC|GMT)\z/i or die "GM invalid: $gmt";
@@ -238,22 +250,22 @@ sub parse_time ($$;$) {
                 ($YYYY, $MM, $DD) = unpack('A4A2A2', $date);
         } else { # legacy clients send YYMMDD
                 ($YYYY, $MM, $DD) = unpack('A2A2A2', $date);
-                if ($YYYY > strftime('%y', @now)) {
-                        my $cur_year = $now[5] + 1900;
+                my $cur_year = $now[5] + 1900;
+                if ($YYYY > $cur_year) {
                         $YYYY += int($cur_year / 1000) * 1000 - 100;
                 }
         }
         if ($gmt) {
-                Time::Local::timegm($ss, $mm, $hh, $DD, $MM - 1, $YYYY);
+                timegm($ss, $mm, $hh, $DD, $MM - 1, $YYYY);
         } else {
-                Time::Local::timelocal($ss, $mm, $hh, $DD, $MM - 1, $YYYY);
+                timelocal($ss, $mm, $hh, $DD, $MM - 1, $YYYY);
         }
 }
 
 sub group_line ($$) {
         my ($self, $ng) = @_;
         my ($min, $max) = $ng->mm->minmax;
-        more($self, "$ng->{name} $max $min n") if defined $min && defined $max;
+        more($self, "$ng->{newsgroup} $max $min n") if defined $min && defined $max;
 }
 
 sub cmd_newgroups ($$$;$$) {
@@ -275,7 +287,6 @@ sub wildmat2re (;$) {
         return $_[0] = qr/.*/ if (!defined $_[0] || $_[0] eq '*');
         my %keep;
         my $salt = rand;
-        use Digest::SHA qw(sha1_hex);
         my $tmp = $_[0];
 
         $tmp =~ s#(?<!\\)\[(.+)(?<!\\)\]#
@@ -313,8 +324,8 @@ sub cmd_newnews ($$$$;$$) {
         ngpat2re($skip);
         my @srch;
         foreach my $ng (@{$self->{nntpd}->{grouplist}}) {
-                $ng->{name} =~ $keep or next;
-                $ng->{name} =~ $skip and next;
+                $ng->{newsgroup} =~ $keep or next;
+                $ng->{newsgroup} =~ $skip and next;
                 my $srch = $ng->search or next;
                 push @srch, $srch;
         };
@@ -382,7 +393,8 @@ sub cmd_last ($) { article_adj($_[0], -1) }
 sub cmd_post ($) {
         my ($self) = @_;
         my $ng = $self->{ng};
-        $ng ? "440 mailto:$ng->{address} to post" : '440 posting not allowed'
+        $ng ? "440 mailto:$ng->{-primary_address} to post"
+                : '440 posting not allowed'
 }
 
 sub cmd_quit ($) {
@@ -392,6 +404,29 @@ sub cmd_quit ($) {
         undef;
 }
 
+sub header_append ($$$) {
+        my ($hdr, $k, $v) = @_;
+        my @v = $hdr->header($k);
+        foreach (@v) {
+                return if $v eq $_;
+        }
+        $hdr->header_set($k, @v, $v);
+}
+
+sub set_nntp_headers {
+        my ($hdr, $ng, $n, $mid) = @_;
+
+        # clobber some
+        $hdr->header_set('Newsgroups', $ng->{newsgroup});
+        $hdr->header_set('Xref', xref($ng, $n));
+        header_append($hdr, 'List-Post', "<mailto:$ng->{-primary_address}>");
+        if (my $url = $ng->base_url) {
+                $mid = mid_escape($mid);
+                header_append($hdr, 'Archived-At', "<$url$mid/>");
+                header_append($hdr, 'List-Archive', "<$url>");
+        }
+}
+
 sub art_lookup ($$$) {
         my ($self, $art, $set_headers) = @_;
         my $ng = $self->{ng};
@@ -428,14 +463,12 @@ find_mid:
                 defined $mid or return $err;
         }
 found:
-        my $o = 'HEAD:' . mid2path($mid);
         my $bytes;
-        my $s = eval { Email::MIME->new($ng->gcf->cat_file($o, \$bytes)) };
-        return $err unless $s;
+        my $s = eval { $ng->msg_by_mid($mid, \$bytes) } or return $err;
+        $s = Email::Simple->new($s);
         my $lines;
         if ($set_headers) {
-                $s->header_set('Newsgroups', $ng->{name});
-                $s->header_set('Xref', xref($ng, $n));
+                set_nntp_headers($s->header_obj, $ng, $n, $mid);
                 $lines = $s->body =~ tr!\n!\n!;
 
                 # must be last
@@ -460,6 +493,12 @@ sub set_art {
         $self->{article} = $art if defined $art && $art =~ /\A\d+\z/;
 }
 
+sub _header ($) {
+        my $hdr = $_[0]->header_obj->as_string;
+        utf8::encode($hdr);
+        $hdr
+}
+
 sub cmd_article ($;$) {
         my ($self, $art) = @_;
         my $r = art_lookup($self, $art, 1);
@@ -467,7 +506,7 @@ sub cmd_article ($;$) {
         my ($n, $mid, $s) = @$r;
         set_art($self, $art);
         more($self, "220 $n <$mid> article retrieved - head and body follow");
-        do_more($self, $s->header_obj->as_string);
+        do_more($self, _header($s));
         do_more($self, "\r\n");
         simple_body_write($self, $s);
 }
@@ -479,7 +518,7 @@ sub cmd_head ($;$) {
         my ($n, $mid, $s) = @$r;
         set_art($self, $art);
         more($self, "221 $n <$mid> article retrieved - head follows");
-        do_more($self, $s->header_obj->as_string);
+        do_more($self, _header($s));
         '.'
 }
 
@@ -533,16 +572,6 @@ sub get_range ($$) {
         [ $beg, $end ];
 }
 
-sub hdr_val ($$) {
-        my ($r, $header) = @_;
-        return $r->[3] if $header =~ /\A:?bytes\z/i;
-        return $r->[4] if $header =~ /\A:?lines\z/i;
-        $r = $r->[2]->header_obj->header($header);
-        defined $r or return;
-        $r =~ s/[\r\n\t]+/ /sg;
-        $r;
-}
-
 sub long_response ($$$$) {
         my ($self, $beg, $end, $cb) = @_;
         die "BUG: nested long response" if $self->{long_res};
@@ -584,9 +613,9 @@ sub long_response ($$$$) {
                         # no recursion, schedule another call ASAP
                         # but only after all pending writes are done
                         update_idle_time($self);
-                        Danga::Socket->AddTimer(0, sub {
-                                $self->write($self->{long_res});
-                        });
+
+                        push @$nextq, $self;
+                        $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
                 } else { # all done!
                         $self->{long_res} = undef;
                         $self->watch_read(1);
@@ -622,7 +651,7 @@ sub hdr_message_id ($$$) { # optimize XHDR Message-ID [range] for slrnpull.
 
 sub xref ($$) {
         my ($ng, $n) = @_;
-        "$ng->{domain} $ng->{name}:$n"
+        "$ng->{domain} $ng->{newsgroup}:$n"
 }
 
 sub mid_lookup ($$) {
@@ -665,8 +694,7 @@ sub hdr_xref ($$$) { # optimize XHDR Xref [range] for rtin
 
 sub search_header_for {
         my ($srch, $mid, $field) = @_;
-        my $smsg = $srch->lookup_message($mid) or return;
-        $smsg = PublicInbox::SearchMsg->load_doc($smsg->{doc});
+        my $smsg = $srch->lookup_mail($mid) or return;
         $smsg->$field;
 }
 
@@ -696,6 +724,7 @@ sub hdr_searchmsg ($$$$) {
                         foreach my $s (@$msgs) {
                                 $tmp .= $s->num . ' ' . $s->$field . "\r\n";
                         }
+                        utf8::encode($tmp);
                         do_more($self, $tmp);
                         # -1 to adjust for implicit increment in long_response
                         $$i = $nr ? $$i + $nr - 1 : long_response_limit;
@@ -784,7 +813,7 @@ sub over_line ($$) {
         my ($num, $smsg) = @_;
         # n.b. field access and procedural calls can be
         # 10%-15% faster than OO method calls:
-        join("\t", $num,
+        my $s = join("\t", $num,
                 $smsg->{subject},
                 $smsg->{from},
                 PublicInbox::SearchMsg::date($smsg),
@@ -792,16 +821,17 @@ sub over_line ($$) {
                 $smsg->{references},
                 PublicInbox::SearchMsg::bytes($smsg),
                 PublicInbox::SearchMsg::lines($smsg));
+        utf8::encode($s);
+        $s
 }
 
 sub cmd_over ($;$) {
         my ($self, $range) = @_;
         if ($range && $range =~ /\A<(.+)>\z/) {
                 my ($ng, $n) = mid_lookup($self, $1);
-                my $smsg = $ng->search->lookup_message($range) or
+                my $smsg = $ng->search->lookup_mail($range) or
                         return '430 No article with that message-id';
                 more($self, '224 Overview information follows (multi-line)');
-                $smsg = PublicInbox::SearchMsg->load_doc($smsg->{doc});
 
                 # Only set article number column if it's the current group
                 my $self_ng = $self->{ng};
@@ -846,7 +876,7 @@ sub cmd_xpath ($$) {
         my @paths;
         foreach my $ng (values %{$self->{nntpd}->{groups}}) {
                 my $n = $ng->mm->num_for($mid);
-                push @paths, "$ng->{name}/$n" if defined $n;
+                push @paths, "$ng->{newsgroup}/$n" if defined $n;
         }
         return '430 no such article on server' unless @paths;
         '223 '.join(' ', @paths);
@@ -865,7 +895,7 @@ sub more ($$) {
 sub do_write ($$) {
         my ($self, $data) = @_;
         my $done = $self->write($data);
-        die if $self->{closed};
+        return if $self->{closed};
 
         # Do not watch for readability if we have data in the queue,
         # instead re-enable watching for readability when we can
@@ -922,6 +952,7 @@ sub event_read {
         $self->{rbuf} .= $$buf;
         while ($r > 0 && $self->{rbuf} =~ s/\A\s*([^\r\n]+)\r?\n//) {
                 my $line = $1;
+                return $self->close if $line =~ /[[:cntrl:]]/s;
                 my $t0 = now();
                 my $fd = $self->{fd};
                 $r = eval { process_line($self, $line) };
@@ -945,19 +976,25 @@ sub watch_read {
                 # and we must double-check again by the time the timer fires
                 # in case we really did dispatch a read event and started
                 # another long response.
-                Danga::Socket->AddTimer(0, sub {
-                        if (&Danga::Socket::POLLIN & $self->{event_watch}) {
-                                event_read($self);
-                        }
-                });
+                push @$nextq, $self;
+                $nextt ||= PublicInbox::EvCleanup::asap(*next_tick);
         }
         $rv;
 }
 
+sub not_idle_long ($$) {
+        my ($self, $now) = @_;
+        defined(my $fd = $self->{fd}) or return;
+        my $ary = $EXPMAP->{$fd} or return;
+        my $exp_at = $ary->[0] + $EXPTIME;
+        $exp_at > $now;
+}
+
 # for graceful shutdown in PublicInbox::Daemon:
-sub busy () {
-        my ($self) = @_;
-        ($self->{rbuf} ne '' || $self->{long_res} || $self->{write_buf_size});
+sub busy {
+        my ($self, $now) = @_;
+        ($self->{rbuf} ne '' || $self->{long_res} || $self->{write_buf_size} ||
+         not_idle_long($self, $now));
 }
 
 1;