Source for gnu.inet.nntp.NNTPConnection

   1: /*
   2:  * NNTPConnection.java
   3:  * Copyright (C) 2002 The Free Software Foundation
   4:  * 
   5:  * This file is part of GNU inetlib, a library.
   6:  * 
   7:  * GNU inetlib is free software; you can redistribute it and/or modify
   8:  * it under the terms of the GNU General Public License as published by
   9:  * the Free Software Foundation; either version 2 of the License, or
  10:  * (at your option) any later version.
  11:  * 
  12:  * GNU inetlib is distributed in the hope that it will be useful,
  13:  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14:  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15:  * GNU General Public License for more details.
  16:  * 
  17:  * You should have received a copy of the GNU General Public License
  18:  * along with this library; if not, write to the Free Software
  19:  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  20:  *
  21:  * Linking this library statically or dynamically with other modules is
  22:  * making a combined work based on this library.  Thus, the terms and
  23:  * conditions of the GNU General Public License cover the whole
  24:  * combination.
  25:  *
  26:  * As a special exception, the copyright holders of this library give you
  27:  * permission to link this library with independent modules to produce an
  28:  * executable, regardless of the license terms of these independent
  29:  * modules, and to copy and distribute the resulting executable under
  30:  * terms of your choice, provided that you also meet, for each linked
  31:  * independent module, the terms and conditions of the license of that
  32:  * module.  An independent module is a module which is not derived from
  33:  * or based on this library.  If you modify this library, you may extend
  34:  * this exception to your version of the library, but you are not
  35:  * obliged to do so.  If you do not wish to do so, delete this
  36:  * exception statement from your version.
  37:  */
  38: 
  39: package gnu.inet.nntp;
  40: 
  41: import java.io.BufferedInputStream;
  42: import java.io.BufferedOutputStream;
  43: import java.io.InputStream;
  44: import java.io.IOException;
  45: import java.io.OutputStream;
  46: import java.lang.reflect.Constructor;
  47: import java.net.InetSocketAddress;
  48: import java.net.ProtocolException;
  49: import java.net.Socket;
  50: import java.text.DateFormat;
  51: import java.text.ParseException;
  52: import java.text.SimpleDateFormat;
  53: import java.util.Calendar;
  54: import java.util.Date;
  55: import java.util.GregorianCalendar;
  56: import java.util.Properties;
  57: import java.util.TimeZone;
  58: 
  59: import javax.security.auth.callback.CallbackHandler;
  60: import javax.security.sasl.Sasl;
  61: import javax.security.sasl.SaslClient;
  62: import javax.security.sasl.SaslException;
  63: 
  64: import gnu.inet.util.CRLFInputStream;
  65: import gnu.inet.util.CRLFOutputStream;
  66: import gnu.inet.util.LineInputStream;
  67: import gnu.inet.util.Logger;
  68: import gnu.inet.util.MessageInputStream;
  69: import gnu.inet.util.SaslCallbackHandler;
  70: import gnu.inet.util.SaslInputStream;
  71: import gnu.inet.util.SaslOutputStream;
  72: 
  73: /**
  74:  * An NNTP client.
  75:  * This object is used to establish and manage a connection to an NNTP
  76:  * server.
  77:  *
  78:  * @author <a hef='mailto:dog@gnu.org'>Chris Burdess</a>
  79:  */
  80: public class NNTPConnection
  81:   implements NNTPConstants
  82: {
  83: 
  84:   /**
  85:    * The default NNTP port.
  86:    */
  87:   public static final int DEFAULT_PORT = 119;
  88: 
  89:   /**
  90:    * The hostname of the host we are connected to.
  91:    */
  92:   protected String hostname;
  93: 
  94:   /**
  95:    * The port on the host we are connected to.
  96:    */
  97:   protected int port;
  98: 
  99:   /**
 100:    * The socket used for network communication.
 101:    */
 102:   protected Socket socket;
 103: 
 104:   /**
 105:    * The socket input stream.
 106:    */
 107:   protected LineInputStream in;
 108: 
 109:   /**
 110:    * The socket output stream.
 111:    */
 112:   protected CRLFOutputStream out;
 113: 
 114:   /**
 115:    * Whether the host permits posting of articles.
 116:    */
 117:   protected boolean canPost;
 118: 
 119:   /**
 120:    * The greeting issued by the host when we connected.
 121:    */
 122:   protected String welcome;
 123: 
 124:   /**
 125:    * Pending data, if any.
 126:    */
 127:   protected PendingData pendingData;
 128: 
 129:   /**
 130:    * Whether to log protocol-level information to stderr.
 131:    */
 132:   protected boolean debug;
 133: 
 134:   private static final String DOT = ".";
 135:   private static final String US_ASCII = "US-ASCII";
 136: 
 137:   /**
 138:    * Creates a new connection object.
 139:    * @param hostname the hostname or IP address of the news server
 140:    */
 141:   public NNTPConnection(String hostname)
 142:     throws IOException
 143:   {
 144:     this(hostname, DEFAULT_PORT, 0, 0, false);
 145:   }
 146: 
 147:   /**
 148:    * Creates a new connection object.
 149:    * @param hostname the hostname or IP address of the news server
 150:    * @param port the port to connect to
 151:    */
 152:   public NNTPConnection(String hostname, int port)
 153:     throws IOException
 154:   {
 155:     this(hostname, port, 0, 0, false);
 156:   }
 157: 
 158:   /**
 159:    * Creates a new connection object.
 160:    * @param hostname the hostname or IP address of the news server
 161:    * @param port the port to connect to
 162:    * @param connectionTimeout the socket connection timeout
 163:    * @param timeout the read timeout on the socket
 164:    * @param debug whether to use debugging
 165:    */
 166:   public NNTPConnection(String hostname, int port,
 167:                         int connectionTimeout, int timeout,
 168:                         boolean debug)
 169:     throws IOException
 170:   {
 171:     if (port < 0)
 172:       {
 173:         port = DEFAULT_PORT;
 174:       }
 175:     
 176:     this.hostname = hostname;
 177:     this.port = port;
 178:     this.debug = debug;
 179:     
 180:     // Set up the socket and streams
 181:     socket = new Socket();
 182:     InetSocketAddress address = new InetSocketAddress(hostname, port);
 183:     if (connectionTimeout > 0)
 184:       {
 185:         socket.connect(address, connectionTimeout);
 186:       }
 187:     else
 188:       {
 189:         socket.connect(address);
 190:       }
 191:     if (timeout > 0)
 192:       {
 193:         socket.setSoTimeout(timeout);
 194:       }
 195:     InputStream in = socket.getInputStream();
 196:     in = new CRLFInputStream(in);
 197:     this.in = new LineInputStream(in);
 198:     OutputStream out = socket.getOutputStream();
 199:     out = new BufferedOutputStream(out);
 200:     this.out = new CRLFOutputStream(out);
 201:     
 202:     // Read the welcome message(RFC977:2.4.3)
 203:     StatusResponse response = parseResponse(read());
 204:     switch (response.status)
 205:       {
 206:       case POSTING_ALLOWED:
 207:         canPost = true;
 208:       case NO_POSTING_ALLOWED:
 209:         welcome = response.getMessage();
 210:         break;
 211:       default:
 212:         throw new NNTPException(response);
 213:       }
 214:   }
 215:   
 216:   /**
 217:    * Return the welcome message sent by the server in reply to the initial
 218:    * connection.
 219:    * This message sometimes contains disclaimers or help information that
 220:    * may be relevant to the user.
 221:    */
 222:   public String getWelcome()
 223:   {
 224:     return welcome;
 225:   }
 226:   
 227:   /*
 228:    * Returns an NNTP-format date string.
 229:    * This is only required when clients use the NEWGROUPS or NEWNEWS
 230:    * methods, therefore rarely: we don't cache any of the variables here.
 231:    */
 232:   String formatDate(Date date)
 233:   {
 234:     DateFormat df = new SimpleDateFormat("yyMMdd HHmmss 'GMT'");
 235:     Calendar cal = new GregorianCalendar();
 236:     TimeZone gmt = TimeZone.getTimeZone("GMT");
 237:     cal.setTimeZone(gmt);
 238:     df.setCalendar(cal);
 239:     cal.setTime(date);
 240:     return df.format(date);
 241:   }
 242: 
 243:   /*
 244:    * Parse the specfied NNTP date text.
 245:    */
 246:   Date parseDate(String text)
 247:     throws ParseException
 248:   {
 249:     DateFormat df = new SimpleDateFormat("yyMMdd HHmmss 'GMT'");
 250:     return df.parse(text);
 251:   }
 252: 
 253:   // RFC977:3.1 The ARTICLE, BODY, HEAD, and STAT commands
 254: 
 255:   /**
 256:    * Send an article retrieval request to the server.
 257:    * @param articleNumber the article number of the article to retrieve
 258:    * @return an article response consisting of the article number and
 259:    * message-id, and an iterator over the lines of the article header and
 260:    * body, separated by an empty line
 261:    */
 262:   public ArticleResponse article(int articleNumber)
 263:     throws IOException
 264:   {
 265:     return articleImpl(ARTICLE, Integer.toString(articleNumber));
 266:   }
 267:   
 268:   /**
 269:    * Send an article retrieval request to the server.
 270:    * @param messageId the message-id of the article to retrieve
 271:    * @return an article response consisting of the article number and
 272:    * message-id, and an iterator over the lines of the article header and
 273:    * body, separated by an empty line
 274:    */
 275:   public ArticleResponse article(String messageId)
 276:     throws IOException
 277:   {
 278:     return articleImpl(ARTICLE, messageId);
 279:   }
 280: 
 281:   /**
 282:    * Send an article head retrieval request to the server.
 283:    * @param articleNumber the article number of the article to head
 284:    * @return an article response consisting of the article number and
 285:    * message-id, and an iterator over the lines of the article header
 286:    */
 287:   public ArticleResponse head(int articleNumber)
 288:     throws IOException
 289:   {
 290:     return articleImpl(HEAD, Integer.toString(articleNumber));
 291:   }
 292: 
 293:   /**
 294:    * Send an article head retrieval request to the server.
 295:    * @param messageId the message-id of the article to head
 296:    * @return an article response consisting of the article number and
 297:    * message-id, and an iterator over the lines of the article header
 298:    */
 299:   public ArticleResponse head(String messageId)
 300:     throws IOException
 301:   {
 302:     return articleImpl(HEAD, messageId);
 303:   }
 304: 
 305:   /**
 306:    * Send an article body retrieval request to the server.
 307:    * @param articleNumber the article number of the article to body
 308:    * @return an article response consisting of the article number and
 309:    * message-id, and an iterator over the lines of the article body
 310:    */
 311:   public ArticleResponse body(int articleNumber)
 312:     throws IOException
 313:   {
 314:     return articleImpl(BODY, Integer.toString(articleNumber));
 315:   }
 316: 
 317:   /**
 318:    * Send an article body retrieval request to the server.
 319:    * @param messageId the message-id of the article to body
 320:    * @return an article response consisting of the article number and
 321:    * message-id, and an iterator over the lines of the article body
 322:    */
 323:   public ArticleResponse body(String messageId)
 324:     throws IOException
 325:   {
 326:     return articleImpl(BODY, messageId);
 327:   }
 328: 
 329:   /**
 330:    * Send an article status request to the server.
 331:    * @param articleNumber the article number of the article to stat
 332:    * @return an article response consisting of the article number and
 333:    * message-id
 334:    */
 335:   public ArticleResponse stat(int articleNumber)
 336:     throws IOException
 337:   {
 338:     return articleImpl(STAT, Integer.toString(articleNumber));
 339:   }
 340: 
 341:   /**
 342:    * Send an article status request to the server.
 343:    * @param messageId the message-id of the article to stat
 344:    * @return an article response consisting of the article number and
 345:    * message-id
 346:    */
 347:   public ArticleResponse stat(String messageId)
 348:     throws IOException
 349:   {
 350:     return articleImpl(STAT, messageId);
 351:   }
 352: 
 353:   /**
 354:    * Performs an ARTICLE, BODY, HEAD, or STAT command.
 355:    * @param command one of the above commands
 356:    * @param messageId the article-number or message-id in string form
 357:    */
 358:   protected ArticleResponse articleImpl(String command, String messageId)
 359:     throws IOException
 360:   {
 361:     if (messageId != null)
 362:       {
 363:         StringBuffer line = new StringBuffer(command);
 364:         line.append(' ');
 365:         line.append(messageId);
 366:         send(line.toString());
 367:       }
 368:     else
 369:       {
 370:         send(command);
 371:       }
 372:     StatusResponse response = parseResponse(read());
 373:     switch (response.status)
 374:       {
 375:       case ARTICLE_FOLLOWS:
 376:       case HEAD_FOLLOWS:
 377:       case BODY_FOLLOWS:
 378:         ArticleResponse aresponse = (ArticleResponse) response;
 379:         ArticleStream astream =
 380:           new ArticleStream(new MessageInputStream(in));
 381:         pendingData = astream;
 382:         aresponse.in = astream;
 383:         return aresponse;
 384:       case ARTICLE_RETRIEVED:
 385:         return (ArticleResponse) response;
 386:       default:
 387:         // NO_GROUP_SELECTED
 388:         // NO_ARTICLE_SELECTED
 389:         // NO_SUCH_ARTICLE_NUMBER
 390:         // NO_SUCH_ARTICLE
 391:         // NO_PREVIOUS_ARTICLE
 392:         // NO_NEXT_ARTICLE
 393:         throw new NNTPException(response);
 394:       }
 395:   }
 396:   
 397:   // RFC977:3.2 The GROUP command
 398: 
 399:   /**
 400:    * Send a group selection command to the server.
 401:    * Returns a group status response.
 402:    * @param name the name of the group to select
 403:    */
 404:   public GroupResponse group(String name)
 405:     throws IOException
 406:   {
 407:     send(GROUP + ' ' + name);
 408:     StatusResponse response = parseResponse(read());
 409:     switch (response.status)
 410:       {
 411:       case GROUP_SELECTED:
 412:         return (GroupResponse) response;
 413:       default:
 414:         // NO_SUCH_GROUP
 415:         throw new NNTPException(response);
 416:       }
 417:   }
 418: 
 419:   // RFC977:3.3 The HELP command
 420: 
 421:   /**
 422:    * Requests a help listing.
 423:    * @return an iterator over a collection of help lines.
 424:    */
 425:   public LineIterator help()
 426:     throws IOException
 427:   {
 428:     send(HELP);
 429:     StatusResponse response = parseResponse(read());
 430:     switch (response.status)
 431:       {
 432:       case HELP_TEXT:
 433:         LineIterator li = new LineIterator(this);
 434:         pendingData = li;
 435:         return li;
 436:       default:
 437:         throw new NNTPException(response);
 438:       }
 439:   }
 440:   
 441:   // RFC977:3.4 The IHAVE command
 442: 
 443:   /**
 444:    * Sends an ihave command indicating that the client has an article with
 445:    * the specified message-id.
 446:    * @param messageId the article message-id
 447:    * @return a PostStream if the server wants the specified article, null
 448:    * otherwise
 449:    */
 450:   public PostStream ihave(String messageId)
 451:     throws IOException
 452:   {
 453:     send(IHAVE + ' ' + messageId);
 454:     StatusResponse response = parseResponse(read());
 455:     switch (response.status)
 456:       {
 457:       case SEND_TRANSFER_ARTICLE:
 458:         return new PostStream(this, false);
 459:       case ARTICLE_NOT_WANTED:
 460:         return null;
 461:       default:
 462:         throw new NNTPException(response);
 463:       }
 464:   }
 465: 
 466:   // RFC(77:3.5 The LAST command
 467: 
 468:   /**
 469:    * Sends a previous article positioning command to the server.
 470:    * @return the article number/message-id pair associated with the new
 471:    * article
 472:    */
 473:   public ArticleResponse last()
 474:     throws IOException
 475:   {
 476:     return articleImpl(LAST, null);
 477:   }
 478: 
 479:   // RFC977:3.6 The LIST command
 480: 
 481:   /**
 482:    * Send a group listing command to the server.
 483:    * Returns a GroupIterator. This must be read fully before other commands
 484:    * are issued.
 485:    */
 486:   public GroupIterator list()
 487:     throws IOException
 488:   {
 489:     return listImpl(LIST);
 490:   }
 491:   
 492:   GroupIterator listImpl(String command)
 493:     throws IOException
 494:   {
 495:     send(command);
 496:     StatusResponse response = parseResponse(read());
 497:     switch (response.status)
 498:       {
 499:       case LIST_FOLLOWS:
 500:         GroupIterator gi = new GroupIterator(this);
 501:         pendingData = gi;
 502:         return gi;
 503:       default:
 504:         throw new NNTPException(response);
 505:       }
 506:   }
 507: 
 508:   // RFC977:3.7 The NEWGROUPS command
 509: 
 510:   /**
 511:    * Returns an iterator over the list of new groups on the server since the
 512:    * specified date.
 513:    * NB this method suffers from a minor millenium bug.
 514:    * 
 515:    * @param since the date from which to list new groups
 516:    * @param distributions if non-null, an array of distributions to match
 517:    */
 518:   public LineIterator newGroups(Date since, String[]distributions)
 519:     throws IOException
 520:   {
 521:     StringBuffer buffer = new StringBuffer(NEWGROUPS);
 522:     buffer.append(' ');
 523:     buffer.append(formatDate(since));
 524:     if (distributions != null)
 525:       {
 526:         buffer.append(' ');
 527:         for (int i = 0; i < distributions.length; i++)
 528:           {
 529:             if (i > 0)
 530:               {
 531:                 buffer.append(',');
 532:               }
 533:             buffer.append(distributions[i]);
 534:           }
 535:       }
 536:     send(buffer.toString());
 537:     StatusResponse response = parseResponse(read());
 538:     switch (response.status)
 539:       {
 540:       case NEWGROUPS_LIST_FOLLOWS:
 541:         LineIterator li = new LineIterator(this);
 542:         pendingData = li;
 543:         return li;
 544:       default:
 545:         throw new NNTPException(response);
 546:       }
 547:   }
 548:   
 549:   // RFC977:3.8 The NEWNEWS command
 550: 
 551:   /**
 552:    * Returns an iterator over the list of message-ids posted or received to
 553:    * the specified newsgroup(s) since the specified date.
 554:    * NB this method suffers from a minor millenium bug.
 555:    *
 556:    * @param newsgroup the newsgroup wildmat
 557:    * @param since the date from which to list new articles
 558:    * @param distributions if non-null, a list of distributions to match
 559:    */
 560:   public LineIterator newNews(String newsgroup, Date since,
 561:                               String[] distributions)
 562:     throws IOException
 563:   {
 564:     StringBuffer buffer = new StringBuffer(NEWNEWS);
 565:     buffer.append(' ');
 566:     buffer.append(newsgroup);
 567:     buffer.append(' ');
 568:     buffer.append(formatDate(since));
 569:     if (distributions != null)
 570:       {
 571:         buffer.append(' ');
 572:         for (int i = 0; i < distributions.length; i++)
 573:           {
 574:             if (i > 0)
 575:               {
 576:                 buffer.append(',');
 577:               }
 578:             buffer.append(distributions[i]);
 579:           }
 580:       }
 581:     send(buffer.toString());
 582:     StatusResponse response = parseResponse(read());
 583:     switch (response.status)
 584:       {
 585:       case NEWNEWS_LIST_FOLLOWS:
 586:         LineIterator li = new LineIterator(this);
 587:         pendingData = li;
 588:         return li;
 589:       default:
 590:         throw new NNTPException(response);
 591:       }
 592:   }
 593:   
 594:   // RFC977:3.9 The NEXT command
 595: 
 596:   /**
 597:    * Sends a next article positioning command to the server.
 598:    * @return the article number/message-id pair associated with the new
 599:    * article
 600:    */
 601:   public ArticleResponse next()
 602:     throws IOException
 603:   {
 604:     return articleImpl(NEXT, null);
 605:   }
 606: 
 607:   // RFC977:3.10 The POST command
 608: 
 609:   /**
 610:    * Post an article. This is a two-stage process.
 611:    * If successful, returns an output stream to write the article to.
 612:    * Clients should call <code>write()</code> on the stream for all the
 613:    * bytes of the article, and finally call <code>close()</code>
 614:    * on the stream.
 615:    * No other method should be called in between.
 616:    * @see #postComplete
 617:    */
 618:   public OutputStream post()
 619:     throws IOException
 620:   {
 621:     send(POST);
 622:     StatusResponse response = parseResponse(read());
 623:     switch (response.status)
 624:       {
 625:       case SEND_ARTICLE:
 626:         return new PostStream(this, false);
 627:       default:
 628:         // POSTING_NOT_ALLOWED
 629:         throw new NNTPException(response);
 630:       }
 631:   }
 632:   
 633:   /**
 634:    * Indicates that the client has finished writing all the bytes of the
 635:    * article.
 636:    * Called by the PostStream during <code>close()</code>.
 637:    * @see #post
 638:    */
 639:   void postComplete()
 640:     throws IOException
 641:   {
 642:     send(DOT);
 643:     StatusResponse response = parseResponse(read());
 644:     switch (response.status)
 645:       {
 646:       case ARTICLE_POSTED:
 647:       case ARTICLE_TRANSFERRED:
 648:         return;
 649:       default:
 650:         // POSTING_FAILED
 651:         // TRANSFER_FAILED
 652:         // ARTICLE_REJECTED
 653:         throw new NNTPException(response);
 654:       }
 655:   }
 656: 
 657:   // RFC977:3.11 The QUIT command
 658: 
 659:   /**
 660:    * Close the connection.
 661:    * After calling this method, no further calls on this object are valid.
 662:    */
 663:   public void quit()
 664:     throws IOException
 665:   {
 666:     send(QUIT);
 667:     StatusResponse response = parseResponse(read());
 668:     switch (response.status)
 669:       {
 670:       case CLOSING_CONNECTION:
 671:         socket.close();
 672:         return;
 673:       default:
 674:         throw new NNTPException(response);
 675:       }
 676:   }
 677:   
 678:   // RFC977:3.12 The SLAVE command
 679: 
 680:   /**
 681:    * Indicates to the server that this is a slave connection.
 682:    */
 683:   public void slave()
 684:     throws IOException
 685:   {
 686:     send(SLAVE);
 687:     StatusResponse response = parseResponse(read());
 688:     switch (response.status)
 689:       {
 690:       case SLAVE_ACKNOWLEDGED:
 691:         break;
 692:       default:
 693:         throw new NNTPException(response);
 694:       }
 695:   }
 696:   
 697:   // RFC2980:1.1 The CHECK command
 698: 
 699:   public boolean check(String messageId)
 700:     throws IOException
 701:   {
 702:     StringBuffer buffer = new StringBuffer(CHECK);
 703:     buffer.append(' ');
 704:     buffer.append(messageId);
 705:     send(buffer.toString());
 706:     StatusResponse response = parseResponse(read());
 707:     switch (response.status)
 708:       {
 709:       case SEND_ARTICLE_VIA_TAKETHIS:
 710:         return true;
 711:       case ARTICLE_NOT_WANTED_VIA_TAKETHIS:
 712:         return false;
 713:       default:
 714:         // SERVICE_DISCONTINUED
 715:         // TRY_AGAIN_LATER
 716:         // TRANSFER_PERMISSION_DENIED
 717:         // COMMAND_NOT_RECOGNIZED
 718:         throw new NNTPException(response);
 719:       }
 720:   }
 721: 
 722:   // RFC2980:1.2 The MODE STREAM command
 723: 
 724:   /**
 725:    * Attempt to initialise the connection in streaming mode.
 726:    * This is generally used to bypass the lock step nature of NNTP in order
 727:    * to perform a series of CHECK and TAKETHIS commands.
 728:    *
 729:    * @return true if the server supports streaming mode
 730:    */
 731:   public boolean modeStream()
 732:     throws IOException
 733:   {
 734:     send(MODE_STREAM);
 735:     StatusResponse response = parseResponse(read());
 736:     switch (response.status)
 737:       {
 738:       case STREAMING_OK:
 739:         return true;
 740:       default:
 741:         // COMMAND_NOT_RECOGNIZED
 742:         return false;
 743:       }
 744:   }
 745:   
 746:   // RFC2980:1.3 The TAKETHIS command
 747: 
 748:   /**
 749:    * Implements the out-of-order takethis command.
 750:    * The client uses the returned output stream to write all the bytes of the
 751:    * article. When complete, it calls <code>close()</code> on the
 752:    * stream.
 753:    * @see #takethisComplete
 754:    */
 755:   public OutputStream takethis(String messageId)
 756:     throws IOException
 757:   {
 758:     send(TAKETHIS + ' ' + messageId);
 759:     return new PostStream(this, true);
 760:   }
 761: 
 762:   /**
 763:    * Completes a takethis transaction.
 764:    * Called by PostStream.close().
 765:    * @see #takethis
 766:    */
 767:   void takethisComplete()
 768:     throws IOException
 769:   {
 770:     send(DOT);
 771:     StatusResponse response = parseResponse(read());
 772:     switch (response.status)
 773:       {
 774:       case ARTICLE_TRANSFERRED_OK:
 775:         return;
 776:       default:
 777:         // SERVICE_DISCONTINUED
 778:         // ARTICLE_TRANSFER_FAILED
 779:         // TRANSFER_PERMISSION_DENIED
 780:         // COMMAND_NOT_RECOGNIZED
 781:         throw new NNTPException(response);
 782:       }
 783:   }
 784:   
 785:   // RFC2980:1.4 The XREPLIC command
 786: 
 787:   // TODO
 788: 
 789:   // RFC2980:2.1.2 The LIST ACTIVE command
 790: 
 791:   /**
 792:    * Returns an iterator over the groups specified according to the wildmat
 793:    * pattern. The iterator must be read fully before other commands are
 794:    * issued.
 795:    * @param wildmat the wildmat pattern. If null, returns all groups. If no
 796:    * groups are matched, returns an empty iterator.
 797:    */
 798:   public GroupIterator listActive(String wildmat)
 799:     throws IOException
 800:   {
 801:     StringBuffer buffer = new StringBuffer(LIST_ACTIVE);
 802:     if (wildmat != null)
 803:       {
 804:         buffer.append(' ');
 805:         buffer.append(wildmat);
 806:       }
 807:     return listImpl(buffer.toString());
 808:   }
 809:   
 810:   // RFC2980:2.1.3 The LIST ACTIVE.TIMES command
 811: 
 812:   /**
 813:    * Returns an iterator over the active.times file.
 814:    * Each ActiveTime object returned provides details of who created the
 815:    * newsgroup and when.
 816:    */
 817:   public ActiveTimesIterator listActiveTimes()
 818:     throws IOException
 819:   {
 820:     send(LIST_ACTIVE_TIMES);
 821:     StatusResponse response = parseResponse(read());
 822:     switch (response.status)
 823:       {
 824:       case LIST_FOLLOWS:
 825:         return new ActiveTimesIterator(this);
 826:       default:
 827:         throw new NNTPException(response);
 828:       }
 829:   }
 830: 
 831:   // RFC2980:2.1.4 The LIST DISTRIBUTIONS command
 832: 
 833:   // TODO
 834: 
 835:   // RFC2980:2.1.5 The LIST DISTRIB.PATS command
 836: 
 837:   // TODO
 838: 
 839:   // RFC2980:2.1.6 The LIST NEWSGROUPS command
 840: 
 841:   /**
 842:    * Returns an iterator over the group descriptions for the given groups.
 843:    * @param wildmat if non-null, limits the groups in the iterator to the
 844:    * specified pattern
 845:    * @return an iterator over group name/description pairs
 846:    * @see #xgtitle
 847:    */
 848:   public PairIterator listNewsgroups(String wildmat)
 849:     throws IOException
 850:   {
 851:     StringBuffer buffer = new StringBuffer(LIST_NEWSGROUPS);
 852:     if (wildmat != null)
 853:       {
 854:         buffer.append(' ');
 855:         buffer.append(wildmat);
 856:       }
 857:     send(buffer.toString());
 858:     StatusResponse response = parseResponse(read());
 859:     switch (response.status)
 860:       {
 861:       case LIST_FOLLOWS:
 862:         PairIterator pi = new PairIterator(this);
 863:         pendingData = pi;
 864:         return pi;
 865:       default:
 866:         throw new NNTPException(response);
 867:       }
 868:   }
 869: 
 870:   // RFC2980:2.1.7 The LIST OVERVIEW.FMT command
 871: 
 872:   /**
 873:    * Returns an iterator over the order in which headers are stored in the
 874:    * overview database.
 875:    * Each line returned by the iterator contains one header field.
 876:    * @see #xover
 877:    */
 878:   public LineIterator listOverviewFmt()
 879:     throws IOException
 880:   {
 881:     send(LIST_OVERVIEW_FMT);
 882:     StatusResponse response = parseResponse(read());
 883:     switch (response.status)
 884:       {
 885:       case LIST_FOLLOWS:
 886:         LineIterator li = new LineIterator(this);
 887:         pendingData = li;
 888:         return li;
 889:       default:
 890:         throw new NNTPException(response);
 891:       }
 892:   }
 893:   
 894:   // RFC2980:2.1.8 The LIST SUBSCRIPTIONS command
 895: 
 896:   /**
 897:    * Returns a list of newsgroups suitable for new users of the server.
 898:    */
 899:   public GroupIterator listSubscriptions()
 900:     throws IOException
 901:   {
 902:     return listImpl(LIST_SUBSCRIPTIONS);
 903:   }
 904: 
 905:   // RFC2980:2.2 The LISTGROUP command
 906: 
 907:   /**
 908:    * Returns a listing of all the article numbers in the specified
 909:    * newsgroup. If the <code>group</code> parameter is null, the currently
 910:    * selected group is assumed.
 911:    * @param group the name of the group to list articles for
 912:    */
 913:   public ArticleNumberIterator listGroup(String group)
 914:     throws IOException
 915:   {
 916:     StringBuffer buffer = new StringBuffer(LISTGROUP);
 917:     if (group != null)
 918:       {
 919:         buffer.append(' ');
 920:         buffer.append(group);
 921:       }
 922:     send(buffer.toString());
 923:     StatusResponse response = parseResponse(read(), true);
 924:     switch (response.status)
 925:       {
 926:       case GROUP_SELECTED:
 927:         ArticleNumberIterator ani = new ArticleNumberIterator(this);
 928:         pendingData = ani;
 929:         return ani;
 930:       default:
 931:         throw new NNTPException(response);
 932:       }
 933:   }
 934: 
 935:   // RFC2980:2.3 The MODE READER command
 936: 
 937:   /**
 938:    * Indicates to the server that this is a user-agent.
 939:    * @return true if posting is allowed, false otherwise.
 940:    */
 941:   public boolean modeReader()
 942:     throws IOException
 943:   {
 944:     send(MODE_READER);
 945:     StatusResponse response = parseResponse(read());
 946:     switch (response.status)
 947:       {
 948:       case POSTING_ALLOWED:
 949:         canPost = true;
 950:         return canPost;
 951:       case POSTING_NOT_ALLOWED:
 952:         canPost = false;
 953:         return canPost;
 954:       default:
 955:         throw new NNTPException(response);
 956:       }
 957:   }
 958:   
 959:   // RFC2980:2.4 The XGTITLE command
 960: 
 961:   /**
 962:    * Returns an iterator over the list of newsgroup descriptions.
 963:    * @param wildmat if non-null, the newsgroups to match
 964:    */
 965:   public PairIterator xgtitle(String wildmat)
 966:     throws IOException
 967:   {
 968:     StringBuffer buffer = new StringBuffer(XGTITLE);
 969:     if (wildmat != null)
 970:       {
 971:         buffer.append(' ');
 972:         buffer.append(wildmat);
 973:       }
 974:     send(buffer.toString());
 975:     StatusResponse response = parseResponse(read());
 976:     switch (response.status)
 977:       {
 978:       case XGTITLE_LIST_FOLLOWS:
 979:         PairIterator pi = new PairIterator(this);
 980:         pendingData = pi;
 981:         return pi;
 982:       default:
 983:         throw new NNTPException(response);
 984:       }
 985:   }
 986:   
 987:   // RFC2980:2.6 The XHDR command
 988: 
 989:   public HeaderIterator xhdr(String header, String range)
 990:     throws IOException
 991:   {
 992:     StringBuffer buffer = new StringBuffer(XHDR);
 993:     buffer.append(' ');
 994:     buffer.append(header);
 995:     if (range != null)
 996:       {
 997:         buffer.append(' ');
 998:         buffer.append(range);
 999:       }
1000:     send(buffer.toString());
1001:     StatusResponse response = parseResponse(read());
1002:     switch (response.status)
1003:       {
1004:       case HEAD_FOLLOWS:
1005:         HeaderIterator hi = new HeaderIterator(this);
1006:         pendingData = hi;
1007:         return hi;
1008:       default:
1009:         // NO_GROUP_SELECTED
1010:         // NO_SUCH_ARTICLE
1011:         throw new NNTPException(response);
1012:       }
1013:   }
1014: 
1015:   // RFC2980:2.7 The XINDEX command
1016: 
1017:   // TODO
1018: 
1019:   // RFC2980:2.8 The XOVER command
1020: 
1021:   public OverviewIterator xover(Range range)
1022:     throws IOException
1023:   {
1024:     StringBuffer buffer = new StringBuffer(XOVER);
1025:     if (range != null)
1026:       {
1027:         buffer.append(' ');
1028:         buffer.append(range.toString());
1029:       }
1030:     send(buffer.toString());
1031:     StatusResponse response = parseResponse(read());
1032:     switch (response.status)
1033:       {
1034:       case OVERVIEW_FOLLOWS:
1035:         OverviewIterator oi = new OverviewIterator(this);
1036:         pendingData = oi;
1037:         return oi;
1038:       default:
1039:         // NO_GROUP_SELECTED
1040:         // PERMISSION_DENIED
1041:         throw new NNTPException(response);
1042:       }
1043:   }
1044:   
1045:   // RFC2980:2.9 The XPAT command
1046: 
1047:   // TODO
1048: 
1049:   // RFC2980:2.10 The XPATH command
1050: 
1051:   // TODO
1052: 
1053:   // RFC2980:2.11 The XROVER command
1054: 
1055:   // TODO
1056: 
1057:   // RFC2980:2.12 The XTHREAD command
1058: 
1059:   // TODO
1060: 
1061:   // RFC2980:3.1.1 Original AUTHINFO
1062: 
1063:   /**
1064:    * Basic authentication strategy.
1065:    * @param username the user to authenticate
1066:    * @param password the(cleartext) password
1067:    * @return true on success, false on failure
1068:    */
1069:   public boolean authinfo(String username, String password)
1070:     throws IOException
1071:   {
1072:     StringBuffer buffer = new StringBuffer(AUTHINFO_USER);
1073:     buffer.append(' ');
1074:     buffer.append(username);
1075:     send(buffer.toString());
1076:     StatusResponse response = parseResponse(read());
1077:     switch (response.status)
1078:       {
1079:       case AUTHINFO_OK:
1080:         return true;
1081:       case SEND_AUTHINFOPASS:
1082:         buffer.setLength(0);
1083:         buffer.append(AUTHINFO_PASS);
1084:         buffer.append(' ');
1085:         buffer.append(password);
1086:         send(buffer.toString());
1087:         response = parseResponse(read());
1088:         switch (response.status)
1089:           {
1090:           case AUTHINFO_OK:
1091:             return true;
1092:           case PERMISSION_DENIED:
1093:             return false;
1094:           default:
1095:             throw new NNTPException(response);
1096:           }
1097:       default:
1098:         // AUTHINFO_REJECTED
1099:         throw new NNTPException(response);
1100:       }
1101:   }
1102: 
1103:   // RFC2980:3.1.2 AUTHINFO SIMPLE
1104: 
1105:   /**
1106:    * Implementation of NNTP simple authentication.
1107:    * Note that use of this authentication strategy is highly deprecated,
1108:    * only use on servers that won't accept any other form of authentication.
1109:    */
1110:   public boolean authinfoSimple(String username, String password)
1111:     throws IOException
1112:   {
1113:     send(AUTHINFO_SIMPLE);
1114:     StatusResponse response = parseResponse(read());
1115:     switch (response.status)
1116:       {
1117:       case SEND_AUTHINFO_SIMPLE:
1118:         StringBuffer buffer = new StringBuffer(username);
1119:         buffer.append(' ');
1120:         buffer.append(password);
1121:         send(buffer.toString());
1122:         response = parseResponse(read());
1123:         switch (response.status)
1124:           {
1125:           case AUTHINFO_SIMPLE_OK:
1126:             return true;
1127:           case AUTHINFO_SIMPLE_DENIED:
1128:             return false;
1129:           default:throw new NNTPException(response);
1130:           }
1131:       default:
1132:         throw new NNTPException(response);
1133:       }
1134:   }
1135:   
1136:   // RFC2980:3.1.3 AUTHINFO GENERIC
1137: 
1138:   /**
1139:    * Authenticates the connection using the specified SASL mechanism,
1140:    * username and password.
1141:    * @param mechanism a SASL authentication mechanism, e.g. LOGIN, PLAIN,
1142:    * CRAM-MD5, GSSAPI
1143:    * @param username the authentication principal
1144:    * @param password the authentication credentials
1145:    */
1146:   public boolean authinfoGeneric(String mechanism,
1147:                                   String username, String password)
1148:     throws IOException
1149:   {
1150:     String[] m = new String[] { mechanism };
1151:     CallbackHandler ch = new SaslCallbackHandler(username, password);
1152:     // Avoid lengthy callback procedure for GNU Crypto
1153:     Properties p = new Properties();
1154:     p.put("gnu.crypto.sasl.username", username);
1155:     p.put("gnu.crypto.sasl.password", password);
1156:     SaslClient sasl =
1157:       Sasl.createSaslClient(m, null, "smtp",
1158:                              socket.getInetAddress().getHostName(),
1159:                              p, ch);
1160:     if (sasl == null)
1161:       {
1162:         return false;
1163:       }
1164:     
1165:     StringBuffer cmd = new StringBuffer(AUTHINFO_GENERIC);
1166:     cmd.append(' ');
1167:     cmd.append(mechanism);
1168:     if (sasl.hasInitialResponse())
1169:       {
1170:         cmd.append(' ');
1171:         byte[] init = sasl.evaluateChallenge(new byte[0]);
1172:         cmd.append(new String(init, "US-ASCII"));
1173:       }
1174:     send(cmd.toString());
1175:     StatusResponse response = parseResponse(read());
1176:     switch (response.status)
1177:       {
1178:       case AUTHINFO_OK:
1179:         String qop = (String) sasl.getNegotiatedProperty(Sasl.QOP);
1180:         if ("auth-int".equalsIgnoreCase(qop)
1181:             || "auth-conf".equalsIgnoreCase(qop))
1182:           {
1183:             InputStream in = socket.getInputStream();
1184:             in = new BufferedInputStream(in);
1185:             in = new SaslInputStream(sasl, in);
1186:             in = new CRLFInputStream(in);
1187:             this.in = new LineInputStream(in);
1188:             OutputStream out = socket.getOutputStream();
1189:             out = new BufferedOutputStream(out);
1190:             out = new SaslOutputStream(sasl, out);
1191:             this.out = new CRLFOutputStream(out);
1192:           }
1193:         return true;
1194:       case PERMISSION_DENIED:
1195:         return false;
1196:       case COMMAND_NOT_RECOGNIZED:
1197:       case SYNTAX_ERROR:
1198:       case INTERNAL_ERROR:
1199:       default:
1200:         throw new NNTPException(response);
1201:         // FIXME how does the server send continuations?
1202:       }
1203:   }
1204:   
1205:   // RFC2980:3.2 The DATE command
1206: 
1207:   /**
1208:    * Returns the date on the server.
1209:    */
1210:   public Date date()
1211:     throws IOException
1212:   {
1213:     send(DATE);
1214:     StatusResponse response = parseResponse(read());
1215:     switch (response.status)
1216:       {
1217:       case DATE_OK:
1218:         String message = response.getMessage();
1219:         try
1220:           {
1221:             DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
1222:             return df.parse(message);
1223:           }
1224:         catch (ParseException e)
1225:           {
1226:             throw new IOException("Invalid date: " + message);
1227:           }
1228:       default:
1229:         throw new NNTPException(response);
1230:       }
1231:   }
1232: 
1233:   // -- Utility functions --
1234: 
1235:   /**
1236:    * Parse a response object from a response line sent by the server.
1237:    */
1238:   protected StatusResponse parseResponse(String line)
1239:     throws ProtocolException
1240:   {
1241:     return parseResponse(line, false);
1242:   }
1243:   
1244:   /**
1245:    * Parse a response object from a response line sent by the server.
1246:    * @param isListGroup whether we are invoking the LISTGROUP command
1247:    */
1248:   protected StatusResponse parseResponse(String line, boolean isListGroup)
1249:     throws ProtocolException
1250:   {
1251:     if (line == null)
1252:       {
1253:         throw new ProtocolException(hostname + " closed connection");
1254:       }
1255:     int start = 0, end;
1256:     short status = -1;
1257:     String statusText = line;
1258:     String message = null;
1259:     end = line.indexOf(' ', start);
1260:     if (end > start)
1261:       {
1262:         statusText = line.substring(start, end);
1263:         message = line.substring(end + 1);
1264:       }
1265:     try
1266:       {
1267:         status = Short.parseShort(statusText);
1268:       }
1269:     catch (NumberFormatException e)
1270:       {
1271:         throw new ProtocolException(line);
1272:       }
1273:     StatusResponse response;
1274:     switch (status)
1275:       {
1276:       case ARTICLE_FOLLOWS:
1277:       case HEAD_FOLLOWS:
1278:       case BODY_FOLLOWS:
1279:       case ARTICLE_RETRIEVED:
1280:       case GROUP_SELECTED:
1281:         /* The LISTGROUP command returns a list of articles with a 211,
1282:          * instead of the newsgroup totals returned with the GROUP command.
1283:          * Check for this case. */
1284:         if (status != GROUP_SELECTED || isListGroup)
1285:           {
1286:             try
1287:               {
1288:                 ArticleResponse aresponse =
1289:                   new ArticleResponse(status, message);
1290:                 // article number
1291:                 start = end + 1;
1292:                 end = line.indexOf(' ', start);
1293:                 if (end > start)
1294:                   {
1295:                     aresponse.articleNumber =
1296:                       Integer.parseInt(line.substring(start, end));
1297:                   }
1298:                 // message-id
1299:                 start = end + 1;
1300:                 end = line.indexOf(' ', start);
1301:                 if (end > start)
1302:                   {
1303:                     aresponse.messageId = line.substring(start, end);
1304:                   }
1305:                 else
1306:                   {
1307:                     aresponse.messageId = line.substring(start);
1308:                   }
1309:                 response = aresponse;
1310:               }
1311:             catch (NumberFormatException e)
1312:               {
1313:                 // This will happen for XHDR
1314:                 response = new StatusResponse(status, message);
1315:               }
1316:             break;
1317:           }
1318:         // This is the normal case for GROUP_SELECTED
1319:         GroupResponse gresponse = new GroupResponse(status, message);
1320:         try
1321:           {
1322:             // count
1323:             start = end + 1;
1324:             end = line.indexOf(' ', start);
1325:             if (end > start)
1326:               {
1327:                 gresponse.count =
1328:                   Integer.parseInt(line.substring(start, end));
1329:               }
1330:             // first
1331:             start = end + 1;
1332:             end = line.indexOf(' ', start);
1333:             if (end > start)
1334:               {
1335:                 gresponse.first =
1336:                   Integer.parseInt(line.substring(start, end));
1337:               }
1338:             // last
1339:             start = end + 1;
1340:             end = line.indexOf(' ', start);
1341:             if (end > start)
1342:               {
1343:                 gresponse.last =
1344:                   Integer.parseInt(line.substring(start, end));
1345:               }
1346:             // group
1347:             start = end + 1;
1348:             end = line.indexOf(' ', start);
1349:             if (end > start)
1350:               {
1351:                 gresponse.group = line.substring(start, end);
1352:               }
1353:             else
1354:               {
1355:                 gresponse.group = line.substring(start);
1356:               }
1357:           }
1358:         catch (NumberFormatException e)
1359:           {
1360:             throw new ProtocolException(line);
1361:           }
1362:         response = gresponse;
1363:         break;
1364:       default:
1365:         response = new StatusResponse(status, message);
1366:       }
1367:     return response;
1368:   }
1369:   
1370:   /**
1371:    * Send a single line to the server.
1372:    * @param line the line to send
1373:    */
1374:   protected void send(String line)
1375:     throws IOException
1376:   {
1377:     if (pendingData != null)
1378:       {
1379:         // Clear pending data
1380:         pendingData.readToEOF();
1381:         pendingData = null;
1382:       }
1383:     if (debug)
1384:       {
1385:         Logger logger = Logger.getInstance();
1386:         logger.log("nntp", ">" + line);
1387:       }
1388:     byte[] data = line.getBytes(US_ASCII);
1389:     out.write(data);
1390:     out.writeln();
1391:     out.flush();
1392:   }
1393:   
1394:   /**
1395:    * Read a single line from the server.
1396:    * @return a line of text
1397:    */
1398:   protected String read()
1399:     throws IOException
1400:   {
1401:     String line = in.readLine();
1402:     if (debug)
1403:       {
1404:         Logger logger = Logger.getInstance();
1405:         if (line == null)
1406:           {
1407:             logger.log("nntp", "<EOF");
1408:           }
1409:         else
1410:           {
1411:             logger.log("nntp", "<" + line);
1412:           }
1413:       }
1414:     return line;
1415:   }
1416:   
1417: }