File Coverage

blib/lib/News/NNTPClient.pm
Criterion Covered Total %
statement 192 302 63.5
branch 35 120 29.1
condition 28 82 34.1
subroutine 48 60 80.0
pod 42 55 76.3
total 345 619 55.7


line stmt bran cond sub pod time code
1             package News::NNTPClient;
2              
3             require 5.002;
4              
5 1     1   711 use Carp;
  1         1  
  1         83  
6 1     1   1027 use Socket qw(1.5 inet_aton pack_sockaddr_in PF_INET SOCK_STREAM AF_INET);
  1         4425  
  1         343  
7              
8 1     1   9 use strict qw(vars subs);
  1         6  
  1         30  
9 1     1   5 use vars qw($VERSION $fhcnt);
  1         2  
  1         4012  
10              
11             $fhcnt = 0; # File handle counter, to insure unique socket.
12             $VERSION = (qw$Revision: 0.37 $)[1];
13              
14             # Create a new NNTP object. Set up defaults for host and port, and
15             # attempt connection. For host, if not supplied, check the
16             # environment variable NNTPSERVER. If that isn't set, then hostname
17             # is "news". For port, check the environment variable NNTPPORT, or
18             # use "nntp" service or 119.
19             sub new {
20 1     1 1 13 my $name = shift;
21 1   50     10 my $host = shift || $ENV{NNTPSERVER} || "news";
22 1   50     1328 my $port = shift || $ENV{NNTPPORT} || getservbyname("nntp","tcp") || 119;
23 1         3 my $debug = shift;
24              
25 1 50       18 my $me = bless {
26             DBUG => defined ($debug) ? $debug : 1,
27             SOCK => $name . "::SOCK" . ++$fhcnt,
28             HOST => $host,
29             PORT => $port,
30             ADDR => "",
31             MESG => "",
32             CODE => 0,
33             POST => undef,
34             EOL => "\n", # End Of Line
35             GMT => 0, # Greenwich Mean Time
36             FDY => 0, # Four Digit Year
37             }, $name;
38              
39 1         6 $me->initialize();
40              
41 1         8 $me;
42             }
43              
44             sub initialize {
45 1     1 1 2 my $me = shift;
46            
47 1 50 33     9 $me->port($me->{PORT}) and
      33        
48             $me->host($me->{HOST}) and
49             $me->connect and
50             $me->response;
51             }
52              
53             # Determine port number. If we were passed a non-numeric port,
54             # attempt to look it up.
55             sub port {
56 1     1 1 2 my $me = shift;
57 1 50       3 my $port = shift or return $me->{PORT};
58              
59 1 50       236 unless ($port =~ /^\d+$/) {
60 0 0 0     0 my $tmp = getservbyname ($port, "tcp") or
61             carp "News::NNTPClient: Bad port: $port" and return;
62 0         0 $port = $tmp;
63             }
64              
65 1         216 $me->{PORT} = $port;
66             }
67              
68             # Resolve hostname.
69             sub host {
70 1     1 1 2 my $me = shift;
71 1 50       5 my $host = shift or return $me->{HOST};
72 1         1 my $addr;
73              
74             # Get address.
75 1 50 50     10071 $addr = inet_aton($host) or
76             carp "News::NNTPClient: Bad hostname: $host" and return;
77              
78 0         0 $me->{ADDR} = $addr;
79              
80             # Get fully qualified domain name if possible
81 0   0     0 $me->{HOST} = gethostbyaddr ($addr, AF_INET) || $host;
82             }
83              
84             # Connect to server.
85             sub connect {
86 0     0 1 0 my $me = shift;
87              
88 0         0 my $SOCK = $me->{SOCK};
89              
90 0 0       0 if (defined fileno $SOCK) {
91 0 0       0 1 < $me->{DBUG} and
92             warn "$SOCK already connected, closing\n";
93 0         0 close $SOCK;
94             }
95              
96 0 0       0 1 < $me->{DBUG} and
97             warn "$SOCK connecting to $me->{HOST}:$me->{PORT}\n";
98              
99 0 0 0     0 socket ($SOCK, PF_INET, SOCK_STREAM, getprotobyname("tcp") || 6) or
      0        
100             carp "News::NNTPClient: Can't open socket: $!" and return;
101              
102 0 0       0 unless (connect($SOCK, pack_sockaddr_in($me->{PORT},$me->{ADDR}))) {
103 0         0 carp "News::NNTPClient: Can't connect socket: $!";
104 0         0 close $SOCK;
105 0         0 return;
106             }
107              
108 0         0 select ((select($SOCK), $|=1)[0]); # Turn on autoflush.
109              
110 0         0 1;
111             }
112              
113             ########################################################################
114             # Helper methods. These methods may be called to return saved
115             # information about the NNTP connection, information about the
116             # package, or to set EOL and debug,
117             ########################################################################
118              
119              
120             # Return version number.
121             sub version {
122 1     1 1 9 my $me = shift;
123              
124             # Get News::NNTPClient::version, if package happens to be
125             # News::NNTPClient.
126 1         2 my $rev = ${ref($me) . "::VERSION"};
  1         6  
127              
128 1         3 $rev;
129             }
130              
131             # With no argument, return debugging level, otherwise set it.
132             sub debug {
133 1     1 1 18 my $me = shift;
134 1         2 my $debug = shift;
135              
136 1 50       10 $me->{DBUG} = $debug if defined $debug;
137              
138 1         3 $me->{DBUG};
139             }
140              
141             # Set EOL
142             sub eol {
143 1     1 1 9 my $me = shift;
144 1         3 my $new = shift;
145 1         2 my $old = $me->{EOL};
146              
147             # Set to new EOL only if passed a value.
148 1 50       4 $me->{EOL} = $new if defined $new;
149              
150 1         3 $old;
151             }
152              
153             # Set GMT
154             sub gmt {
155 2     2 1 13 my $me = shift;
156 2         4 my $new = shift;
157 2         5 my $old = $me->{GMT};
158              
159             # Set to new GMT only if passed a value.
160 2 100       6 $me->{GMT} = $new if defined $new;
161              
162 2         4 $old;
163             }
164              
165             # Set Four digit year flag.
166             sub fourdigityear {
167 3     3 1 78 my $me = shift;
168 3         4 my $new = shift;
169 3         5 my $old = $me->{FDY};
170              
171             # Set to new FDY only if passed a value.
172 3 100       8 $me->{FDY} = $new if defined $new;
173              
174 3         7 $old;
175             }
176              
177             # Return boolean according to code < 400.
178             sub ok {
179 1     1 1 9 my $me = shift;
180              
181             # Codes less than 400 are good.
182 1 50       6 0 < $me->{CODE} and $me->{CODE} < 400;
183             }
184              
185             # Return boolean according to code < 400 and print message if not ok.
186             sub okprint {
187 1     1 1 12 my $me = shift;
188              
189 1 50 33     6 warn "NNTPERROR: $me->{CODE} $me->{MESG}\n"
190             if 400 <= $me->{CODE} and $me->{DBUG};
191              
192             # Codes less than 400 are good.
193 1 50       4 0 < $me->{CODE} and $me->{CODE} < 400;
194             }
195              
196             # Return the most recent message
197             sub message {
198 1     1 1 9 my $me = shift;
199              
200 1         4 "$me->{MESG}$me->{EOL}";
201             }
202              
203             # Return the most recent code
204             sub code {
205 1     1 1 9 my $me = shift;
206              
207 1         12 $me->{CODE};
208             }
209              
210             # Return boolean according to post ok flag.
211             sub postok {
212 1     1 1 9 my $me = shift;
213              
214 1         3 $me->{POST};
215             }
216              
217             ########################################################################
218             # NNTP methods.
219             ########################################################################
220              
221             # Fetch an article.
222             sub article {
223 1     1 1 78 my $me = shift;
224 1   50     7 my $msgid = shift || "";
225              
226 1         39 $me->{CMND} = "fetch";
227 1         5 $me->command("ARTICLE $msgid");
228             }
229              
230             # Fetch body of an article.
231             sub body {
232 1     1 1 71 my $me = shift;
233 1   50     7 my $msgid = shift || "";
234              
235 1         3 $me->{CMND} = "fetch";
236 1         3 $me->command("BODY $msgid");
237             }
238              
239             # Fetch header of an article.
240             sub head {
241 1     1 1 67 my $me = shift;
242 1   50     6 my $msgid = shift || "";
243              
244 1         9 $me->{CMND} = "fetch";
245 1         5 $me->command("HEAD $msgid");
246             }
247              
248             # Fetch status of an article. Return Message-ID if found.
249             sub stat {
250 1     1 1 63 my $me = shift;
251 1   50     7 my $msgid = shift || "";
252              
253 1         3 $me->{CMND} = "msgid";
254 1         5 $me->command("STAT $msgid");
255             }
256              
257             # Move current article pointer backwards. Return Message-ID if found.
258             sub last {
259 1     1 1 75 my $me = shift;
260              
261 1         3 $me->{CMND} = "msgid";
262 1         6 $me->command("LAST");
263             }
264              
265             # Move current article pointer forwards. Return Message-ID if found.
266             sub next {
267 1     1 1 65 my $me = shift;
268              
269 1         3 $me->{CMND} = "msgid";
270 1         3 $me->command("NEXT");
271             }
272              
273             # Set the group.
274             sub group {
275 1     1 1 434 my $me = shift;
276 1   50     5 my $group = shift || "";
277              
278 1         2 $me->{CMND} = "groupinfo";
279 1         5 $me->command("GROUP $group");
280             }
281              
282             # List all groups.
283             sub list {
284 1     1 1 88 my $me = shift;
285 1   50     7 my $type = shift || "";
286 1   50     7 my $pat = shift || "";
287              
288 1         3 $me->{CMND} = "fetch";
289 1         5 $me->command("LIST $type $pat");
290             }
291              
292             # List new groups since date/time.
293             sub newgroups {
294 1     1 1 13 my $me = shift;
295 1         6 my $since = $me->yymmdd_hhmmss(shift);
296              
297 1         5 my $dist = distributions(@_);
298              
299 1         3 $me->{CMND} = "fetch";
300 1         5 $me->command("NEWGROUPS $since $dist");
301             }
302              
303             # List new news since date/time. If first argument is a timestamp
304             # instead of a group, use default group. Otherwise use second
305             # argument for time stamp. Default group is set by the group method,
306             # or is all groups (*) if not set.
307             sub newnews {
308 1     1 1 78 my $me = shift;
309 1         3 my $group = shift;
310 1         2 my $since;
311              
312 1 50       4 if ($group) {
313 1 50       6 if ($group =~ /^[\d ]+/) {
314 1         2 $since = $group;
315 1         2 $group = "";
316             } else {
317 0         0 $since = shift;
318             }
319             }
320              
321 1   50     11 $group ||= $me->{GROUP} || "*";
      33        
322 1         3 $since = $me->yymmdd_hhmmss($since);
323              
324 1         3 my $dist = distributions(@_);
325              
326 1         2 $me->{CMND} = "fetch";
327 1         4 $me->command("NEWNEWS $group $since $dist");
328             }
329              
330             # Get help text.
331             sub help {
332 1     1 1 68 my $me = shift;
333              
334 1         3 $me->{CMND} = "fetch";
335 1         3 $me->command("HELP");
336             }
337              
338             # Post an article.
339             sub post {
340 1     1 1 145 my $me = shift;
341              
342 1 50       3 $me->command("POST") or return;
343              
344 0         0 $me->squirt(@_);
345             }
346              
347             # Transfer an article.
348             sub ihave {
349 0     0 1 0 my $me = shift;
350 0   0     0 my $msgid = shift || "";
351              
352 0 0       0 $me->command("IHAVE $msgid") or return;
353              
354 0         0 $me->squirt(@_);
355             }
356              
357             # Authinfo command
358             sub authinfo {
359 0     0 1 0 my $me = shift;
360 0   0     0 my $user = shift || "guest";
361 0   0     0 my $pass = shift || "foobar";
362              
363 0 0       0 $me->command("AUTHINFO USER $user") && $me->command("AUTHINFO PASS $pass");
364             }
365              
366             # Turn on slave mode, whatever that means.
367             sub slave {
368 1     1 1 65 my $me = shift;
369              
370 1         4 $me->command("SLAVE");
371             }
372              
373             # All done.
374             sub quit {
375 1     1 1 82 my $me = shift;
376              
377 1 50       9 return unless defined fileno $me->{SOCK};
378              
379 0         0 my $ret = $me->command("QUIT");
380              
381 0         0 close $me->{SOCK};
382              
383 0         0 $ret;
384             }
385              
386             sub DESTROY {
387 0     0   0 my $me = shift;
388              
389 0         0 $me->quit;
390             }
391              
392             ########################################################################
393             # Extended NNTP methods. Not all of these are implemented on all
394             # servers.
395             ########################################################################
396              
397             # Mode reader command.
398             sub mode_reader {
399 1     1 1 8 my $me = shift;
400              
401 1         6 $me->command("MODE READER");
402             }
403              
404             # Returns date
405             sub date {
406 1     1 1 81 my $me = shift;
407              
408 1         2 $me->{CMND} = "msg";
409 1         4 $me->command("DATE");
410             }
411              
412             # Return list of article numbers in group.
413             sub listgroup {
414 1     1 1 67 my $me = shift;
415 1   50     9 my $group = shift || "";
416              
417 1         3 $me->{CMND} = "fetch";
418 1         5 $me->command("LISTGROUP $group");
419             }
420              
421             # Get message of the day.
422             sub xmotd {
423 1     1 1 498 my $me = shift;
424 1         4 my $since = $me->yymmdd_hhmmss(shift);
425              
426 1         3 $me->{CMND} = "fetch";
427 1         5 $me->command("XMOTD $since");
428             }
429              
430             # Return titles for newsgroups matching pattern.
431             sub xgtitle {
432 1     1 1 70 my $me = shift;
433 1   50     7 my $group_pattern = shift || "";
434              
435 1         2 $me->{CMND} = "fetch";
436 1         4 $me->command("XGTITLE $group_pattern");
437             }
438              
439             # Return path name for article?
440             sub xpath {
441 1     1 1 70 my $me = shift;
442 1   50     7 my $msgid = shift || "";
443              
444 1         2 $me->{CMND} = "msg";
445 1         5 $me->command("XPATH $msgid");
446             }
447              
448             # Fetch a header for a range of articles. If ARG1 is numeric, use it
449             # as first entry of article range and use Message-ID as the header.
450             # Otherwise ARG1 is header, and ARG2 is first entry of article range.
451             sub xhdr {
452 1     1 1 82 my $me = shift;
453 1   50     7 my $header = shift || "message-id";
454 1   50     5 my $list = shift || 1;
455 1         2 my $last = shift;
456              
457 1 50       4 $list = "$list-$last" if $last;
458              
459 1         9 $me->{CMND} = "fetch";
460 1         5 $me->command("XHDR $header $list");
461             }
462              
463             sub xpat {
464 1     1 1 66 my $me = shift;
465 1   50     7 my $header = shift || "subject";
466 1   50     6 my $list = shift || 1;
467 1         1 my $last = shift;
468 1         2 my $patterns = "";
469            
470 1 50       3 if ($last) {
471 0 0       0 if ($last =~ /^\d+$/) {
472 0         0 $list = "$list-$last";
473             } else {
474 0         0 $patterns = $last;
475             }
476             }
477              
478 1 50       4 $patterns .= @_ ? " @_" : "";
479              
480 1 50       4 $patterns = "*" unless $patterns;
481              
482 1         3 $me->{CMND} = "fetch";
483 1         5 $me->command("XPAT $header $list $patterns");
484             }
485              
486             # Fetch overview for range of articles.
487             sub xover {
488 1     1 1 69 my $me = shift;
489 1   50     8 my $list = shift || 1;
490 1         1 my $last = shift;
491              
492 1 50       4 $list = "$list-$last" if $last;
493              
494 1         2 $me->{CMND} = "fetch";
495 1         5 $me->command("XOVER $list");
496             }
497              
498             # Fetch thread file.
499             sub xthread {
500 1     1 0 72 my $me = shift;
501 1 50       6 my $file = @_ ? "dbinit" : "thread";
502              
503 1         2 $me->{CMND} = "fetchbinary";
504 1         4 $me->command("XTHREAD $file");
505             }
506              
507             # Fetch index
508             sub xindex {
509 1     1 1 64 my $me = shift;
510 1   50     10 my $group = shift || $me->{GROUP} || "";
511              
512 1         3 $me->{CMND} = "fetch";
513 1         4 $me->command("XINDEX $group");
514             }
515              
516             # Search??? Expects search criteria, format unknown.
517             sub xsearch {
518 1     1 1 62 my $me = shift;
519              
520 1 50       95 $me->command("XSEARCH") or return;
521              
522 0         0 $me->squirt(@_);
523             }
524              
525             ########################################################################
526             # Subroutines to implement basic methods.
527             ########################################################################
528              
529             # Send a command.
530             sub cmd {
531 25     25 0 30 my ($me, $cmd) = @_;
532 25         71 local $\ = "\015\012";
533              
534 25         45 my $SOCK = $me->{SOCK};
535              
536 25 50       54 1 < $me->{DBUG} and warn "$SOCK command: $cmd\n";
537              
538 25 50 50     3525 defined fileno $SOCK or
539             carp "News::NNTPClient: $SOCK has been closed\n" and return;
540              
541 0         0 print $SOCK $cmd;
542             }
543              
544             # Send a command and retrieve status. The only reason for not doing
545             # all the work in cmd is so this method can be replaced in a subclass,
546             # and the subclass can call cmd to do the real work.
547             sub command {
548 25     25 0 187 my $me = shift;
549              
550 25 50       73 $me->cmd(@_) or return;
551              
552 0         0 $me->response();
553             }
554              
555             # Like message, but with okprint
556             sub msg {
557 0     0 0 0 my $me = shift;
558              
559 0 0       0 $me->okprint() or return;
560              
561 0         0 $me->{MESG};
562             }
563              
564             # Extract Group info from MESG.
565             sub groupinfo {
566 0     0 0 0 my $me = shift;
567              
568 0         0 $me->{GROUP} = "";
569              
570             # est-articles first-article last-article group-name
571 0 0 0     0 if ($me->okprint and $me->{MESG} =~ /(\d+)\s+(\d+)\s+(\d+)\s+(\w+)/) {
572 0         0 $me->{GROUP} = $4;
573 0 0       0 return wantarray ? ($2, $3) : "$2-$3";
574             }
575              
576 0         0 return;
577             }
578              
579             # Extract Message-ID from MESG.
580             sub msgid {
581 0     0 0 0 my $me = shift;
582              
583 0 0       0 $me->okprint() and $me->{MESG} =~ /(<[^>]+>)/ ? $1 : "";
    0          
584             }
585              
586             # Fetch text from server until single dot.
587             sub fetch {
588 0     0 0 0 my $me = shift;
589 0         0 local $/ = "\012"; # Only use LF to account for possible missing CR
590 0         0 local $_;
591              
592 0 0       0 return unless $me->okprint();
593              
594 0         0 my @lines;
595              
596 0         0 my $SOCK = $me->{SOCK};
597              
598             # Loop reading lines until we receive a line with a single period.
599 0         0 while (<$SOCK>) {
600 0         0 s/\015?\012$/$me->{EOL}/; # Change termination
601              
602 0 0       0 last if $_ eq ".$me->{EOL}";
603              
604 0         0 s/^\.\././; # Fix up escaped dots.
605 0         0 push @lines, $_; # Save each line.
606             }
607              
608 0 0       0 1 < $me->{DBUG} and warn "$SOCK received ${\scalar @lines} lines\n";
  0         0  
609              
610 0 0       0 wantarray ? @lines : \@lines;
611             }
612              
613             # Fetch binary data from server, terminated by: \r\n.\r\n Used by xthread
614             sub fetchbinary {
615 0     0 0 0 my $me = shift;
616 0         0 local $/ = "\015\012.\015\012";
617 0         0 local $_;
618              
619 0 0       0 return unless $me->okprint();
620              
621 0         0 my $SOCK = $me->{SOCK};
622              
623 0         0 my $data = <$SOCK>;
624              
625 0         0 chomp $data;
626              
627 0 0       0 1 < $me->{DBUG} and warn "$SOCK received ${\length $data} bytes\n";
  0         0  
628              
629 0         0 $data;
630             }
631              
632             # Figure out what we should return to sender
633             sub returnval {
634 0     0 0 0 my $me = shift;
635              
636 0   0     0 my $command = $me->{CMND} || "okprint";
637              
638 0         0 $me->{CMND} = ""; # reset after use.
639              
640 0         0 $me->$command();
641             }
642              
643             # Read response to some action (connect, command or squirt)
644             sub response {
645 0     0 0 0 my $me = shift;
646 0         0 local $/ = "\012"; # Only use LF to account for possible missing CR
647 0         0 local $_;
648            
649 0         0 my $SOCK = $me->{SOCK};
650              
651 0         0 $_ = <$SOCK>;
652              
653 0         0 $me->{CODE} = 0;
654 0         0 $me->{MESG} = "";
655              
656 0 0 0     0 defined ($_) or
657             carp "News::NNTPClient unexpected EOF on $SOCK\n" and return;
658              
659 0         0 s/\015?\012$//; # Remove termination
660              
661 0 0       0 if (/^((\d\d)(\d))\s*(.*)/) { # Split out numeric code and message.
662 0 0       0 $me->{POST} = !$3 if $2 == 20;
663 0         0 $me->{CODE} = $1;
664 0         0 $me->{MESG} = $4;
665             } else {
666 0         0 warn "News::NNTPClient garbled response: $_\n";
667 0         0 return;
668             }
669              
670 0 0       0 1 < $me->{DBUG} and warn "$SOCK result($me->{CODE}): $me->{MESG}\n";
671              
672 0         0 $me->returnval();
673             }
674              
675             sub squirt {
676 0     0 0 0 my $me = shift;
677 0         0 local $\ = "\015\012";
678              
679 0         0 my $SOCK = $me->{SOCK};
680              
681 0 0       0 1 < $me->{DBUG} and warn "$SOCK sending ${\scalar @_} lines\n";
  0         0  
682              
683 0         0 foreach (@_) {
684 0         0 local ($_) = $_;
685             # Print each line, possibly prepending a dot for lines
686             # starting with a dot and trimming any trailing \n.
687 0         0 s/^\./../;
688 0         0 s/\n$//;
689 0         0 print $SOCK $_;
690             }
691              
692 0         0 print $SOCK "."; # Terminate message.
693              
694 0 0       0 1 < $me->{DBUG} and warn "$SOCK done sending\n";
695              
696 0         0 $me->response();
697             }
698              
699             # Return time in YYYYMMDD HHMMSS format, for use with newnews and
700             # newgroups commands. If passed a string already in that format, just
701             # return it. Otherwise use localtime() to convert seconds to
702             # date/time. Default is current time.
703             sub yymmdd_hhmmss {
704 3     3 0 5 my $me = shift;
705 3   66     10 my $time = shift || time();
706              
707             # Already in the correct format?
708 3 50       10 return $time if $time =~ /^\d{8}\s+\d{6}(\s*GMT)?$/;
709              
710             # Check for old format.
711 3 50       7 if ($time =~ /^\d{6}\s+\d{6}(\s*GMT)?$/) {
712 0 0       0 carp "Short year in date, using anyway\n" if $me->{FDY};
713 0         0 return $time;
714             }
715              
716             # returns Seconds, Minutes, Hours, days, months - 1, years.
717 3 50       32 my @t = ($me->{GMT} ? gmtime($time) : localtime($time))[0..5];
718              
719 3         4 $t[4]++; # Fix up month
720 3 50       16 if ($me->{FDY}) {
721 0         0 $t[5] += 1900; # Fix up year for 4 digit year.
722             } else {
723 3         7 $t[5] %= 100; # Fix up year for 2 digit year.
724             }
725 3         5 my $fmt = "%.02d" x 3;
726 3 50       188 sprintf "$fmt $fmt%s", reverse(@t), $me->{GMT} ? " GMT" : "";
727             }
728              
729             # Convert list of newsgroup prefixes to distribution list. For
730             # example: comp news -> "". Returns null string if passed
731             # an empty list.
732             sub distributions {
733 2 50 33 2 0 25 @_ and "<" . join(",", @_) . ">" or "";
734             }
735              
736             1;
737              
738             __END__