/*
* $Id: POP3Connection.java,v 1.1 2003/10/19 08:51:37 dog Exp $
* Copyright (C) 2003 The Free Software Foundation
*
* This file is part of GNU inetlib, a library.
*
* GNU inetlib is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GNU inetlib is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* As a special exception, if you link this library with other files to
* produce an executable, this library does not by itself cause the
* resulting executable to be covered by the GNU General Public License.
* This exception does not however invalidate any other reasons why the
* executable file might be covered by the GNU General Public License.
*/
package gnu.inet.pop3;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.ProtocolException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.callback.CallbackHandler;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import javax.security.sasl.SaslException;
import gnu.inet.util.BASE64;
import gnu.inet.util.CRLFInputStream;
import gnu.inet.util.CRLFOutputStream;
import gnu.inet.util.LineInputStream;
import gnu.inet.util.Logger;
import gnu.inet.util.MessageInputStream;
import gnu.inet.util.SaslCallbackHandler;
import gnu.inet.util.SaslInputStream;
import gnu.inet.util.SaslOutputStream;
/**
* A POP3 client connection.
* This implements the entire POP3 specification as detailed in RFC 1939,
* with the exception of the no-arg LIST and UIDL commands (use STAT
* followed by multiple LIST and/or UIDL instead) and TOP with a specified
* number of content lines. It also implements the POP3 extension mechanism
* CAPA, documented in RFC 2449, as well as the STLS command to initiate TLS
* over POP3 documented in RFC 2595 and the AUTH command in RFC 1734.
*
* @author Chris Burdess
* @version $Revision: 1.1 $ $Date: 2003/10/19 08:51:37 $
*/
public class POP3Connection
{
/**
* The default POP3 port.
*/
public static final int DEFAULT_PORT = 110;
// -- POP3 vocabulary --
private static final String _OK = "+OK";
private static final String _ERR = "-ERR";
private static final String _READY = "+ ";
protected static final String STAT = "STAT";
protected static final String LIST = "LIST";
protected static final String RETR = "RETR";
protected static final String DELE = "DELE";
protected static final String NOOP = "NOOP";
protected static final String RSET = "RSET";
protected static final String QUIT = "QUIT";
protected static final String TOP = "TOP";
protected static final String UIDL = "UIDL";
protected static final String USER = "USER";
protected static final String PASS = "PASS";
protected static final String APOP = "APOP";
protected static final String CAPA = "CAPA";
protected static final String STLS = "STLS";
protected static final String AUTH = "AUTH";
protected static final int OK = 0;
protected static final int ERR = 1;
protected static final int READY = 2;
/**
* The socket used to communicate with the server.
*/
protected Socket socket;
/**
* The socket input stream.
*/
protected LineInputStream in;
/**
* The socket output stream.
*/
protected CRLFOutputStream out;
/**
* The last response received from the server.
* The status code (+OK or -ERR) is stripped from the line.
*/
protected String response;
/**
* If true, print debugging information.
*/
protected boolean debug;
/**
* The APOP timestamp, if sent by the server on connection.
* Otherwise null.
*/
protected byte[] timestamp;
/**
* Creates a new connection to the server.
* @param hostname the hostname of the server to connect to
* @param port the port to connect to (if <=0, use default POP3 port)
*/
public POP3Connection(String hostname, int port)
throws UnknownHostException, IOException
{
this(hostname, port, -1, -1, false);
}
/**
* Creates a new connection to the server.
* @param hostname the hostname of the server to connect to
* @param port the port to connect to (if <=0, use default POP3 port)
* @param connectionTimeout the connection timeout, in milliseconds
* @param timeout the I/O timeout, in milliseconds
* @param debug print debugging information
*/
public POP3Connection(String hostname, int port,
int connectionTimeout, int timeout, boolean debug)
throws UnknownHostException, IOException
{
this.debug = debug;
if (port<=0)
port = DEFAULT_PORT;
// Set up socket
// TODO connection timeout
socket = new Socket(hostname, port);
if (timeout>0)
socket.setSoTimeout(timeout);
InputStream in = socket.getInputStream();
in = new BufferedInputStream(in);
in = new CRLFInputStream(in);
this.in = new LineInputStream(in);
OutputStream out = socket.getOutputStream();
out = new BufferedOutputStream(out);
this.out = new CRLFOutputStream(out);
if (getResponse()!=OK)
throw new ProtocolException("Connect failed: "+response);
// APOP timestamp
timestamp = parseTimestamp(response);
}
/**
* Authenticates the connection using the specified SASL mechanism,
* username, and password.
* @param mechanism a SASL authentication mechanism, e.g. LOGIN, PLAIN,
* CRAM-MD5, GSSAPI
* @param username the authentication principal
* @param password the authentication credentials
* @return true if authentication was successful, false otherwise
*/
public boolean auth(String mechanism, String username, String password)
throws IOException
{
try
{
String[] m = new String[] { mechanism };
CallbackHandler ch = new SaslCallbackHandler(username, password);
// Avoid lengthy callback procedure for GNU Crypto
Properties p = new Properties();
p.put("gnu.crypto.sasl.username", username);
p.put("gnu.crypto.sasl.password", password);
SaslClient sasl = Sasl.createSaslClient(m, null, "smtp",
socket.getInetAddress().getHostName(), p, ch);
StringBuffer cmd = new StringBuffer(AUTH);
cmd.append(' ');
cmd.append(mechanism);
if (sasl.hasInitialResponse())
{
cmd.append(' ');
byte[] init = sasl.evaluateChallenge(new byte[0]);
cmd.append(new String(init, "US-ASCII"));
}
send(cmd.toString());
while (true)
{
switch (getResponse())
{
case ERR:
return false;
case OK:
String qop = (String)sasl.getNegotiatedProperty(Sasl.QOP);
if ("auth-int".equalsIgnoreCase(qop)
|| "auth-conf".equalsIgnoreCase(qop))
{
InputStream in = socket.getInputStream();
in = new BufferedInputStream(in);
in = new SaslInputStream(sasl, in);
in = new CRLFInputStream(in);
this.in = new LineInputStream(in);
OutputStream out = socket.getOutputStream();
out = new BufferedOutputStream(out);
out = new SaslOutputStream(sasl, out);
this.out = new CRLFOutputStream(out);
}
return true;
case READY:
try
{
byte[] c0 = response.getBytes("US-ASCII");
byte[] c1 = BASE64.decode(c0); // challenge
byte[] r0 = sasl.evaluateChallenge(c1);
byte[] r1 = BASE64.encode(r0); // response
out.write(r1);
out.write(0x0d);
out.flush();
}
catch (SaslException e)
{
// Error in SASL challenge evaluation - cancel exchange
out.write(0x2a);
out.write(0x0d);
out.flush();
}
}
}
}
catch (SaslException e)
{
return false; // No provider for mechanism
}
catch (ClassNotFoundException e)
{
return false; // No javax.security.sasl classes
}
}
/**
* Authenticate the specified user using the APOP MD5-based method.
* This does not transmit the password in the clear, but doesn't provide
* any transport-level privacy features either.
* @param username the user to authenticate
* @param password the user's password
*/
public boolean apop(String username, String password)
throws IOException
{
if (username==null || password==null || timestamp!=null)
return false;
// APOP
try
{
byte[] secret = password.getBytes("US-ASCII");
// compute digest
byte[] target = new byte[timestamp.length + secret.length];
System.arraycopy(timestamp, 0, target, 0, timestamp.length);
System.arraycopy(secret, 0, target, timestamp.length, secret.length);
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] db = md5.digest(target);
// create hexadecimal representation
StringBuffer digest = new StringBuffer();
for (int i=0; iauth or apop
method if the server
* understands them.
* @param username the user to authenticate
* @param password the user's password
*/
public boolean login(String username, String password)
throws IOException
{
if (username==null || password==null)
return false;
// USER
String cmd = new StringBuffer(USER)
.append(' ')
.append(username)
.toString();
send(cmd);
if (getResponse()!=OK)
return false;
// PASS
cmd = new StringBuffer(PASS)
.append(' ')
.append(password)
.toString();
send(cmd);
return getResponse()==OK;
}
/**
* Attempts to start TLS on the specified connection.
* See RFC 2595 for details
* @return true if successful, false otherwise
*/
public boolean stls()
throws IOException
{
try
{
// Use SSLSocketFactory to negotiate a TLS session and wrap the
// current socket.
SSLSocketFactory factory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
send(STLS);
if (getResponse()!=OK)
return false;
socket = factory.createSocket(socket,
socket.getInetAddress().getHostName(),
socket.getPort(),
true);
// set up streams
InputStream in = socket.getInputStream();
in = new BufferedInputStream(in);
in = new CRLFInputStream(in);
this.in = new LineInputStream(in);
OutputStream out = socket.getOutputStream();
out = new BufferedOutputStream(out);
this.out = new CRLFOutputStream(out);
return true;
}
catch (ClassNotFoundException e)
{
return false; // No javax.net classes in runtime
}
}
/**
* Returns the number of messages in the maildrop.
*/
public int stat()
throws IOException
{
send(STAT);
if (getResponse()!=OK)
throw new ProtocolException("STAT failed: "+response);
try
{
return Integer.parseInt(response.substring(0, response.indexOf(' ')));
}
catch (NumberFormatException e)
{
throw new ProtocolException("Not a number: "+response);
}
catch (ArrayIndexOutOfBoundsException e)
{
throw new ProtocolException("Not a STAT response: "+response);
}
}
/**
* Returns the size of the specified message.
* @param msgnum the message number
*/
public int list(int msgnum)
throws IOException
{
String cmd = new StringBuffer(LIST)
.append(' ')
.append(msgnum)
.toString();
send(cmd);
if (getResponse()!=OK)
throw new ProtocolException("LIST failed: "+response);
try
{
return Integer.parseInt(response.substring(response.indexOf(' ')+1));
}
catch (NumberFormatException e)
{
throw new ProtocolException("Not a number: "+response);
}
}
/**
* Returns an input stream containing the entire message.
* This input stream must be read in its entirety before further commands
* can be issued on this connection.
* @param msgnum the message number
*/
public InputStream retr(int msgnum)
throws IOException
{
String cmd = new StringBuffer(RETR)
.append(' ')
.append(msgnum)
.toString();
send(cmd);
if (getResponse()!=OK)
throw new ProtocolException("RETR failed: "+response);
return new MessageInputStream(in);
}
/**
* Marks the specified message as deleted.
* @param msgnum the message number
*/
public void dele(int msgnum)
throws IOException
{
String cmd = new StringBuffer(DELE)
.append(' ')
.append(msgnum)
.toString();
send(cmd);
if (getResponse()!=OK)
throw new ProtocolException("DELE failed: "+response);
}
/**
* Does nothing.
* This can be used to keep the connection alive.
*/
public void noop()
throws IOException
{
send(NOOP);
if (getResponse()!=OK)
throw new ProtocolException("NOOP failed: "+response);
}
/**
* If any messages have been marked as deleted, they are unmarked.
*/
public void rset()
throws IOException
{
send(RSET);
if (getResponse()!=OK)
throw new ProtocolException("RSET failed: "+response);
}
/**
* Closes the connection.
* No further commands may be issued on this connection after this method
* has been called.
* @return true if all deleted messages were successfully removed, false
* otherwise
*/
public boolean quit()
throws IOException
{
send(QUIT);
int ret = getResponse();
socket.close();
return ret==OK;
}
/**
* Returns just the headers of the specified message as an input stream.
* The stream must be read in its entirety before further commands can be
* issued.
* @param msgnum the message number
*/
public InputStream top(int msgnum)
throws IOException
{
String cmd = new StringBuffer(TOP)
.append(' ')
.append(msgnum)
.append(' ')
.append(0)
.toString();
send(cmd);
if (getResponse()!=OK)
throw new ProtocolException("TOP failed: "+response);
return new MessageInputStream(in);
}
/**
* Returns a unique identifier for the specified message.
* @param msgnum the message number
*/
public String uidl(int msgnum)
throws IOException
{
String cmd = new StringBuffer(UIDL)
.append(' ')
.append(msgnum)
.toString();
send(cmd);
if (getResponse()!=OK)
throw new ProtocolException("UIDL failed: "+response);
return response.substring(response.indexOf(' ')+1);
}
/**
* Returns a list of capabilities supported by the POP3 server.
* If the server does not support POP3 extensions, returns
* null
.
*/
public List capa()
throws IOException
{
send(CAPA);
if (getResponse()==OK)
{
final String DOT = ".";
List list = new ArrayList();
for (String line = in.readLine();
!DOT.equals(line);
line = in.readLine())
list.add(line);
return list;
}
return null;
}
/**
* Send the command to the server.
* If debug
is true
,
* the command is logged.
*/
protected void send(String command)
throws IOException
{
if (debug)
{
Logger logger = Logger.getInstance();
logger.log("pop3", "> "+command);
}
out.write(command);
out.writeln();
out.flush();
}
/**
* Parse the response from the server.
* If debug
is true
,
* the response is logged.
*/
protected int getResponse()
throws IOException
{
response = in.readLine();
if (debug)
{
Logger logger = Logger.getInstance();
logger.log("pop3", "< "+response);
}
if (response.indexOf(_OK)==0)
{
response = response.substring(3).trim();
return OK;
}
else if (response.indexOf(_ERR)==0)
{
response = response.substring(4).trim();
return ERR;
}
else if (response.indexOf(_READY)==0)
{
response = response.substring(2).trim();
return READY;
}
else
{
throw new ProtocolException("Unexpected response: "+response);
}
}
/*
* Parse the APOP timestamp from the server's banner greeting.
*/
byte[] parseTimestamp(String greeting)
throws IOException
{
int bra = greeting.indexOf('<');
if (bra!=-1)
{
int ket = greeting.indexOf('>', bra);
if (ket!=-1)
{
String mid = greeting.substring(bra, ket+1);
int at = mid.indexOf('@');
if (at!=-1) // This is a valid RFC822 msg-id
return mid.getBytes("US-ASCII");
}
}
return null;
}
}