001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.geronimo.javamail.store.pop3.connection;
019    
020    import java.io.*;
021    import java.net.InetAddress;
022    import java.net.Socket;
023    import java.net.SocketException;
024    import java.net.UnknownHostException;
025    import java.security.MessageDigest;
026    import java.security.NoSuchAlgorithmException;
027    import java.util.ArrayList;
028    import java.util.Date;
029    import java.util.HashMap;
030    import java.util.Iterator;
031    import java.util.LinkedList;
032    import java.util.List;
033    import java.util.StringTokenizer;
034    
035    import javax.mail.MessagingException;
036    import javax.mail.internet.InternetHeaders;
037    
038    import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 
039    import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
040    import org.apache.geronimo.javamail.store.pop3.POP3Constants;         
041    import org.apache.geronimo.javamail.util.CommandFailedException;      
042    import org.apache.geronimo.javamail.util.InvalidCommandException;      
043    import org.apache.geronimo.javamail.util.MIMEInputReader;
044    import org.apache.geronimo.javamail.util.MailConnection; 
045    import org.apache.geronimo.javamail.util.ProtocolProperties; 
046    import org.apache.geronimo.mail.util.Base64;
047    import org.apache.geronimo.mail.util.Hex;
048    
049    /**
050     * Simple implementation of POP3 transport.  
051     *
052     * @version $Rev: 597135 $ $Date: 2007-11-21 11:26:57 -0500 (Wed, 21 Nov 2007) $
053     */
054    public class POP3Connection extends MailConnection implements POP3Constants {
055        
056        static final protected String MAIL_APOP_ENABLED = "apop.enable"; 
057        static final protected String MAIL_AUTH_ENABLED = "auth.enable"; 
058        static final protected String MAIL_RESET_QUIT = "rsetbeforequit"; 
059        static final protected String MAIL_DISABLE_TOP = "disabletop";
060        static final protected String MAIL_FORGET_TOP = "forgettopheaders"; 
061        
062        // the initial greeting string, which might be required for APOP authentication. 
063        protected String greeting; 
064        // is use of the AUTH command enabled 
065        protected boolean authEnabled; 
066        // is use of APOP command enabled 
067        protected boolean apopEnabled; 
068        // input reader wrapped around the socket input stream 
069        protected BufferedReader reader; 
070        // output writer wrapped around the socket output stream. 
071        protected PrintWriter writer; 
072        // this connection was closed unexpectedly
073        protected boolean closed;
074        // indicates whether this conneciton is currently logged in.  Once 
075        // we send a QUIT, we're finished. 
076        protected boolean loggedIn; 
077        // indicates whether we need to avoid using the TOP command 
078        // when retrieving headers
079        protected boolean topDisabled = false; 
080    
081        /**
082         * Normal constructor for an POP3Connection() object.
083         *
084         * @param store    The store we're associated with (source of parameter values).
085         * @param host     The target host name of the IMAP server.
086         * @param port     The target listening port of the server.  Defaults to 119 if
087         *                 the port is specified as -1.
088         * @param username The login user name (can be null unless authentication is
089         *                 required).
090         * @param password Password associated with the userid account.  Can be null if
091         *                 authentication is not required.
092         * @param sslConnection
093         *                 True if this is targetted as an SSLConnection.
094         * @param debug    The session debug flag.
095         */
096        public POP3Connection(ProtocolProperties props) {
097            super(props);
098            
099            // 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