Source for gnu.inet.smtp.SMTPConnection

   1: /*
   2:  * SMTPConnection.java
   3:  * Copyright (C) 2003 Chris Burdess <dog@gnu.org>
   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.smtp;
  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.net.InetSocketAddress;
  47: import java.net.ProtocolException;
  48: import java.net.Socket;
  49: import java.security.GeneralSecurityException;
  50: import java.util.ArrayList;
  51: import java.util.Collections;
  52: import java.util.List;
  53: import java.util.Properties;
  54: 
  55: import javax.net.ssl.SSLContext;
  56: import javax.net.ssl.SSLSocket;
  57: import javax.net.ssl.SSLSocketFactory;
  58: import javax.net.ssl.TrustManager;
  59: 
  60: import javax.security.auth.callback.CallbackHandler;
  61: import javax.security.sasl.Sasl;
  62: import javax.security.sasl.SaslClient;
  63: import javax.security.sasl.SaslException;
  64: 
  65: import gnu.inet.util.BASE64;
  66: import gnu.inet.util.CRLFInputStream;
  67: import gnu.inet.util.CRLFOutputStream;
  68: import gnu.inet.util.EmptyX509TrustManager;
  69: import gnu.inet.util.LineInputStream;
  70: import gnu.inet.util.Logger;
  71: import gnu.inet.util.MessageOutputStream;
  72: import gnu.inet.util.SaslCallbackHandler;
  73: import gnu.inet.util.SaslCramMD5;
  74: import gnu.inet.util.SaslInputStream;
  75: import gnu.inet.util.SaslLogin;
  76: import gnu.inet.util.SaslOutputStream;
  77: import gnu.inet.util.SaslPlain;
  78: 
  79: /**
  80:  * An SMTP client.
  81:  * This implements RFC 2821.
  82:  *
  83:  * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
  84:  */
  85: public class SMTPConnection
  86: {
  87: 
  88:   /**
  89:    * The default SMTP port.
  90:    */
  91:   public static final int DEFAULT_PORT = 25;
  92: 
  93:   protected static final String MAIL_FROM = "MAIL FROM:";
  94:   protected static final String RCPT_TO = "RCPT TO:";
  95:   protected static final String SP = " ";
  96:   protected static final String DATA = "DATA";
  97:   protected static final String FINISH_DATA = "\n.";
  98:   protected static final String RSET = "RSET";
  99:   protected static final String VRFY = "VRFY";
 100:   protected static final String EXPN = "EXPN";
 101:   protected static final String HELP = "HELP";
 102:   protected static final String NOOP = "NOOP";
 103:   protected static final String QUIT = "QUIT";
 104:   protected static final String HELO = "HELO";
 105:   protected static final String EHLO = "EHLO";
 106:   protected static final String AUTH = "AUTH";
 107:   protected static final String STARTTLS = "STARTTLS";
 108: 
 109:   protected static final int INFO = 214;
 110:   protected static final int READY = 220;
 111:   protected static final int OK = 250;
 112:   protected static final int OK_NOT_LOCAL = 251;
 113:   protected static final int OK_UNVERIFIED = 252;
 114:   protected static final int SEND_DATA = 354;
 115:   protected static final int AMBIGUOUS = 553;
 116: 
 117:   /**
 118:    * The underlying socket used for communicating with the server.
 119:    */
 120:   protected Socket socket;
 121: 
 122:   /**
 123:    * The input stream used to read responses from the server.
 124:    */
 125:   protected LineInputStream in;
 126: 
 127:   /**
 128:    * The output stream used to send commands to the server.
 129:    */
 130:   protected CRLFOutputStream out;
 131: 
 132:   /**
 133:    * If true, log events.
 134:    */
 135:   protected boolean debug;
 136: 
 137:   /**
 138:    * The last response message received from the server.
 139:    */
 140:   protected String response;
 141: 
 142:   /**
 143:    * If true, there are more responses to read.
 144:    */
 145:   protected boolean continuation;
 146: 
 147:   /**
 148:    * The greeting message given by the server.
 149:    */
 150:   protected final String greeting;
 151: 
 152:   /**
 153:    * Creates a new connection to the specified host, using the default SMTP
 154:    * port.
 155:    * @param host the server hostname
 156:    */
 157:   public SMTPConnection(String host)
 158:     throws IOException
 159:   {
 160:     this(host, DEFAULT_PORT);
 161:   }
 162: 
 163:   /**
 164:    * Creates a new connection to the specified host, using the specified
 165:    * port.
 166:    * @param host the server hostname
 167:    * @param port the port to connect to
 168:    */
 169:   public SMTPConnection(String host, int port)
 170:     throws IOException
 171:   {
 172:     this(host, port, 0, 0, false);
 173:   }
 174: 
 175:   /**
 176:    * Creates a new connection to the specified host, using the specified
 177:    * port.
 178:    * @param host the server hostname
 179:    * @param port the port to connect to
 180:    * @param connectionTimeout the connection timeout in milliseconds
 181:    * @param timeout the I/O timeout in milliseconds
 182:    * @param debug whether to log progress
 183:    */
 184:   public SMTPConnection(String host, int port,
 185:                         int connectionTimeout, int timeout, boolean debug)
 186:     throws IOException
 187:   {
 188:     if (port <= 0)
 189:       {
 190:         port = DEFAULT_PORT;
 191:       }
 192:     response = null;
 193:     continuation = false;
 194:     this.debug = debug;
 195:     
 196:     // Initialise socket
 197:     socket = new Socket();
 198:     InetSocketAddress address = new InetSocketAddress(host, port);
 199:     if (connectionTimeout > 0)
 200:       {
 201:         socket.connect(address, connectionTimeout);
 202:       }
 203:     else
 204:       {
 205:         socket.connect(address);
 206:       }
 207:     if (timeout > 0)
 208:       {
 209:         socket.setSoTimeout(timeout);
 210:       }
 211:     
 212:     // Initialise streams
 213:     InputStream in = socket.getInputStream();
 214:     in = new BufferedInputStream(in);
 215:     in = new CRLFInputStream(in);
 216:     this.in = new LineInputStream(in);
 217:     OutputStream out = socket.getOutputStream();
 218:     out = new BufferedOutputStream(out);
 219:     this.out = new CRLFOutputStream(out);
 220:     
 221:     // Greeting
 222:     StringBuffer greetingBuffer = new StringBuffer();
 223:     boolean notFirst = false;
 224:     do
 225:       {
 226:         if (getResponse() != READY)
 227:           {
 228:             throw new ProtocolException(response);
 229:           }
 230:         if (notFirst)
 231:           {
 232:             greetingBuffer.append('\n');
 233:           }
 234:         else
 235:           {
 236:             notFirst = true;
 237:           }
 238:         greetingBuffer.append(response);
 239:         
 240:       }
 241:     while (continuation);
 242:     greeting = greetingBuffer.toString();
 243:   }
 244:   
 245:   /**
 246:    * Returns the server greeting message.
 247:    */
 248:   public String getGreeting()
 249:   {
 250:     return greeting;
 251:   }
 252: 
 253:   /**
 254:    * Returns the text of the last response received from the server.
 255:    */
 256:   public String getLastResponse()
 257:   {
 258:     return response;
 259:   }
 260: 
 261:   // -- 3.3 Mail transactions --
 262: 
 263:   /**
 264:    * Execute a MAIL command.
 265:    * @param reversePath the source mailbox(from address)
 266:    * @param parameters optional ESMTP parameters
 267:    * @return true if accepted, false otherwise
 268:    */
 269:   public boolean mailFrom(String reversePath, ParameterList parameters)
 270:     throws IOException
 271:   {
 272:     StringBuffer command = new StringBuffer(MAIL_FROM);
 273:     command.append('<');
 274:     command.append(reversePath);
 275:     command.append('>');
 276:     if (parameters != null)
 277:       {
 278:         command.append(SP);
 279:         command.append(parameters);
 280:       }
 281:     send(command.toString());
 282:     switch (getAllResponses())
 283:       {
 284:       case OK:
 285:       case OK_NOT_LOCAL:
 286:       case OK_UNVERIFIED:
 287:         return true;
 288:       default:
 289:         return false;
 290:       }
 291:   }
 292: 
 293:   /**
 294:    * Execute a RCPT command.
 295:    * @param forwardPath the forward-path(recipient address)
 296:    * @param parameters optional ESMTP parameters
 297:    * @return true if successful, false otherwise
 298:    */
 299:   public boolean rcptTo(String forwardPath, ParameterList parameters)
 300:     throws IOException
 301:   {
 302:     StringBuffer command = new StringBuffer(RCPT_TO);
 303:     command.append('<');
 304:     command.append(forwardPath);
 305:     command.append('>');
 306:     if (parameters != null)
 307:       {
 308:         command.append(SP);
 309:         command.append(parameters);
 310:       }
 311:     send(command.toString());
 312:     switch (getAllResponses())
 313:       {
 314:       case OK:
 315:       case OK_NOT_LOCAL:
 316:       case OK_UNVERIFIED:
 317:         return true;
 318:       default:
 319:         return false;
 320:       }
 321:   }
 322: 
 323:   /**
 324:    * Requests an output stream to write message data to.
 325:    * When the entire message has been written to the stream, the
 326:    * <code>flush</code> method must be called on the stream. Until then no
 327:    * further methods should be called on the connection.
 328:    * Immediately after this procedure is complete, <code>finishData</code>
 329:    * must be called to complete the transfer and determine its success.
 330:    * @return a stream for writing messages to
 331:    */
 332:   public OutputStream data()
 333:     throws IOException
 334:   {
 335:     send(DATA);
 336:     switch (getAllResponses())
 337:       {
 338:       case SEND_DATA:
 339:         return new MessageOutputStream(out);
 340:       default:
 341:         throw new ProtocolException(response);
 342:       }
 343:   }
 344: 
 345:   /**
 346:    * Completes the DATA procedure.
 347:    * @see #data
 348:    * @return true id transfer was successful, false otherwise
 349:    */
 350:   public boolean finishData()
 351:     throws IOException
 352:   {
 353:     send(FINISH_DATA);
 354:     switch (getAllResponses())
 355:       {
 356:       case OK:
 357:         return true;
 358:       default:
 359:         return false;
 360:       }
 361:   }
 362: 
 363:   /**
 364:    * Aborts the current mail transaction.
 365:    */
 366:   public void rset()
 367:     throws IOException
 368:   {
 369:     send(RSET);
 370:     if (getAllResponses() != OK)
 371:       {
 372:         throw new ProtocolException(response);
 373:       }
 374:   }
 375: 
 376:   // -- 3.5 Commands for Debugging Addresses --
 377: 
 378:   /**
 379:    * Returns a list of valid possibilities for the specified address, or
 380:    * null on failure.
 381:    * @param address a mailbox, or real name and mailbox
 382:    */
 383:   public List vrfy(String address)
 384:     throws IOException
 385:   {
 386:     String command = VRFY + ' ' + address;
 387:     send(command);
 388:     List list = new ArrayList();
 389:     do
 390:       {
 391:         switch (getResponse())
 392:           {
 393:           case OK:
 394:           case AMBIGUOUS:
 395:             response = response.trim();
 396:             if (response.indexOf('@') != -1)
 397:               {
 398:                 list.add(response);
 399:               }
 400:             else if (response.indexOf('<') != -1)
 401:               {
 402:                 list.add(response);
 403:               }
 404:             else if (response.indexOf(' ') == -1)
 405:               {
 406:                 list.add(response);
 407:               }
 408:             break;
 409:           default:
 410:             return null;
 411:           }
 412:       }
 413:     while (continuation);
 414:     return Collections.unmodifiableList(list);
 415:   }
 416: 
 417:   /**
 418:    * Returns a list of valid possibilities for the specified mailing list,
 419:    * or null on failure.
 420:    * @param address a mailing list name
 421:    */
 422:   public List expn(String address)
 423:     throws IOException
 424:   {
 425:     String command = EXPN + ' ' + address;
 426:     send(command);
 427:     List list = new ArrayList();
 428:     do
 429:       {
 430:         switch (getResponse())
 431:           {
 432:           case OK:
 433:             response = response.trim();
 434:             list.add(response);
 435:             break;
 436:           default:
 437:             return null;
 438:           }
 439:       }
 440:     while (continuation);
 441:     return Collections.unmodifiableList(list);
 442:   }
 443: 
 444:   /**
 445:    * Returns some useful information about the specified parameter.
 446:    * Typically this is a command.
 447:    * @param arg the context of the query, or null for general information
 448:    * @return a list of possibly useful information, or null if the command
 449:    * failed.
 450:    */
 451:   public List help(String arg)
 452:     throws IOException
 453:   {
 454:     String command = (arg == null) ? HELP :
 455:       HELP + ' ' + arg;
 456:     send(command);
 457:     List list = new ArrayList();
 458:     do
 459:       {
 460:         switch (getResponse())
 461:           {
 462:           case INFO:
 463:             list.add(response);
 464:             break;
 465:           default:
 466:             return null;
 467:           }
 468:       }
 469:     while (continuation);
 470:     return Collections.unmodifiableList(list);
 471:   }
 472: 
 473:   /**
 474:    * Issues a NOOP command.
 475:    * This does nothing, but can be used to keep the connection alive.
 476:    */
 477:   public void noop()
 478:     throws IOException
 479:   {
 480:     send(NOOP);
 481:     getAllResponses();
 482:   }
 483: 
 484:   /**
 485:    * Close the connection to the server.
 486:    */
 487:   public void quit()
 488:     throws IOException
 489:   {
 490:     try
 491:       {
 492:         send(QUIT);
 493:         getAllResponses();
 494:         /* RFC 2821 states that the server MUST send an OK reply here, but
 495:          * many don't: postfix, for instance, sends 221.
 496:          * In any case we have done our best. */
 497:       }
 498:     catch (IOException e)
 499:       {
 500:       }
 501:     finally
 502:       {
 503:         // Close the socket anyway.
 504:         socket.close();
 505:       }
 506:   }
 507: 
 508:   /**
 509:    * Issues a HELO command.
 510:    * @param hostname the local host name
 511:    */
 512:   public boolean helo(String hostname)
 513:     throws IOException
 514:   {
 515:     String command = HELO + ' ' + hostname;
 516:     send(command);
 517:     return (getAllResponses() == OK);
 518:   }
 519: 
 520:   /**
 521:    * Issues an EHLO command.
 522:    * If successful, returns a list of the SMTP extensions supported by the
 523:    * server.
 524:    * Otherwise returns null, and HELO should be called.
 525:    * @param hostname the local host name
 526:    */
 527:   public List ehlo(String hostname)
 528:     throws IOException
 529:   {
 530:     String command = EHLO + ' ' + hostname;
 531:     send(command);
 532:     List extensions = new ArrayList();
 533:     do
 534:       {
 535:         switch (getResponse())
 536:           {
 537:           case OK:
 538:             extensions.add(response);
 539:             break;
 540:           default:
 541:             return null;
 542:           }
 543:       }
 544:     while (continuation);
 545:     return Collections.unmodifiableList(extensions);
 546:   }
 547: 
 548:   /**
 549:    * Negotiate TLS over the current connection.
 550:    * This depends on many features, such as the JSSE classes being in the
 551:    * classpath. Returns true if successful, false otherwise.
 552:    */
 553:   public boolean starttls()
 554:     throws IOException
 555:   {
 556:     return starttls(new EmptyX509TrustManager());
 557:   }
 558:   
 559:   /**
 560:    * Negotiate TLS over the current connection.
 561:    * This depends on many features, such as the JSSE classes being in the
 562:    * classpath. Returns true if successful, false otherwise.
 563:    * @param tm the custom trust manager to use
 564:    */
 565:   public boolean starttls(TrustManager tm)
 566:     throws IOException
 567:   {
 568:     try
 569:       {
 570:         // Use SSLSocketFactory to negotiate a TLS session and wrap the
 571:         // current socket.
 572:         SSLContext context = SSLContext.getInstance("TLS");
 573:         // We don't require strong validation of the server certificate
 574:         TrustManager[] trust = new TrustManager[] { tm };
 575:         context.init(null, trust, null);
 576:         SSLSocketFactory factory = context.getSocketFactory();
 577:         
 578:         send(STARTTLS);
 579:         if (getAllResponses() != READY)
 580:           {
 581:             return false;
 582:           }
 583:         
 584:         String hostname = socket.getInetAddress().getHostName();
 585:         int port = socket.getPort();
 586:         SSLSocket ss =
 587:           (SSLSocket) factory.createSocket(socket, hostname, port, true);
 588:         String[] protocols = { "TLSv1", "SSLv3" };
 589:         ss.setEnabledProtocols(protocols);
 590:         ss.setUseClientMode(true);
 591:         ss.startHandshake();
 592:         
 593:         // Set up streams
 594:         InputStream in = ss.getInputStream();
 595:         in = new BufferedInputStream(in);
 596:         in = new CRLFInputStream(in);
 597:         this.in = new LineInputStream(in);
 598:         OutputStream out = ss.getOutputStream();
 599:         out = new BufferedOutputStream(out);
 600:         this.out = new CRLFOutputStream(out);
 601:         return true;
 602:       }
 603:     catch (GeneralSecurityException e)
 604:       {
 605:         return false;
 606:       }
 607:   }
 608: 
 609:   // -- Authentication --
 610: 
 611:   /**
 612:    * Authenticates the connection using the specified SASL mechanism,
 613:    * username, and password.
 614:    * @param mechanism a SASL authentication mechanism, e.g. LOGIN, PLAIN,
 615:    * CRAM-MD5, GSSAPI
 616:    * @param username the authentication principal
 617:    * @param password the authentication credentials
 618:    * @return true if authentication was successful, false otherwise
 619:    */
 620:   public boolean authenticate(String mechanism, String username,
 621:                               String password) throws IOException
 622:   {
 623:     try
 624:       {
 625:         String[] m = new String[] { mechanism };
 626:         CallbackHandler ch = new SaslCallbackHandler(username, password);
 627:         // Avoid lengthy callback procedure for GNU Crypto
 628:         Properties p = new Properties();
 629:         p.put("gnu.crypto.sasl.username", username);
 630:         p.put("gnu.crypto.sasl.password", password);
 631:         SaslClient sasl =
 632:           Sasl.createSaslClient(m, null, "smtp",
 633:                                 socket.getInetAddress().getHostName(),
 634:                                 p, ch);
 635:         if (sasl == null)
 636:           {
 637:             // Fall back to home-grown SASL clients
 638:             if ("LOGIN".equalsIgnoreCase(mechanism))
 639:               {
 640:                 sasl = new SaslLogin(username, password);
 641:               }
 642:             else if ("PLAIN".equalsIgnoreCase(mechanism))
 643:               {
 644:                 sasl = new SaslPlain(username, password);
 645:               }
 646:             else if ("CRAM-MD5".equalsIgnoreCase(mechanism))
 647:               {
 648:                 sasl = new SaslCramMD5(username, password);
 649:               }
 650:             else
 651:               {
 652:                 return false;
 653:               }
 654:           }
 655:         
 656:         StringBuffer cmd = new StringBuffer(AUTH);
 657:         cmd.append(' ');
 658:         cmd.append(mechanism);
 659:         if (sasl.hasInitialResponse())
 660:           {
 661:             cmd.append(' ');
 662:             byte[] init = sasl.evaluateChallenge(new byte[0]);
 663:             if (init.length == 0)
 664:               {
 665:                 cmd.append('=');
 666:               }
 667:             else
 668:               {
 669:                 cmd.append(new String(BASE64.encode(init), "US-ASCII"));
 670:               }
 671:           }
 672:         send(cmd.toString());
 673:         while (true)
 674:           {
 675:             switch (getAllResponses())
 676:               {
 677:               case 334:
 678:                 try
 679:                   {
 680:                     byte[] c0 = response.getBytes("US-ASCII");
 681:                     byte[] c1 = BASE64.decode(c0);       // challenge
 682:                     byte[] r0 = sasl.evaluateChallenge(c1);
 683:                     byte[] r1 = BASE64.encode(r0);       // response
 684:                     out.write(r1);
 685:                     out.write(0x0d);
 686:                     out.flush();
 687:                     if (debug)
 688:                       {
 689:                         Logger logger = Logger.getInstance();
 690:                         logger.log("smtp", "> " +
 691:                                     new String(r1, "US-ASCII"));
 692:                       }
 693:                   }
 694:                 catch (SaslException e)
 695:                   {
 696:                     // Error in SASL challenge evaluation - cancel exchange
 697:                     out.write(0x2a);
 698:                     out.write(0x0d);
 699:                     out.flush();
 700:                     if (debug)
 701:                       {
 702:                         Logger logger = Logger.getInstance();
 703:                         logger.log("smtp", "> *");
 704:                       }
 705:                   }
 706:                 break;
 707:               case 235:
 708:                 String qop = (String) sasl.getNegotiatedProperty(Sasl.QOP);
 709:                 if ("auth-int".equalsIgnoreCase(qop)
 710:                     || "auth-conf".equalsIgnoreCase(qop))
 711:                   {
 712:                     InputStream in = socket.getInputStream();
 713:                     in = new BufferedInputStream(in);
 714:                     in = new SaslInputStream(sasl, in);
 715:                     in = new CRLFInputStream(in);
 716:                     this.in = new LineInputStream(in);
 717:                     OutputStream out = socket.getOutputStream();
 718:                     out = new BufferedOutputStream(out);
 719:                     out = new SaslOutputStream(sasl, out);
 720:                     this.out = new CRLFOutputStream(out);
 721:                   }
 722:                 return true;
 723:               default:
 724:                 return false;
 725:               }
 726:           }
 727:       }
 728:     catch (SaslException e)
 729:       {
 730:         e.printStackTrace(System.err);
 731:         return false;             // No provider for mechanism
 732:       }
 733:     catch (RuntimeException e)
 734:       {
 735:         e.printStackTrace(System.err);
 736:         return false;             // No javax.security.sasl classes
 737:       }
 738:   }
 739: 
 740:   // -- Utility methods --
 741: 
 742:   /**
 743:    * Send the specified command string to the server.
 744:    * @param command the command to send
 745:    */
 746:   protected void send(String command)
 747:     throws IOException
 748:   {
 749:     if (debug)
 750:       {
 751:         Logger logger = Logger.getInstance();
 752:         logger.log("smtp", "> " + command);
 753:       }
 754:     out.write(command.getBytes("US-ASCII"));
 755:     out.write(0x0d);
 756:     out.flush();
 757:   }
 758:   
 759:   /**
 760:    * Returns the next response from the server.
 761:    */
 762:   protected int getResponse()
 763:     throws IOException
 764:   {
 765:     String line = null;
 766:     try
 767:       {
 768:         line = in.readLine();
 769:         // Handle special case eg 334 where CRLF occurs after code.
 770:         if (line.length() < 4)
 771:           {
 772:             line = line + '\n' + in.readLine();
 773:           }
 774:         if (debug)
 775:           {
 776:             Logger logger = Logger.getInstance();
 777:             logger.log("smtp", "< " + line);
 778:           }
 779:         int code = Integer.parseInt(line.substring(0, 3));
 780:         continuation = (line.charAt(3) == '-');
 781:         response = line.substring(4);
 782:         return code;
 783:       }
 784:     catch (NumberFormatException e)
 785:       {
 786:         throw new ProtocolException("Unexpected response: " + line);
 787:       }
 788:   }
 789: 
 790:   /**
 791:    * Returns the next response from the server.
 792:    * If the response is a continuation, continues to read responses until
 793:    * continuation ceases. If a different response code from the first is
 794:    * encountered, this causes a protocol exception.
 795:    */
 796:   protected int getAllResponses()
 797:     throws IOException
 798:   {
 799:     int code1, code;
 800:     boolean err = false;
 801:     code1 = code = getResponse();
 802:     while (continuation)
 803:       {
 804:         code = getResponse();
 805:         if (code != code1)
 806:           {
 807:             err = true;
 808:           }
 809:       }
 810:     if (err)
 811:       {
 812:         throw new ProtocolException("Conflicting response codes");
 813:       }
 814:     return code;
 815:   }
 816: 
 817: }