blob: aa90882a285fda72e70982ae0f0362762f2928c1 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.javamail.store.pop3.connection;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;
import org.apache.geronimo.javamail.authentication.AuthenticatorFactory;
import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
import org.apache.geronimo.javamail.store.imap.connection.IMAPResponseStream;
import org.apache.geronimo.javamail.store.pop3.POP3Constants;
import org.apache.geronimo.javamail.util.CommandFailedException;
import org.apache.geronimo.javamail.util.InvalidCommandException;
import org.apache.geronimo.javamail.util.MIMEInputReader;
import org.apache.geronimo.javamail.util.MailConnection;
import org.apache.geronimo.javamail.util.ProtocolProperties;
import org.apache.geronimo.mail.util.Base64;
import org.apache.geronimo.mail.util.Hex;
/**
* Simple implementation of POP3 transport.
*
* @version $Rev$ $Date$
*/
public class POP3Connection extends MailConnection implements POP3Constants {
static final protected String MAIL_APOP_ENABLED = "apop.enable";
static final protected String MAIL_AUTH_ENABLED = "auth.enable";
static final protected String MAIL_RESET_QUIT = "rsetbeforequit";
static final protected String MAIL_DISABLE_TOP = "disabletop";
//static final protected String MAIL_FORGET_TOP = "forgettopheaders"; //TODO forgettopheaders
// the initial greeting string, which might be required for APOP authentication.
protected String greeting;
// is use of the AUTH command enabled
protected boolean authEnabled;
// is use of APOP command enabled
protected boolean apopEnabled;
// input reader wrapped around the socket input stream
protected BufferedReader reader;
// output writer wrapped around the socket output stream.
protected PrintWriter writer;
// this connection was closed unexpectedly
protected boolean closed;
// indicates whether this connection is currently logged in. Once
// we send a QUIT, we're finished.
protected boolean loggedIn;
// indicates whether we need to avoid using the TOP command
// when retrieving headers
protected boolean topDisabled = false;
// is TLS enabled on our part?
protected boolean useTLS = false;
// is TLS required on our part?
protected boolean requireTLS = false;
/**
* Normal constructor for an POP3Connection() object.
*
* @param props The protocol properties abstraction containing our
* property modifiers.
*/
public POP3Connection(ProtocolProperties props) {
super(props);
// get our login properties flags
authEnabled = props.getBooleanProperty(MAIL_AUTH_ENABLED, false);
apopEnabled = props.getBooleanProperty(MAIL_APOP_ENABLED, false);
topDisabled = props.getBooleanProperty(MAIL_DISABLE_TOP, false);
// and also check for TLS enablement.
useTLS = props.getBooleanProperty(MAIL_STARTTLS_ENABLE, false);
// and also check if TLS is required.
requireTLS = props.getBooleanProperty(MAIL_STARTTLS_REQUIRED, false);
}
/**
* Connect to the server and do the initial handshaking.
*
* @exception MessagingException
*/
public boolean protocolConnect(String host, int port, String authid, String realm, String username, String password) throws MessagingException {
this.serverHost = host;
this.serverPort = port;
this.realm = realm;
this.authid = authid;
this.username = username;
this.password = password;
try {
// create socket and connect to server.
getConnection();
// consume the welcome line
getWelcome();
// if we're not already using an SSL connection, and we have permission to issue STARTTLS or its even required
// try to setup a SSL connection
if (!sslConnection && (useTLS || requireTLS)) {
// tell the server of our intention to start a TLS session
POP3Response starttlsResponse = null;
try {
starttlsResponse = sendCommand("STLS");
} catch (CommandFailedException e) {
}
//if the server does not support TLS check if its required.
//If true then throw an error, if not establish a non SSL connection
if(requireTLS && (starttlsResponse == null || starttlsResponse.isError())) {
throw new MessagingException("Server doesn't support required transport level security");
} else if(starttlsResponse != null && starttlsResponse.getStatus() == POP3Response.OK) {
// The connection is then handled by the superclass level.
getConnectedTLSSocket();
} else {
if (debug) {
debugOut("STARTTLS is enabled but not required and server does not support it. So we establish a connection without transport level security");
}
}
}
getConnection();
// go login with the server
if (login())
{
loggedIn = true;
return true;
}
return false;
} catch (IOException e) {
if (debug) {
debugOut("I/O exception establishing connection", e);
}
throw new MessagingException("Connection error", e);
}
}
/**
* Create a transport connection object and connect it to the
* target server.
*
* @exception MessagingException
*/
protected void getConnection() throws MessagingException
{
try {
// do all of the non-protocol specific set up. This will get our socket established
// and ready use.
super.getConnection();
} catch (IOException e) {
throw new MessagingException("Unable to obtain a connection to the POP3 server", e);
}
// The POP3 protocol is inherently a string-based protocol, so we get
// string readers/writers for the connection streams. Note that we explicitly
// set the encoding to ensure that an inappropriate native encoding is not picked up.
try {
reader = new BufferedReader(new InputStreamReader(inputStream, "ISO8859-1"));
writer = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream(outputStream), "ISO8859-1"));
} catch (UnsupportedEncodingException e) {
}
}
protected void getWelcome() throws IOException {
// just read the line and consume it. If debug is
// enabled, there I/O stream will be traced
greeting = reader.readLine();
}
public String toString() {
return "POP3Connection host: " + serverHost + " port: " + serverPort;
}
/**
* Close the connection. On completion, we'll be disconnected from
* the server and unable to send more data.
*
* @exception MessagingException
*/
public void close() throws MessagingException {
// if we're already closed, get outta here.
if (socket == null) {
return;
}
try {
// say goodbye
logout();
} finally {
// and close up the connection. We do this in a finally block to make sure the connection
// is shut down even if quit gets an error.
closeServerConnection();
// get rid of our response processor too.
reader = null;
writer = null;
}
}
/**
* Tag this connection as having been closed by the
* server. This will not be returned to the
* connection pool.
*/
public void setClosed() {
closed = true;
}
/**
* Test if the connection has been forcibly closed.
*
* @return True if the server disconnected the connection.
*/
public boolean isClosed() {
return closed;
}
protected POP3Response sendCommand(String cmd) throws MessagingException {
return sendCommand(cmd, false);
}
protected POP3Response sendMultiLineCommand(String cmd) throws MessagingException {
return sendCommand(cmd, true);
}
protected synchronized POP3Response sendCommand(String cmd, boolean multiLine) throws MessagingException {
if (socket.isConnected()) {
{
// NOTE: We don't use println() because it uses the platform concept of a newline rather
// than using CRLF, which is required by the POP3 protocol.
writer.write(cmd);
writer.write("\r\n");
writer.flush();
POP3Response response = buildResponse(multiLine);
if (response.isError()) {
throw new CommandFailedException("Error issuing POP3 command: " + cmd);
}
return response;
}
}
throw new MessagingException("Connection to Mail Server is lost, connection " + this.toString());
}
/**
* Build a POP3Response item from the response stream.
*
* @param isMultiLineResponse
* If true, this command is expecting multiple lines back from the server.
*
* @return A POP3Response item with all of the command response data.
* @exception MessagingException
*/
protected POP3Response buildResponse(boolean isMultiLineResponse) throws MessagingException {
int status = ERR;
byte[] data = null;
String line;
//MIMEInputReader source = new MIMEInputReader(reader); //TODO unused
try {
line = reader.readLine();
} catch (IOException e) {
throw new MessagingException("Error in receving response");
}
if (line == null || line.trim().equals("")) {
throw new MessagingException("Empty Response");
}
if (line.startsWith("+OK")) {
status = OK;
line = removeStatusField(line);
if (isMultiLineResponse) {
data = getMultiLineResponse();
}
} else if (line.startsWith("-ERR")) {
status = ERR;
line = removeStatusField(line);
}else if (line.startsWith("+")) {
status = CHALLENGE;
line = removeStatusField(line);
if (isMultiLineResponse) {
data = getMultiLineResponse();
}
} else {
throw new MessagingException("Unexpected response: " + line);
}
return new POP3Response(status, line, data);
}
private static String removeStatusField(String line) {
return line.substring(line.indexOf(SPACE) + 1);
}
/**
* This could be a multiline response
*/
private byte[] getMultiLineResponse() throws MessagingException {
MIMEInputReader source = new MIMEInputReader(reader);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// it's more efficient to do this a buffer at a time.
// the MIMEInputReader takes care of the byte-stuffing and
// ".\r\n" input terminator for us.
try {
OutputStreamWriter outWriter = new OutputStreamWriter(out, "ISO8859-1");
char buffer[] = new char[500];
try {
int charsRead = -1;
while ((charsRead = source.read(buffer)) >= 0) {
outWriter.write(buffer, 0, charsRead);
}
outWriter.flush();
} catch (IOException e) {
throw new MessagingException("Error processing a multi-line response", e);
}
} catch (UnsupportedEncodingException e) {
}
return out.toByteArray();
}
/**
* Retrieve the raw message content from the POP3
* server. This is all of the message data, including
* the header.
*
* @param sequenceNumber
* The message sequence number.
*
* @return A byte array containing all of the message data.
* @exception MessagingException
*/
public byte[] retrieveMessageData(int sequenceNumber) throws MessagingException {
POP3Response msgResponse = sendMultiLineCommand("RETR " + sequenceNumber);
// we want the data directly in this case.
return msgResponse.getData();
}
/**
* Retrieve the message header information for a given
* message, returned as an input stream suitable
* for loading the message data.
*
* @param sequenceNumber
* The server sequence number for the message.
*
* @return An inputstream that can be used to read the message
* data.
* @exception MessagingException
*/
public ByteArrayInputStream retrieveMessageHeaders(int sequenceNumber) throws MessagingException {
POP3Response msgResponse;
// some POP3 servers don't correctly implement TOP, so this can be disabled. If
// we can't use TOP, then use RETR and retrieve everything. We can just hand back
// the stream, as the header loading routine will stop at the first
// null line.
if (topDisabled) {
msgResponse = sendMultiLineCommand("RETR " + sequenceNumber);
}
else {
msgResponse = sendMultiLineCommand("TOP " + sequenceNumber + " 0");
}
// just load the returned message data as a set of headers
return msgResponse.getContentStream();
}
/**
* Retrieve the total message size from the mail
* server. This is the size of the headers plus
* the size of the message content.
*
* @param sequenceNumber
* The message sequence number.
*
* @return The full size of the message.
* @exception MessagingException
*/
public int retrieveMessageSize(int sequenceNumber) throws MessagingException {
POP3Response msgResponse = sendCommand("LIST " + sequenceNumber);
// Convert this into the parsed response type we need.
POP3ListResponse list = new POP3ListResponse(msgResponse);
// this returns the total message size
return list.getSize();
}
/**
* Retrieve the mail drop status information.
*
* @return An object representing the returned mail drop status.
* @exception MessagingException
*/
public POP3StatusResponse retrieveMailboxStatus() throws MessagingException {
// issue the STAT command and return this into a status response
return new POP3StatusResponse(sendCommand("STAT"));
}
/**
* Retrieve the UID for an individual message.
*
* @param sequenceNumber
* The target message sequence number.
*
* @return The string UID maintained by the server.
* @exception MessagingException
*/
public String retrieveMessageUid(int sequenceNumber) throws MessagingException {
POP3Response msgResponse = sendCommand("UIDL " + sequenceNumber);
String message = msgResponse.getFirstLine();
// the UID is everything after the blank separating the message number and the UID.
// there's not supposed to be anything else on the message, but trim it of whitespace
// just to be on the safe side.
return message.substring(message.indexOf(' ') + 1).trim();
}
/**
* Delete a single message from the mail server.
*
* @param sequenceNumber
* The sequence number of the message to delete.
*
* @exception MessagingException
*/
public void deleteMessage(int sequenceNumber) throws MessagingException {
// just issue the command...we ignore the command response
sendCommand("DELE " + sequenceNumber);
}
/**
* Logout from the mail server. This sends a QUIT
* command, which will likely sever the mail connection.
*
* @exception MessagingException
*/
public void logout() throws MessagingException {
// we may have already sent the QUIT command
if (!loggedIn) {
return;
}
// just issue the command...we ignore the command response
sendCommand("QUIT");
loggedIn = false;
}
/**
* Perform a reset on the mail server.
*
* @exception MessagingException
*/
public void reset() throws MessagingException {
// some mail servers mark retrieved messages for deletion
// automatically. This will reset the read flags before
// we go through normal cleanup.
if (props.getBooleanProperty(MAIL_RESET_QUIT, false)) {
// just send an RSET command first
sendCommand("RSET");
}
}
/**
* Ping the mail server to see if we still have an active connection.
*
* @exception MessagingException thrown if we do not have an active connection.
*/
public void pingServer() throws MessagingException {
// just issue the command...we ignore the command response
sendCommand("NOOP");
}
/**
* Login to the mail server, using whichever method is
* configured. This will try multiple methods, if allowed,
* in decreasing levels of security.
*
* @return true if the login was successful.
* @exception MessagingException
*/
public synchronized boolean login() throws MessagingException {
// permitted to use the AUTH command?
if (authEnabled) {
try {
// go do the SASL thing
return processSaslAuthentication();
} catch (MessagingException e) {
// Any error here means fall back to the next mechanism
}
}
if (apopEnabled) {
try {
// go do the SASL thing
return processAPOPAuthentication();
} catch (MessagingException e) {
// Any error here means fall back to the next mechanism
}
}
try {
// do the tried and true login processing.
return processLogin();
} catch (MessagingException e) {
}
// everything failed...can't get in
return false;
}
/**
* Process a basic LOGIN operation, using the
* plain test USER/PASS command combo.
*
* @return true if we logged successfully.
* @exception MessagingException
*/
public boolean processLogin() throws MessagingException {
// start by sending the USER command, followed by
// the PASS command
sendCommand("USER " + username);
sendCommand("PASS " + password);
return true; // we're in
}
/**
* Process logging in using the APOP command. Only
* works on servers that give a timestamp value
* in the welcome response.
*
* @return true if the login was accepted.
* @exception MessagingException
*/
public boolean processAPOPAuthentication() throws MessagingException {
int timeStart = greeting.indexOf('<');
// if we didn't get an APOP challenge on the greeting, throw an exception
// the main login processor will swallow that and fall back to the next
// mechanism
if (timeStart == -1) {
throw new MessagingException("POP3 Server does not support APOP");
}
int timeEnd = greeting.indexOf('>');
String timeStamp = greeting.substring(timeStart, timeEnd + 1);
// we create the digest password using the timestamp value sent to use
// concatenated with the password.
String digestPassword = timeStamp + password;
byte[] digest;
try {
// create a digest value from the password.
MessageDigest md = MessageDigest.getInstance("MD5");
digest = md.digest(digestPassword.getBytes("iso-8859-1"));
} catch (NoSuchAlgorithmException e) {
// this shouldn't happen, but if it does, we'll just try a plain
// login.
throw new MessagingException("Unable to create MD5 digest", e);
} catch (UnsupportedEncodingException e) {
// this shouldn't happen, but if it does, we'll just try a plain
// login.
throw new MessagingException("Unable to create MD5 digest", e);
}
// this will throw an exception if it gives an error failure
sendCommand("APOP " + username + " " + new String(Hex.encode(digest)));
// no exception, we must have passed
return true;
}
/**
* Process SASL-type authentication.
*
* @return Returns true if the server support a SASL authentication mechanism and
* accepted reponse challenges.
* @exception MessagingException
*/
protected boolean processSaslAuthentication() throws MessagingException {
// if unable to get an appropriate authenticator, just fail it.
ClientAuthenticator authenticator = getSaslAuthenticator();
if (authenticator == null) {
throw new MessagingException("Unable to obtain SASL authenticator");
}
// go process the login.
return processLogin(authenticator);
}
/**
* Attempt to retrieve a SASL authenticator for this
* protocol.
*
* @return A SASL authenticator, or null if a suitable one
* was not located.
*/
protected ClientAuthenticator getSaslAuthenticator() {
return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
}
/**
* Process a login using the provided authenticator object.
*
* NB: This method is synchronized because we have a multi-step process going on
* here. No other commands should be sent to the server until we complete.
*
* @return Returns true if the server support a SASL authentication mechanism and
* accepted reponse challenges.
* @exception MessagingException
*/
protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException {
if (debug) {
debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
}
POP3Response response = sendCommand("AUTH " + authenticator.getMechanismName());
// now process the challenge sequence. We get a continuation response back for each stage of the
// authentication, and finally an OK when everything passes muster.
while (true) {
// this should be a continuation reply, if things are still good.
if (response.isChallenge()) {
// we're passed back a challenge value, Base64 encoded.
byte[] challenge = response.decodeChallengeResponse();
try {
String responseString = new String(Base64.encode(authenticator.evaluateChallenge(challenge)), "US-ASCII");
// have the authenticator evaluate and send back the encoded response.
response = sendCommand(responseString);
} catch (UnsupportedEncodingException ex) {
}
}
else {
// there are only two choices here, OK or a continuation. OK means
// we've passed muster and are in.
return true;
}
}
}
/**
* Merge the configured SASL mechanisms with the capabilities that the
* server has indicated it supports, returning a merged list that can
* be used for selecting a mechanism.
*
* @return A List representing the intersection of the configured list and the
* capabilities list.
*/
protected List selectSaslMechanisms() {
// just return the set that have been explicity permitted
return getSaslMechanisms();
}
}