X-Git-Url: https://git.distorted.org.uk/~mdw/sgt/utils/blobdiff_plain/3096ed75f04f2ed03fbac480b67165b7280f9fcb..67784f1f3e5992d8f43ca5006f0e6d8fb19949ea:/nntpid/nntpid diff --git a/nntpid/nntpid b/nntpid/nntpid index 2966964..ffdde0d 100755 --- a/nntpid/nntpid +++ b/nntpid/nntpid @@ -6,6 +6,7 @@ # Usage: nntpid # or: nntpid messageid (angle brackets optional) # or: nntpid news.group.name 1234 (group+number form) +# or: nntpid -a news.group.name (download all available articles) # # The name of your news server is obtained from the environment variable # NNTPSERVER, or from the file /etc/nntpserver if that's not set. @@ -15,23 +16,30 @@ # a 480 response from the news server; if your news server isn't paranoid # then the script will never need to look at NNTPAUTH. -# Copyright 2000 Simon Tatham. All rights reserved. -# FIXME: put in a licence notice. +# Copyright 2000,2004,2011 Simon Tatham. All rights reserved. require 5.002; use Socket; use FileHandle; $usage = - "usage: nntpid [ -v ] \n" . - " or: nntpid [ -v ] \n" . - "where: -v verbose (print interaction with news server)\n" . + "usage: nntpid [ -v ] [ -d ]
[
...] display articles\n" . + " or: nntpid [ -v ] [ -d ] display articles read from standard input\n" . + " or: nntpid [ -v ] -a dump a newsgroup in mbox format\n" . + "where:
a news article specified in one of several ways:\n" . + " - Message-Id in angle brackets\n" . + " - bare Message-Id without angle brackets\n" . + " - newsgroup and article number in separate words\n" . + " - newsgroup and article number separated by :\n" . + " -v verbose (print interaction with news server)\n" . + " -d direct output (don't consider using PAGER)\n" . + " -a dump all articles in group to stdout as mbox\n" . " also: nntpid --version report version number\n" . " nntpid --help display this help text\n" . " nntpid --licence display (MIT) licence text\n"; $licence = - "nntpid is copyright 2000,2004 Simon Tatham.\n" . + "nntpid is copyright 2000,2004,2011 Simon Tatham.\n" . "\n" . "Permission is hereby granted, free of charge, to any person\n" . "obtaining a copy of this software and associated documentation files\n" . @@ -53,9 +61,15 @@ $licence = "CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n" . "SOFTWARE.\n"; +$pager = 1; +$verbose = 0; +$mode = 'list'; + while ($ARGV[0] =~ /^-(.+)$/) { shift @ARGV; - $verbose=1, next if $1 eq "v"; + $verbose = 1, next if $1 eq "v"; + $pager = 0, next if $1 eq "d"; + $mode = 'all', next if $1 eq "a"; if ($1 eq "-help") { print STDERR $usage; exit 0; @@ -72,49 +86,149 @@ while ($ARGV[0] =~ /^-(.+)$/) { } } -die $usage if !defined $ARGV[0]; - -if (defined $ARGV[1]) { +if ($mode eq 'all') { + # -a uses completely different command-line semantics from the + # normal ones.. + die "nntpid: -a expected exactly one argument\n" if @ARGV != 1; $group = $ARGV[0]; - $mid = $ARGV[1]; +} elsif (!@ARGV) { + # We will read article ids from standard input once we've connected + # to the NNTP server. + $mode = 'stdin'; } else { - $group = "misc.misc"; - $mid = $ARGV[0]; - $mid =~ s/^$//; - $mid = "<$mid>"; + @list = (); + while (defined ($arg = shift @ARGV)) { + # See if this argument makes sense on its own. + ($group, $mid) = &parsearticle($arg); + if (defined $mid) { + push @list, $arg; + } else { + # If it doesn't, try concatenating it with a space to the next + # argument (so you can provide a group and article number in two + # successive command-line arguments). + $args = $arg . " " . $ARGV[0]; + ($group, $mid) = &parsearticle($args); + if (defined $mid) { + push @list, $args; + shift @ARGV; # and eat the second argument + } else { + # If all else fails, die in panic. + die "nntpid: argument '$arg': unable to parse\n"; + } + } + } } $ns=$ENV{'NNTPSERVER'}; if (!defined $ns or !length $ns) { $ns = `cat /etc/nntpserver`; + chomp $ns; } $port = (getservbyname("nntp", "tcp"))[2]; $ns = inet_aton($ns); $proto = getprotobyname("tcp"); $paddr = sockaddr_in($port, $ns); -socket(S,PF_INET,SOCK_STREAM,$proto) or die "socket: $!"; -connect(S,$paddr) or die "connect: $!"; +&connect; +if ($mode eq 'all') { + # Write out the entire contents of a newsgroup in mbox format. + $numbers = &docmd("GROUP $group"); + @numbers = split / /, $numbers; + $fatal = 0; # ignore failure to retrieve any given article + for ($mid = $numbers[1]; $mid <= $numbers[2]; $mid++) { + $art = &getart("$group:$mid"); + $art =~ s/\n(>*From )/\n>$1/gs; + print "From nntpid ".(localtime)."\n".$art."\n"; + } +} elsif ($mode eq 'stdin') { + while (<>) { + chomp; + s/^\s+//; s/\s+$//; # trim whitespace + &displayarticle($_); + } +} elsif ($mode eq 'list') { + for $item (@list) { + &displayarticle($item); + } +} -S->autoflush(1); +sub parsearticle { + # Article identifiers used as input to this program can be in a + # variety of formats. This function untangles one into a standard + # format, which is either (undef, message-id) or (group, article + # number). In case of parse failure, it returns (undef, undef). + my $art = shift @_; + if ($art =~ /^(.*<)?([^<>]*\@[^<>]*)(>.*)?$/) { + # Anything with an @ sign is treated as a Message-ID. We trim + # angle brackets and anything outside them. + return (undef, $2); + } elsif ($art =~ /^(\S+)(\s+|:)(\d+)$/) { + # A group name and article number separated by whitespace or a + # colon. + return ($1, $3); + } else { + # Unable to parse. + return (undef, undef); + } +} -&getline; -$code =~ /^2\d\d/ or die "no initial greeting from server\n"; +sub displayarticle { + my $mid = shift @_; -&docmd("MODE READER"); -# some servers require a GROUP before an ARTICLE command -&docmd("GROUP $group"); -&docmd("ARTICLE $mid"); -while (1) { - &getline; - s/[\r\n]//g; - last if /^\.$/; - s/^\.//; - print STDOUT "$_\n"; + &connect; + + my $art = &getart($mid); + + if ($pager and -t STDOUT) { + # Close the NNTP connection before invoking the pager, in case the + # user spends so long looking at the article that the server times + # us out. + &disconnect; + + $pagername = $ENV{"PAGER"}; + $pagername = "more" unless defined $pagername; + open PAGER, "| $pagername"; + print PAGER $art; + close PAGER; + } else { + print $art; + } +} + +sub getart { + my $art = shift @_; + my $group; + my $mid; + + ($group, $mid) = &parsearticle($art); + if (!defined $mid) { + warn "unable to parse '$art'\n"; + return undef; + } elsif (defined $group) { + # This is a (group, article number) pair. + &docmd("GROUP $group"); + $ret = &docmd("ARTICLE $mid"); + } else { + # This is a Message-Id. Some NNTP servers will insist on having + # seen a GROUP command before 'ARTICLE ', + # so ensure we've sent one. + &docmd("GROUP misc.misc") unless $in_a_group; + $ret = &docmd("ARTICLE <$mid>"); + } + + return undef if !defined $ret; + $in_a_group = 1; + + $art = ""; + while (1) { + &getline; + s/[\r\n]//g; + last if /^\.$/; + s/^\.//; + $art .= "$_\n"; + } + return $art; } -&docmd("QUIT"); -close S; sub putline { my ($line) = @_; @@ -127,16 +241,50 @@ sub getline { s/[\r\n]*$//s; $code = substr($_,0,3); print STDERR "<<< $_\n" if $verbose; + return substr($_,4); +} + +sub connect { + return if $connected; + socket(S,PF_INET,SOCK_STREAM,$proto) or die "socket: $!"; + connect(S,$paddr) or die "connect: $!"; + + S->autoflush(1); + + $fatal = 1; # most errors need to be fatal + + &getline; + $code =~ /^2\d\d/ or die "no initial greeting from server\n"; + + &docmd("MODE READER"); + + $connected = 1; + $in_a_group = 0; +} + +sub disconnect { + &docmd("QUIT"); + close S; + $connected = 0; } sub docmd { my ($cmd) = @_; - while (1) { + # We go at most twice round the following loop. If the first attempt + # to fetch the article fails with a 480 response, we try again + # having authenticated first; but if the second attempt also fails + # with 480, then the authentication didn't work, so we should give + # up rather than try it pointlessly again. + for my $n (0,1) { &putline($cmd); - &getline; + $line = &getline; if ($code eq "480") { &auth; } else { last; } } - $code =~ /^2\d\d/ or die "failed on `$cmd':\n$_\n"; + if ($code !~ /^2\d\d/) { + die "failed on `$cmd':\n$_\n" if $fatal; + return undef; + } + return $line; } sub auth {