/*
* Created on Nov 4, 2004
*
* This file is part of susimail project, see http://susi.i2p/
*
* Copyright (C) 2004-2005 <susi23@mail.i2p>
*
* This program 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.
*
* This program 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 program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Revision: 1.1 $
*/
package i2p.susi.webmail.pop3;
import i2p.susi.debug.Debug;
import i2p.susi.webmail.Messages;
import i2p.susi.webmail.NewMailListener;
import i2p.susi.webmail.WebMail;
import i2p.susi.util.Config;
import i2p.susi.util.ReadBuffer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import net.i2p.data.DataHelper;
/**
* @author susi23
*/
public class POP3MailBox implements NewMailListener {
private final String host, user, pass;
private String lastLine, lastError;
private final int port;
private int mails;
private boolean connected;
private boolean gotCAPA;
private boolean supportsPipelining;
private boolean supportsTOP;
private boolean supportsUIDL;
/** ID to size */
private final HashMap<Integer, Integer> sizes;
/** UIDL to ID */
private final HashMap<String, Integer> uidlToID;
private Socket socket;
private final AtomicLong lastActive;
private final AtomicLong lastChecked;
private final Object synchronizer;
private final DelayedDeleter delayedDeleter;
// instantiated after first successful connection
private BackgroundChecker backgroundChecker;
// instantiated after every successful connection
private IdleCloser idleCloser;
private volatile NewMailListener newMailListener;
/**
* Does not connect. Caller must call connectToServer() if desired.
*
* @param host
* @param port
* @param user
* @param pass
*/
public POP3MailBox(String host, int port, String user, String pass) {
Debug.debug(
Debug.DEBUG,
"Mailbox(" + host + "," + port + "," + user + ",password)");
this.host = host;
this.port = port;
this.user = user;
this.pass = pass;
uidlToID = new HashMap<String, Integer>();
sizes = new HashMap<Integer, Integer>();
synchronizer = new Object();
// this appears in the UI so translate
lastLine = _t("No response from server");
lastActive = new AtomicLong(System.currentTimeMillis());
lastChecked = new AtomicLong();
delayedDeleter = new DelayedDeleter(this);
}
/**
* Fetch the header. Does not cache.
*
* @param uidl
* @return Byte buffer containing header data or null
*/
public ReadBuffer getHeader( String uidl ) {
synchronized( synchronizer ) {
try {
// we must be connected to know the UIDL to ID mapping
checkConnection();
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error fetching header: " + ioe);
return null;
}
int id = getIDfromUIDL(uidl);
if (id < 0)
return null;
return getHeader(id);
}
}
/**
* retrieves header from pop3 server (with TOP command and RETR as fallback)
* Caller must sync.
*
* @param id message id
* @return Byte buffer containing header data or null
*/
private ReadBuffer getHeader( int id ) {
Debug.debug(Debug.DEBUG, "getHeader(" + id + ")");
ReadBuffer header = null;
if (id >= 1 && id <= mails) {
/*
* try 'TOP n 0' command
*/
header = sendCmdN("TOP " + id + " 0" );
if( header == null) {
/*
* try 'RETR n' command
*/
header = sendCmdN("RETR " + id );
if (header == null)
Debug.debug( Debug.DEBUG, "RETR returned null" );
}
} else {
lastError = "Message id out of range.";
}
return header;
}
/**
* Fetch the body. Does not cache.
*
* @param uidl
* @return Byte buffer containing body data or null
*/
public ReadBuffer getBody( String uidl ) {
synchronized( synchronizer ) {
try {
// we must be connected to know the UIDL to ID mapping
checkConnection();
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error fetching body: " + ioe);
return null;
}
int id = getIDfromUIDL(uidl);
if (id < 0)
return null;
return getBody(id);
}
}
/**
* Fetch headers and/or bodies. Does not cache.
* ReadBuffer objects are inserted into the requests.
* No total time limit.
*
* @since 0.9.13
*/
public void getBodies(Collection<FetchRequest> requests) {
List<SendRecv> srs = new ArrayList<SendRecv>(requests.size());
synchronized( synchronizer ) {
try {
// we must be connected to know the UIDL to ID mapping
checkConnection();
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error fetching: " + ioe);
return;
}
for (FetchRequest fr : requests) {
int id = getIDfromUIDL(fr.getUIDL());
if (id < 0)
continue;
SendRecv sr;
if (fr.getHeaderOnly() && supportsTOP)
sr = new SendRecv("TOP " + id + " 0", Mode.RB);
else
sr = new SendRecv("RETR " + id, Mode.RB);
sr.savedObject = fr;
srs.add(sr);
}
if (srs.isEmpty())
return;
try {
sendCmds(srs);
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error fetching bodies: " + ioe);
// todo maybe
}
}
for (SendRecv sr : srs) {
if (sr.result) {
FetchRequest fr = (FetchRequest) sr.savedObject;
fr.setBuffer(sr.rb);
}
}
}
/**
* retrieve message body from pop3 server (via RETR command)
* Caller must sync.
*
* @param id message id
* @return Byte buffer containing body data or null
*/
private ReadBuffer getBody(int id) {
Debug.debug(Debug.DEBUG, "getBody(" + id + ")");
ReadBuffer body = null;
if (id >= 1 && id <= mails) {
try {
body = sendCmdN( "RETR " + id );
if (body == null)
Debug.debug( Debug.DEBUG, "RETR returned null" );
} catch (OutOfMemoryError oom) {
Debug.debug( Debug.ERROR, "OOM fetching mail" );
lastError = oom.toString();
close();
}
}
else {
lastError = "Message id out of range.";
}
return body;
}
/**
* Call performDelete() after this or they will come back
* UNUSED
*
* @param uidl
* @return Success of delete operation: true if successful.
*/
/****
public boolean delete( String uidl )
{
Debug.debug(Debug.DEBUG, "delete(" + uidl + ")");
synchronized( synchronizer ) {
try {
// we must be connected to know the UIDL to ID mapping
checkConnection();
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error deleting: " + ioe);
return false;
}
int id = getIDfromUIDL(uidl);
if (id < 0)
return false;
return delete(id);
}
}
****/
/**
* Queue for later deletion. Non-blocking.
*
* @since 0.9.13
*/
public void queueForDeletion(Collection<String> uidls) {
for (String uidl : uidls) {
queueForDeletion(uidl);
}
}
/**
* Queue for later deletion. Non-blocking.
*
* @since 0.9.13
*/
public void queueForDeletion(String uidl) {
Debug.debug(Debug.DEBUG, "Queueing for deletion: " + uidl);
delayedDeleter.queueDelete(uidl);
}
/**
* Delete all at once and close. Does not reconnect.
* Do NOT call performDelete() after this.
* Returns all UIDLs successfully deleted OR were not known by the server.
*
* @since 0.9.13
*/
Collection<String> delete(Collection<String> uidls) {
List<String> rv = new ArrayList<String>(uidls.size());
List<SendRecv> srs = new ArrayList<SendRecv>(uidls.size() + 1);
synchronized( synchronizer ) {
try {
// we must be connected to know the UIDL to ID mapping
checkConnection();
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error deleting: " + ioe);
return rv;
}
for (String uidl : uidls) {
int id = getIDfromUIDL(uidl);
if (id < 0) {
// presumed already deleted
rv.add(uidl);
continue;
}
SendRecv sr = new SendRecv("DELE " + id, Mode.A1);
sr.savedObject = uidl;
srs.add(sr);
}
if (srs.isEmpty())
return rv;
// TODO don't quit now, just set timer to quit later
SendRecv quit = new SendRecv("QUIT", Mode.A1);
srs.add(quit);
try {
sendCmds(srs);
// do NOT call close() here, we included QUIT above
try {
socket.close();
} catch (IOException e) {}
clear();
// result of QUIT
boolean success = srs.get(srs.size() - 1).result;
if (success) {
for (int i = 0; i < srs.size() - 1; i++) {
SendRecv sr = srs.get(i);
// ignore sr.result, if it failed it's because
// it's already deleted
rv.add((String) sr.savedObject);
}
}
// why reconnect?
//connect();
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error deleting: " + ioe);
// todo maybe
}
}
return rv;
}
/**
* delete message on pop3 server
* UNUSED
*
* @param id message id
* @return Success of delete operation: true if successful.
*/
/****
private boolean delete(int id)
{
Debug.debug(Debug.DEBUG, "delete(" + id + ")");
boolean result = false;
synchronized( synchronizer ) {
try {
result = sendCmd1a( "DELE " + id );
}
catch (IOException e) {
}
}
return result;
}
****/
/**
* Get cached size of a message (via previous LIST command).
*
* @param uidl
* @return Message size in bytes or 0 if not found
*/
public int getSize( String uidl ) {
synchronized( synchronizer ) {
int id = getIDfromUIDL(uidl);
if (id < 0)
return 0;
return getSize(id);
}
}
/**
* Get cached size of a message (via previous LIST command).
* Caller must sync.
*
* @param id message id
* @return Message size in bytes or 0 if not found
*/
private int getSize(int id) {
int result = 0;
/*
* find value in hashtable
*/
Integer resultObj = sizes.get(Integer.valueOf(id));
if (resultObj != null)
result = resultObj.intValue();
Debug.debug(Debug.DEBUG, "getSize(" + id + ") = " + result);
return result;
}
/**
* Is the connection is still alive
*
* @return true or false
*/
public boolean isConnected() {
if (socket == null
|| !socket.isConnected()
|| socket.isInputShutdown()
|| socket.isOutputShutdown()
|| socket.isClosed()) {
connected = false;
}
return connected;
}
/**
* If not connected, connect now.
* Should be called from all public methods before sending a command.
* Caller must sync.
*
* @return true or false
*/
private void checkConnection() throws IOException {
Debug.debug(Debug.DEBUG, "checkConnection()");
if (!isConnected()) {
connect();
if (!isConnected())
throw new IOException("Cannot connect");
}
}
/**
* Timestamp.
*
* @since 0.9.13
*/
private void updateActivity() {
lastActive.set(System.currentTimeMillis());
}
/**
* Timestamp.
*
* @since 0.9.13
*/
long getLastActivity() {
return lastActive.get();
}
/**
* Timestamp. When we last successfully got the UIDL list.
*
* @since 0.9.13
*/
long getLastChecked() {
return lastChecked.get();
}
/**
*
* @param response line starting with +OK
*/
private void updateMailCount(String response) {
if (response == null || response.length() < 4) {
mails = 0;
return;
}
response = response.trim();
try {
int i = response.indexOf(' ', 5);
mails =
Integer.parseInt(
i != -1
? response.substring(4, i)
: response.substring(4));
} catch (NumberFormatException nfe) {
mails = 0;
}
}
/**
* Caller must sync.
*
* @throws IOException
*/
private void updateUIDLs(List<String> lines) {
uidlToID.clear();
if (lines != null) {
for (String line : lines) {
int j = line.indexOf(' ');
if( j != -1 ) {
try {
int n = Integer.parseInt( line.substring( 0, j ) );
String uidl = line.substring(j + 1).trim();
uidlToID.put( uidl, Integer.valueOf( n ) );
} catch (NumberFormatException nfe) {
Debug.debug(Debug.DEBUG, "UIDL error " + nfe);
} catch (IndexOutOfBoundsException ioobe) {
Debug.debug(Debug.DEBUG, "UIDL error " + ioobe);
}
}
}
lastChecked.set(System.currentTimeMillis());
} else {
Debug.debug(Debug.DEBUG, "Error getting UIDL list from server.");
}
}
/**
* Caller must sync.
*
* @throws IOException
*/
private void updateSizes(List<String> lines) {
/*
* try LIST
*/
sizes.clear();
if (lines != null) {
for (String line : lines) {
int j = line.indexOf(' ');
if (j != -1) {
try {
int key = Integer.parseInt(line.substring(0, j));
int value = Integer.parseInt(line.substring(j + 1).trim());
sizes.put(Integer.valueOf(key), Integer.valueOf(value));
} catch (NumberFormatException nfe) {
Debug.debug(Debug.DEBUG, "LIST error " + nfe);
}
}
}
} else {
Debug.debug(Debug.DEBUG, "Error getting LIST from server.");
}
}
/**
*
*
*/
public void refresh() {
synchronized( synchronizer ) {
close(true);
connect();
}
}
/**
* Caller must sync.
*/
private void clear()
{
uidlToID.clear();
sizes.clear();
mails = 0;
}
/**
* Connect to pop3 server if not connected.
* Does nothing if already connected.
*
* @return true if connected
* @since 0.9.13
*/
public boolean connectToServer() {
synchronized( synchronizer ) {
if (isConnected())
return true;
connect();
return isConnected();
}
}
/**
* connect to pop3 server, login with USER and PASS and try STAT then
*
* Caller must sync.
*/
private void connect() {
Debug.debug(Debug.DEBUG, "connect()");
if (Debug.getLevel() == Debug.DEBUG)
(new Exception("I did it")).printStackTrace();
clear();
if (socket != null && socket.isConnected())
close();
try {
socket = new Socket(host, port);
} catch (UnknownHostException e) {
lastError = e.toString();
return;
} catch (IOException e) {
Debug.debug( Debug.DEBUG, "Error connecting: " + e);
lastError = e.toString();
return;
}
if (socket != null) {
try {
// pipeline 2 commands
lastError = "";
socket.setSoTimeout(120*1000);
boolean ok = doHandshake();
if (ok) {
// TODO APOP (unsupported by postman)
List<SendRecv> cmds = new ArrayList<SendRecv>(4);
cmds.add(new SendRecv("USER " + user, Mode.A1));
cmds.add(new SendRecv("PASS " + pass, Mode.A1));
socket.setSoTimeout(60*1000);
ok = sendCmds(cmds);
}
if (ok) {
connected = true;
List<SendRecv> cmds = new ArrayList<SendRecv>(4);
SendRecv stat = new SendRecv("STAT", Mode.A1);
cmds.add(stat);
SendRecv uidl = new SendRecv("UIDL", Mode.LS);
cmds.add(uidl);
SendRecv list = new SendRecv("LIST", Mode.LS);
cmds.add(list);
// check individual responses
socket.setSoTimeout(120*1000);
ok = sendCmds(cmds);
if (stat.result)
updateMailCount(stat.response);
else
Debug.debug(Debug.DEBUG, "STAT failed");
if (uidl.result)
updateUIDLs(uidl.ls);
else
Debug.debug(Debug.DEBUG, "UIDL failed");
if (list.result)
updateSizes(list.ls);
else
Debug.debug(Debug.DEBUG, "LIST failed");
socket.setSoTimeout(300*1000);
if (ok && backgroundChecker == null &&
Boolean.parseBoolean(Config.getProperty(WebMail.CONFIG_BACKGROUND_CHECK)))
backgroundChecker = new BackgroundChecker(this);
if (ok && idleCloser == null)
idleCloser = new IdleCloser(this);
} else {
if (lastError.equals(""))
lastError = _t("Error connecting to server");
close();
}
}
catch (NumberFormatException e1) {
lastError = _t("Error opening mailbox") + ": " + e1;
}
catch (IOException e1) {
lastError = _t("Error opening mailbox") + ": " + e1.getLocalizedMessage();
}
}
}
/**
* Check the initial response, send CAPA, check the CAPA result
* Caller must sync.
*
* @return true if successful
* @throws IOException
* @since 0.9.13
*/
private boolean doHandshake() throws IOException {
List<SendRecv> cmds = new ArrayList<SendRecv>(2);
cmds.add(new SendRecv(null, Mode.A1));
SendRecv capa = null;
if (gotCAPA) {
Debug.debug(Debug.DEBUG, "Skipping CAPA");
} else {
capa = new SendRecv("CAPA", Mode.LS);
cmds.add(capa);
}
boolean rv = sendCmds(cmds);
if (rv && capa != null) {
if (capa.ls != null) {
for (String cap : capa.ls) {
String t = cap.trim();
if (t.equals("PIPELINING"))
supportsPipelining = true;
else if (t.equals("UIDL"))
supportsUIDL = true;
else if (t.equals("TOP"))
supportsTOP = true;
}
}
gotCAPA = true;
Debug.debug(Debug.DEBUG, "POP3 server caps: pipelining? " + supportsPipelining +
" UIDL? " + supportsUIDL +
" TOP? " + supportsTOP);
}
return rv;
}
/**
* send command to pop3 server (and expect single line answer)
* Response will be in lastLine. Does not read past the first line of the response.
* Caller must sync.
*
* @param cmd command to send
* @return true if command was successful (+OK)
* @throws IOException
*/
private boolean sendCmd1a(String cmd) throws IOException {
boolean result = false;
sendCmd1aNoWait(cmd);
socket.getOutputStream().flush();
String foo = DataHelper.readLine(socket.getInputStream());
updateActivity();
// Debug.debug(Debug.DEBUG, "sendCmd1a: read " + read + " bytes");
if (foo != null) {
lastLine = foo;
if (lastLine.startsWith("+OK")) {
if (cmd.startsWith("PASS"))
cmd = "PASS provided";
Debug.debug(Debug.DEBUG, "sendCmd1a: (" + cmd + ") success: \"" + lastLine.trim() + '"');
result = true;
} else {
if (cmd.startsWith("PASS"))
cmd = "PASS provided";
Debug.debug(Debug.DEBUG, "sendCmd1a: (" + cmd + ") FAIL: \"" + lastLine.trim() + '"');
lastError = lastLine;
}
} else {
Debug.debug(Debug.DEBUG, "sendCmd1a: (" + cmd + ") NO RESPONSE");
lastError = _t("No response from server");
throw new IOException(lastError);
}
return result;
}
/**
* Send commands to pop3 server all at once (and expect answers).
* Sets lastError to the FIRST error.
* Caller must sync.
*
* @param cmd command to send
* @param rcvLines lines to receive
* @return true if ALL received lines were successful (+OK)
* @throws IOException
* @since 0.9.13
*/
private boolean sendCmds(List<SendRecv> cmds) throws IOException {
boolean result = true;
boolean pipe = supportsPipelining;
if (pipe) {
Debug.debug(Debug.DEBUG, "POP3 pipelining " + cmds.size() + " commands");
for (SendRecv sr : cmds) {
String cmd = sr.send;
if (cmd != null)
sendCmd1aNoWait(cmd);
}
} // else we will do it below
socket.getOutputStream().flush();
InputStream in = socket.getInputStream();
int i = 0;
for (SendRecv sr : cmds) {
if (!pipe) {
String cmd = sr.send;
if (cmd != null) {
sendCmd1aNoWait(cmd);
socket.getOutputStream().flush();
}
}
String foo = DataHelper.readLine(in);
updateActivity();
if (foo == null) {
lastError = _t("No response from server");
throw new IOException(lastError);
}
sr.response = foo.trim();
i++;
if (!foo.startsWith("+OK")) {
Debug.debug(Debug.DEBUG, "Fail after " + i + " of " + cmds.size() + " responses: \"" + foo.trim() + '"');
if (result)
lastError = foo; // actually the first error, for better info to the user
result = false;
sr.result = false;
} else {
Debug.debug(Debug.DEBUG, "OK after " + i + " of " + cmds.size() + " responses: \"" + foo.trim() + '"');
switch (sr.mode) {
case A1:
sr.result = true;
break;
case RB:
try {
sr.rb = getResultNa();
sr.result = true;
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error getting RB: " + ioe);
result = false;
sr.result = false;
}
break;
case LS:
try {
sr.ls = getResultNl();
sr.result = true;
} catch (IOException ioe) {
Debug.debug( Debug.DEBUG, "Error getting LS: " + ioe);
result = false;
sr.result = false;
}
break;
}
}
lastLine = foo;
}
return result;
}
/**
* send command to pop3 server. Does NOT flush or read or wait.
* Caller must sync.
*
* @param cmd command to send non-null
* @throws IOException
* @since 0.9.13
*/
private void sendCmd1aNoWait(String cmd) throws IOException {
/*
* dont log password
*/
String msg = cmd;
if (msg.startsWith("PASS"))
msg = "PASS provided";
Debug.debug(Debug.DEBUG, "sendCmd1a(" + msg + ")");
cmd += "\r\n";
socket.getOutputStream().write(DataHelper.getASCII(cmd));
updateActivity();
}
/**
* Tries twice
* Caller must sync.
*
* @return buffer or null
*/
private ReadBuffer sendCmdN(String cmd )
{
synchronized (synchronizer) {
try {
return sendCmdNa(cmd);
} catch (IOException e) {
lastError = e.toString();
Debug.debug( Debug.DEBUG, "sendCmdNa throws: " + e);
}
connect();
if (connected) {
try {
return sendCmdNa(cmd);
} catch (IOException e2) {
lastError = e2.toString();
Debug.debug( Debug.DEBUG, "2nd sendCmdNa throws: " + e2);
}
} else {
Debug.debug( Debug.DEBUG, "not connected after reconnect" );
}
}
return null;
}
/**
* No total timeout (result could be large)
* Caller must sync.
*
* @return buffer or null
* @throws IOException
*/
private ReadBuffer sendCmdNa(String cmd) throws IOException
{
if (sendCmd1a(cmd)) {
return getResultNa();
} else {
Debug.debug( Debug.DEBUG, "sendCmd1a returned false" );
return null;
}
}
/**
* Like sendCmdNa but returns a list of strings, one per line.
* Strings will have trailing \r but not \n.
* Total timeout 2 minutes.
* Caller must sync.
*
* @return the lines or null on error
* @throws IOException on timeout
* @since 0.9.13
*/
private List<String> sendCmdNl(String cmd) throws IOException
{
if (sendCmd1a(cmd)) {
return getResultNl();
} else {
Debug.debug( Debug.DEBUG, "sendCmd1a returned false" );
return null;
}
}
/**
* No total timeout (result could be large)
* Caller must sync.
*
* @return buffer non-null
* @throws IOException
*/
private ReadBuffer getResultNa() throws IOException
{
InputStream input = socket.getInputStream();
StringBuilder buf = new StringBuilder(512);
ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
while (DataHelper.readLine(input, buf)) {
updateActivity();
int len = buf.length();
if (len == 0)
break; // huh? no \r?
if (len == 2 && buf.charAt(0) == '.' && buf.charAt(1) == '\r')
break;
String line;
// RFC 1939 sec. 3 de-byte-stuffing
if (buf.charAt(0) == '.')
line = buf.substring(1);
else
line = buf.toString();
baos.write(DataHelper.getASCII(line));
if (buf.charAt(len - 1) != '\r')
baos.write((byte) '\n');
baos.write((byte) '\n');
buf.setLength(0);
}
return new ReadBuffer(baos.toByteArray(), 0, baos.size());
}
/**
* Like getResultNa but returns a list of strings, one per line.
* Strings will have trailing \r but not \n.
* Total timeout 2 minutes.
* Caller must sync.
*
* @return the lines non-null
* @throws IOException on timeout
* @since 0.9.13
*/
private List<String> getResultNl() throws IOException
{
List<String> rv = new ArrayList<String>(16);
long timeOut = 120*1000;
InputStream input = socket.getInputStream();
long startTime = System.currentTimeMillis();
StringBuilder buf = new StringBuilder(512);
while (DataHelper.readLine(input, buf)) {
updateActivity();
int len = buf.length();
if (len == 0)
break; // huh? no \r?
if (len == 2 && buf.charAt(0) == '.' && buf.charAt(1) == '\r')
break;
if( System.currentTimeMillis() - startTime > timeOut )
throw new IOException( "Timeout while waiting on server response." );
String line;
// RFC 1939 sec. 3 de-byte-stuffing
if (buf.charAt(0) == '.')
line = buf.substring(1);
else
line = buf.toString();
rv.add(line);
buf.setLength(0);
}
return rv;
}
/**
* Warning - forces a connection.
*
* @return The amount of e-mails available.
*/
public int getNumMails() {
synchronized( synchronizer ) {
Debug.debug(Debug.DEBUG, "getNumMails()");
try {
checkConnection();
} catch (IOException ioe) {}
return connected ? mails : 0;
}
}
/**
* @return The most recent error message.
*/
public String lastError() {
//Debug.debug(Debug.DEBUG, "lastError()");
// Hide the "-ERR" from the user
String e = lastError;
if (e.startsWith("-ERR ") && e.length() > 5)
e = e.substring(5);
// translate this common error
if (e.trim().equals("Login failed."))
e = _t("Login failed");
return e;
}
/**
* Relay from the checker to the webmail session object,
* which relays to MailCache, which will fetch the mail from us
* in a big circle
*
* @since 0.9.13
*/
public void setNewMailListener(NewMailListener nml) {
newMailListener = nml;
}
/**
* Relay from the checker to the webmail session object,
* which relays to MailCache, which will fetch the mail from us
* in a big circle
*
* @since 0.9.13
*/
public void foundNewMail() {
NewMailListener nml = newMailListener;
if (nml != null)
nml.foundNewMail();
}
/**
* Close without waiting for response,
* and remove any delayed tasks and resources.
*/
public void destroy() {
delayedDeleter.cancel();
synchronized( synchronizer ) {
if (backgroundChecker != null)
backgroundChecker.cancel();
close(false);
}
}
/**
* For helper threads to lock
* @since 0.9.13
*/
Object getLock() {
return synchronizer;
}
/**
* Do we have UIDLs to delete?
* @since 0.9.13
*/
boolean hasQueuedDeletions() {
return !delayedDeleter.getQueued().isEmpty();
}
/**
* Close without waiting for response.
* Deletes all queued deletions.
*/
public void close() {
close(false);
}
/**
* Close and optionally wait for response.
* Deletes all queued deletions.
* @since 0.9.13
*/
void close(boolean shouldWait) {
synchronized( synchronizer ) {
Debug.debug(Debug.DEBUG, "close()");
if (idleCloser != null)
idleCloser.cancel();
if (socket != null && socket.isConnected()) {
try {
Collection<String> toDelete = delayedDeleter.getQueued();
Map<String, Integer> sendDelete = new HashMap<String, Integer>(toDelete.size());
for (String uidl : toDelete) {
int id = getIDfromUIDL(uidl);
if (id >= 0) {
sendDelete.put(uidl, Integer.valueOf(id));
}
}
if (shouldWait) {
if (!sendDelete.isEmpty()) {
// Verify deleted, remove from the delete queue
// this does the quit and close
Collection<String> deleted = delete(sendDelete.keySet());
for (String uidl : deleted) {
delayedDeleter.removeQueued(uidl);
}
} else {
sendCmd1a("QUIT");
}
Debug.debug( Debug.DEBUG, "close() with wait complete");
} else {
if (!sendDelete.isEmpty()) {
// spray and pray the deletions, don't remove from delete queue
for (Integer id : sendDelete.values()) {
sendCmd1aNoWait("DELE " + id);
}
}
sendCmd1aNoWait("QUIT");
}
socket.close();
} catch (IOException e) {
Debug.debug( Debug.DEBUG, "error closing: " + e);
}
}
socket = null;
connected = false;
clear();
}
}
/**
* returns number of message with given UIDL
* Caller must sync.
*
* @param uidl
* @return Message number or -1
*/
private int getIDfromUIDL( String uidl )
{
int result = -1;
Integer intObject = uidlToID.get( uidl );
if( intObject != null ) {
result = intObject.intValue();
}
return result;
}
/**
* Unused
* @param id
* @return UIDL or null
*/
/****
public String getUIDLfromID( int id )
{
synchronized( synchronizer ) {
try {
return uidlList.get( id );
} catch (IndexOutOfBoundsException ioobe) {
return null;
}
}
}
****/
/**
* Only if connected. Does not force a connect.
* If not connected, returns null.
*
* @return A new array of the available UIDLs. No particular order.
*/
public Collection<String> getUIDLs()
{
if (!isConnected())
return null;
synchronized( synchronizer ) {
return new ArrayList<String>(uidlToID.keySet());
}
}
/**
*
* @param args
*/
/****
public static void main( String[] args )
{
Debug.setLevel( Debug.DEBUG );
POP3MailBox mailbox = new POP3MailBox( "localhost", 7660 , "test", "test");
ReadBuffer readBuffer = mailbox.sendCmdN( "LIST" );
System.out.println( "list='" + readBuffer + "'" );
}
****/
/**
* Close and reconnect. Takes a while.
* UNUSED
*/
/****
public void performDelete()
{
synchronized( synchronizer ) {
close(true);
// why reconnect?
//connect();
}
}
****/
/** for SendRecv */
private enum Mode {
/** no extra lines (sendCmd1a) */
A1,
/** return extra lines in ReadBuffer (sendCmdNa) */
RB,
/** return extra lines in List of Strings (sendCmdNl) */
LS
}
/**
* A command to send and a mode to receive and return the results
* @since 0.9.13
*/
private static class SendRecv {
public final String send;
public final Mode mode;
public String response;
public boolean result;
public ReadBuffer rb;
public List<String> ls;
// to remember things
public Object savedObject;
/** @param s may be null */
public SendRecv(String s, Mode m) {
send = s;
mode = m;
}
}
public interface FetchRequest {
public String getUIDL();
public boolean getHeaderOnly();
public void setBuffer(ReadBuffer buffer);
}
/** translate */
private static String _t(String s) {
return Messages.getString(s);
}
}