View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.store.pop3.connection;
19  
20  import java.io.*;
21  import java.net.InetAddress;
22  import java.net.Socket;
23  import java.net.SocketException;
24  import java.net.UnknownHostException;
25  import java.security.MessageDigest;
26  import java.security.NoSuchAlgorithmException;
27  import java.util.ArrayList;
28  import java.util.Date;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.LinkedList;
32  import java.util.List;
33  import java.util.StringTokenizer;
34  
35  import javax.mail.MessagingException;
36  import javax.mail.internet.InternetHeaders;
37  
38  import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 
39  import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
40  import org.apache.geronimo.javamail.store.pop3.POP3Constants;         
41  import org.apache.geronimo.javamail.util.CommandFailedException;      
42  import org.apache.geronimo.javamail.util.InvalidCommandException;      
43  import org.apache.geronimo.javamail.util.MIMEInputReader;
44  import org.apache.geronimo.javamail.util.MailConnection; 
45  import org.apache.geronimo.javamail.util.ProtocolProperties; 
46  import org.apache.geronimo.mail.util.Base64;
47  import org.apache.geronimo.mail.util.Hex;
48  
49  /**
50   * Simple implementation of POP3 transport.  
51   *
52   * @version $Rev: 597135 $ $Date: 2007-11-21 11:26:57 -0500 (Wed, 21 Nov 2007) $
53   */
54  public class POP3Connection extends MailConnection implements POP3Constants {
55      
56      static final protected String MAIL_APOP_ENABLED = "apop.enable"; 
57      static final protected String MAIL_AUTH_ENABLED = "auth.enable"; 
58      static final protected String MAIL_RESET_QUIT = "rsetbeforequit"; 
59      static final protected String MAIL_DISABLE_TOP = "disabletop";
60      static final protected String MAIL_FORGET_TOP = "forgettopheaders"; 
61      
62      // the initial greeting string, which might be required for APOP authentication. 
63      protected String greeting; 
64      // is use of the AUTH command enabled 
65      protected boolean authEnabled; 
66      // is use of APOP command enabled 
67      protected boolean apopEnabled; 
68      // input reader wrapped around the socket input stream 
69      protected BufferedReader reader; 
70      // output writer wrapped around the socket output stream. 
71      protected PrintWriter writer; 
72      // this connection was closed unexpectedly
73      protected boolean closed;
74      // indicates whether this conneciton is currently logged in.  Once 
75      // we send a QUIT, we're finished. 
76      protected boolean loggedIn; 
77      // indicates whether we need to avoid using the TOP command 
78      // when retrieving headers
79      protected boolean topDisabled = false; 
80  
81      /**
82       * Normal constructor for an POP3Connection() object.
83       *
84       * @param store    The store we're associated with (source of parameter values).
85       * @param host     The target host name of the IMAP server.
86       * @param port     The target listening port of the server.  Defaults to 119 if
87       *                 the port is specified as -1.
88       * @param username The login user name (can be null unless authentication is
89       *                 required).
90       * @param password Password associated with the userid account.  Can be null if
91       *                 authentication is not required.
92       * @param sslConnection
93       *                 True if this is targetted as an SSLConnection.
94       * @param debug    The session debug flag.
95       */
96      public POP3Connection(ProtocolProperties props) {
97          super(props);
98          
99          // get our login properties flags 
100         authEnabled = props.getBooleanProperty(MAIL_AUTH_ENABLED, false); 
101         apopEnabled = props.getBooleanProperty(MAIL_APOP_ENABLED, false); 
102         topDisabled = props.getBooleanProperty(MAIL_DISABLE_TOP, false); 
103     }
104 
105                           
106     /**
107      * Connect to the server and do the initial handshaking.
108      *
109      * @exception MessagingException
110      */
111     public boolean protocolConnect(String host, int port, String authid, String realm, String username, String password) throws MessagingException {
112         this.serverHost = host; 
113         this.serverPort = port; 
114         this.realm = realm; 
115         this.authid = authid; 
116         this.username = username; 
117         this.password = password; 
118         
119         try {
120             // create socket and connect to server.
121             getConnection();
122             // consume the welcome line 
123             getWelcome(); 
124 
125             // go login with the server 
126             if (login())  
127             {
128                 loggedIn = true; 
129                 return true;
130             }
131             return false; 
132         } catch (IOException e) {
133             if (debug) {
134                 debugOut("I/O exception establishing connection", e);
135             }
136             throw new MessagingException("Connection error", e);
137         }
138     }
139 
140 
141     /**
142      * Create a transport connection object and connect it to the
143      * target server.
144      *
145      * @exception MessagingException
146      */
147     protected void getConnection() throws MessagingException
148     {
149         try {
150             // do all of the non-protocol specific set up.  This will get our socket established 
151             // and ready use. 
152             super.getConnection(); 
153         } catch (IOException e) {
154             throw new MessagingException("Unable to obtain a connection to the POP3 server", e); 
155         }
156         
157         // The POp3 protocol is inherently a string-based protocol, so we get 
158         // string readers/writers for the connection streams 
159         reader = new BufferedReader(new InputStreamReader(inputStream));
160         writer = new PrintWriter(new BufferedOutputStream(outputStream));
161     }
162     
163     protected void getWelcome() throws IOException {
164         // just read the line and consume it.  If debug is 
165         // enabled, there I/O stream will be traced
166         greeting = reader.readLine(); 
167     }
168 
169     public String toString() {
170         return "POP3Connection host: " + serverHost + " port: " + serverPort;
171     }
172 
173 
174     /**
175      * Close the connection.  On completion, we'll be disconnected from
176      * the server and unable to send more data.
177      *
178      * @exception MessagingException
179      */
180     public void close() throws MessagingException {
181         // if we're already closed, get outta here.
182         if (socket == null) {
183             return;
184         }
185         try {
186             // say goodbye
187             logout();   
188         } finally {
189             // and close up the connection.  We do this in a finally block to make sure the connection
190             // is shut down even if quit gets an error.
191             closeServerConnection();
192             // get rid of our response processor too. 
193             reader = null; 
194             writer = null; 
195         }
196     }
197 
198     
199     /**
200      * Tag this connection as having been closed by the 
201      * server.  This will not be returned to the 
202      * connection pool. 
203      */
204     public void setClosed() {
205         closed = true;
206     }
207     
208     /**
209      * Test if the connnection has been forcibly closed.
210      * 
211      * @return True if the server disconnected the connection.
212      */
213     public boolean isClosed() {
214         return closed; 
215     }
216     
217     protected POP3Response sendCommand(String cmd) throws MessagingException {
218         return sendCommand(cmd, false); 
219     }
220     
221     protected POP3Response sendMultiLineCommand(String cmd) throws MessagingException {
222         return sendCommand(cmd, true); 
223     }
224 
225     protected synchronized POP3Response sendCommand(String cmd, boolean multiLine) throws MessagingException {
226         if (socket.isConnected()) {
227             {
228                 // NOTE:  We don't use println() because it uses the platform concept of a newline rather 
229                 // than using CRLF, which is required by the POP3 protocol.  
230                 writer.write(cmd);
231                 writer.write("\r\n"); 
232                 writer.flush();
233                 
234                 POP3Response response = buildResponse(multiLine); 
235                 if (response.isError()) {
236                     throw new CommandFailedException("Error issuing POP3 command: " + cmd); 
237                 }
238                 return response; 
239             }
240         }
241         throw new MessagingException("Connection to Mail Server is lost, connection " + this.toString());
242     }
243     
244     /**
245      * Build a POP3Response item from the response stream.
246      * 
247      * @param isMultiLineResponse
248      *               If true, this command is expecting multiple lines back from the server.
249      * 
250      * @return A POP3Response item with all of the command response data. 
251      * @exception MessagingException
252      */
253     protected POP3Response buildResponse(boolean isMultiLineResponse) throws MessagingException {
254         int status = ERR;
255         byte[] data = null;
256 
257         String line;
258         MIMEInputReader source = new MIMEInputReader(reader); 
259         
260         try {
261             line = reader.readLine();
262         } catch (IOException e) {
263             throw new MessagingException("Error in receving response");
264         }
265         
266         if (line == null || line.trim().equals("")) {
267             throw new MessagingException("Empty Response");
268         }
269         
270         if (line.startsWith("+OK")) {
271             status = OK;
272             line = removeStatusField(line);
273             if (isMultiLineResponse) {
274                 data = getMultiLineResponse();
275             }
276         } else if (line.startsWith("-ERR")) {
277             status = ERR;
278             line = removeStatusField(line);
279         }else if (line.startsWith("+")) {
280         	status = CHALLENGE;
281         	line = removeStatusField(line);
282         	if (isMultiLineResponse) {
283         		data = getMultiLineResponse();
284         	}
285         } else {
286             throw new MessagingException("Unexpected response: " + line);
287         }
288         return new POP3Response(status, line, data);
289     }
290 
291     private static String removeStatusField(String line) {
292         return line.substring(line.indexOf(SPACE) + 1);
293     }
294 
295     /**
296      * This could be a multiline response
297      */
298     private byte[] getMultiLineResponse() throws MessagingException {
299 
300         MIMEInputReader source = new MIMEInputReader(reader); 
301         
302         ByteArrayOutputStream out = new ByteArrayOutputStream();
303         
304         // it's more efficient to do this a buffer at a time. 
305         // the MIMEInputReader takes care of the byte-stuffing and 
306         // ".\r\n" input terminator for us. 
307         OutputStreamWriter outWriter = new OutputStreamWriter(out); 
308         char buffer[] = new char[500]; 
309         try {
310             int charsRead = -1;
311             while ((charsRead = source.read(buffer)) >= 0) {
312                 outWriter.write(buffer, 0, charsRead);
313             }
314             outWriter.flush(); 
315         } catch (IOException e) {
316             throw new MessagingException("Error processing a multi-line response", e);
317         }
318         
319         return out.toByteArray();
320     }
321     
322     
323     /**
324      * Retrieve the raw message content from the POP3 
325      * server.  This is all of the message data, including 
326      * the header. 
327      * 
328      * @param sequenceNumber
329      *               The message sequence number.
330      * 
331      * @return A byte array containing all of the message data. 
332      * @exception MessagingException
333      */
334     public byte[] retrieveMessageData(int sequenceNumber) throws MessagingException {
335         POP3Response msgResponse = sendMultiLineCommand("RETR " + sequenceNumber); 
336         // we want the data directly in this case. 
337         return msgResponse.getData();          
338     }
339     
340     /**
341      * Retrieve the message header information for a given 
342      * message, returned as an input stream suitable 
343      * for loading the message data.
344      * 
345      * @param sequenceNumber
346      *               The server sequence number for the message.
347      * 
348      * @return An inputstream that can be used to read the message 
349      *         data.
350      * @exception MessagingException
351      */
352     public ByteArrayInputStream retrieveMessageHeaders(int sequenceNumber) throws MessagingException {
353         POP3Response msgResponse; 
354         
355         // some POP3 servers don't correctly implement TOP, so this can be disabled.  If 
356         // we can't use TOP, then use RETR and retrieve everything.  We can just hand back 
357         // the stream, as the header loading routine will stop at the first 
358         // null line. 
359         if (topDisabled) {
360             msgResponse = sendMultiLineCommand("RETR " + sequenceNumber); 
361         }
362         else {
363             msgResponse = sendMultiLineCommand("TOP " + sequenceNumber + " 0"); 
364         }
365         
366         // just load the returned message data as a set of headers 
367         return msgResponse.getContentStream();
368     }
369     
370     /**
371      * Retrieve the total message size from the mail 
372      * server.  This is the size of the headers plus 
373      * the size of the message content. 
374      * 
375      * @param sequenceNumber
376      *               The message sequence number.
377      * 
378      * @return The full size of the message.
379      * @exception MessagingException
380      */
381     public int retrieveMessageSize(int sequenceNumber) throws MessagingException {
382         POP3Response msgResponse = sendCommand("LIST " + sequenceNumber); 
383         // Convert this into the parsed response type we need. 
384         POP3ListResponse list = new POP3ListResponse(msgResponse); 
385         // this returns the total message size 
386         return list.getSize(); 
387     }
388     
389     /**
390      * Retrieve the mail drop status information.
391      * 
392      * @return An object representing the returned mail drop status. 
393      * @exception MessagingException
394      */
395     public POP3StatusResponse retrieveMailboxStatus() throws MessagingException {
396         // issue the STAT command and return this into a status response 
397         return new POP3StatusResponse(sendCommand("STAT")); 
398     }
399     
400     
401     /**
402      * Retrieve the UID for an individual message.
403      * 
404      * @param sequenceNumber
405      *               The target message sequence number.
406      * 
407      * @return The string UID maintained by the server. 
408      * @exception MessagingException
409      */
410     public String retrieveMessageUid(int sequenceNumber) throws MessagingException {
411         POP3Response msgResponse = sendCommand("UIDL " + sequenceNumber); 
412         
413         String message = msgResponse.getFirstLine(); 
414         // the UID is everything after the blank separating the message number and the UID. 
415         // there's not supposed to be anything else on the message, but trim it of whitespace 
416         // just to be on the safe side. 
417         return message.substring(message.indexOf(' ') + 1).trim(); 
418     }
419     
420     
421     /**
422      * Delete a single message from the mail server.
423      * 
424      * @param sequenceNumber
425      *               The sequence number of the message to delete.
426      * 
427      * @exception MessagingException
428      */
429     public void deleteMessage(int sequenceNumber) throws MessagingException {
430         // just issue the command...we ignore the command response 
431         sendCommand("DELE " + sequenceNumber); 
432     }
433     
434     /**
435      * Logout from the mail server.  This sends a QUIT 
436      * command, which will likely sever the mail connection.
437      * 
438      * @exception MessagingException
439      */
440     public void logout() throws MessagingException {
441         // we may have already sent the QUIT command
442         if (!loggedIn) {
443             return; 
444         }
445         // just issue the command...we ignore the command response 
446         sendCommand("QUIT"); 
447         loggedIn = false; 
448     }
449     
450     /**
451      * Perform a reset on the mail server.                   
452      * 
453      * @exception MessagingException
454      */
455     public void reset() throws MessagingException {
456         // some mail servers mark retrieved messages for deletion 
457         // automatically.  This will reset the read flags before 
458         // we go through normal cleanup. 
459         if (props.getBooleanProperty(MAIL_RESET_QUIT, false)) {
460             // just send an RSET command first
461             sendCommand("RSET"); 
462         }
463     }
464     
465     /**
466      * Ping the mail server to see if we still have an active connection. 
467      * 
468      * @exception MessagingException thrown if we do not have an active connection. 
469      */
470     public void pingServer() throws MessagingException {
471         // just issue the command...we ignore the command response 
472         sendCommand("NOOP"); 
473     }
474     
475     /**
476      * Login to the mail server, using whichever method is 
477      * configured.  This will try multiple methods, if allowed, 
478      * in decreasing levels of security. 
479      * 
480      * @return true if the login was successful. 
481      * @exception MessagingException
482      */
483     public synchronized boolean login() throws MessagingException {
484         // permitted to use the AUTH command?
485         if (authEnabled) {
486             try {
487                 // go do the SASL thing 
488                 return processSaslAuthentication(); 
489             } catch (MessagingException e) {
490                 // Any error here means fall back to the next mechanism 
491             }
492         }
493         
494         if (apopEnabled) {
495             try {
496                 // go do the SASL thing 
497                 return processAPOPAuthentication(); 
498             } catch (MessagingException e) {
499                 // Any error here means fall back to the next mechanism 
500             }
501         }
502         
503         try {
504             // do the tried and true login processing. 
505             return processLogin(); 
506         } catch (MessagingException e) {
507         }
508         // everything failed...can't get in 
509         return false; 
510     }
511     
512     
513     /**
514      * Process a basic LOGIN operation, using the 
515      * plain test USER/PASS command combo. 
516      * 
517      * @return true if we logged successfully. 
518      * @exception MessagingException
519      */
520     public boolean processLogin() throws MessagingException {
521         // start by sending the USER command, followed by  
522         // the PASS command
523         sendCommand("USER " + username); 
524         sendCommand("PASS " + password); 
525         return true;       // we're in 
526     }
527     
528     /**
529      * Process logging in using the APOP command.  Only 
530      * works on servers that give a timestamp value 
531      * in the welcome response. 
532      * 
533      * @return true if the login was accepted. 
534      * @exception MessagingException
535      */
536     public boolean processAPOPAuthentication() throws MessagingException {
537         int timeStart = greeting.indexOf('<'); 
538         // if we didn't get an APOP challenge on the greeting, throw an exception 
539         // the main login processor will swallow that and fall back to the next 
540         // mechanism 
541         if (timeStart == -1) {
542             throw new MessagingException("POP3 Server does not support APOP"); 
543         }
544         int timeEnd = greeting.indexOf('>'); 
545         String timeStamp = greeting.substring(timeStart, timeEnd + 1); 
546         
547         // we create the digest password using the timestamp value sent to use 
548         // concatenated with the password. 
549         String digestPassword = timeStamp + password; 
550         
551         byte[] digest; 
552         
553         try {
554             // create a digest value from the password. 
555             MessageDigest md = MessageDigest.getInstance("MD5"); 
556             digest = md.digest(digestPassword.getBytes("iso-8859-1")); 
557         } catch (NoSuchAlgorithmException e) {
558             // this shouldn't happen, but if it does, we'll just try a plain 
559             // login. 
560             throw new MessagingException("Unable to create MD5 digest", e); 
561         } catch (UnsupportedEncodingException e) {
562             // this shouldn't happen, but if it does, we'll just try a plain 
563             // login. 
564             throw new MessagingException("Unable to create MD5 digest", e); 
565         }
566         // this will throw an exception if it gives an error failure
567         sendCommand("APOP " + username + " " + Hex.encode(digest)); 
568         // no exception, we must have passed 
569         return true; 
570     }
571 
572     
573     /**
574      * Process SASL-type authentication.
575      * 
576      * @return Returns true if the server support a SASL authentication mechanism and
577      *         accepted reponse challenges.
578      * @exception MessagingException
579      */
580     protected boolean processSaslAuthentication() throws MessagingException {
581         // if unable to get an appropriate authenticator, just fail it. 
582         ClientAuthenticator authenticator = getSaslAuthenticator(); 
583         if (authenticator == null) {
584             throw new MessagingException("Unable to obtain SASL authenticator"); 
585         }
586         
587         // go process the login.
588         return processLogin(authenticator);
589     }
590     
591     /**
592      * Attempt to retrieve a SASL authenticator for this 
593      * protocol. 
594      * 
595      * @return A SASL authenticator, or null if a suitable one 
596      *         was not located.
597      */
598     protected ClientAuthenticator getSaslAuthenticator() {
599         return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm); 
600     }
601 
602 
603     /**
604      * Process a login using the provided authenticator object.
605      * 
606      * NB:  This method is synchronized because we have a multi-step process going on 
607      * here.  No other commands should be sent to the server until we complete. 
608      *
609      * @return Returns true if the server support a SASL authentication mechanism and
610      * accepted reponse challenges.
611      * @exception MessagingException
612      */
613     protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException {
614         if (debug) {
615             debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
616         }
617 
618         POP3Response response = sendCommand("AUTH " + authenticator.getMechanismName()); 
619 
620         // now process the challenge sequence.  We get a continuation response back for each stage of the 
621         // authentication, and finally an OK when everything passes muster.     
622         while (true) {
623             // this should be a continuation reply, if things are still good.
624             if (response.isChallenge()) {
625                 // we're passed back a challenge value, Base64 encoded.
626                 byte[] challenge = response.decodeChallengeResponse();
627                 
628                 String responseString = new String(Base64.encode(authenticator.evaluateChallenge(challenge)));
629 
630                 // have the authenticator evaluate and send back the encoded response.
631                 response = sendCommand(responseString);
632             }
633             else {
634                 // there are only two choices here, OK or a continuation.  OK means 
635                 // we've passed muster and are in. 
636                 return true; 
637             }
638         }
639     }
640     
641     
642     /**
643      * Merge the configured SASL mechanisms with the capabilities that the 
644      * server has indicated it supports, returning a merged list that can 
645      * be used for selecting a mechanism. 
646      * 
647      * @return A List representing the intersection of the configured list and the 
648      *         capabilities list.
649      */
650     protected List selectSaslMechanisms() {
651         // just return the set that have been explicity permitted 
652         return getSaslMechanisms(); 
653     }
654 }
655