diff --git a/src/java/davmail/AbstractConnection.java b/src/java/davmail/AbstractConnection.java index 602f26f5..aa33df8c 100644 --- a/src/java/davmail/AbstractConnection.java +++ b/src/java/davmail/AbstractConnection.java @@ -1,6 +1,7 @@ package davmail; import davmail.exchange.ExchangeSession; +import davmail.exchange.ExchangeSessionFactory; import davmail.smtp.SmtpConnection; import davmail.tray.DavGatewayTray; import org.apache.commons.httpclient.util.Base64; @@ -142,13 +143,7 @@ public class AbstractConnection extends Thread { } catch (IOException e2) { DavGatewayTray.warn("Exception closing client socket", e2); } - try { - if (session != null) { - session.close(); - } - } catch (IOException e3) { - DavGatewayTray.warn("Exception closing gateway", e3); - } + ExchangeSessionFactory.close(session); } protected String base64Encode(String value) { diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java index 9b15d1f1..61b4560e 100644 --- a/src/java/davmail/exchange/ExchangeSession.java +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -64,11 +64,20 @@ public class ExchangeSession { private String currentFolderUrl; private WebdavResource wdr = null; + private ExchangeSessionFactory.PoolKey poolKey; + + ExchangeSessionFactory.PoolKey getPoolKey() { + return poolKey; + } + /** * Create an exchange session for the given URL. * The session is not actually established until a call to login() + * + * @param poolKey session pool key */ - ExchangeSession() { + ExchangeSession(ExchangeSessionFactory.PoolKey poolKey) { + this.poolKey = poolKey; // SimpleDateFormat are not thread safe, need to create one instance for // each session dateParser = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); @@ -76,6 +85,31 @@ public class ExchangeSession { LOGGER.debug("Session " + this + " created"); } + public boolean isExpired() { + boolean isExpired = false; + HttpMethod testMethod = null; + try { + testMethod = DavGatewayHttpClientFacade.executeFollowRedirects(wdr.retrieveSessionInstance(), + URIUtil.encodePath(currentFolderUrl)); + String queryString = testMethod.getQueryString(); + + if (testMethod.getStatusCode() != HttpStatus.SC_OK) { + isExpired = true; + } else if (queryString != null && queryString.contains("reason=")) { + isExpired = true; + } + + } catch (IOException e) { + isExpired = true; + } finally { + if (testMethod != null) { + testMethod.releaseConnection(); + } + } + + return isExpired; + } + /** * Test authentication mode : form based or basic. * @@ -197,25 +231,23 @@ public class ExchangeSession { return result; } - void login(String userName, String password) throws IOException { + void login() throws IOException { LOGGER.debug("Session " + this + " login"); try { - String baseUrl = Settings.getProperty("davmail.url"); - - boolean isBasicAuthentication = isBasicAuthentication(baseUrl); + boolean isBasicAuthentication = isBasicAuthentication(poolKey.url); // get proxy configuration from setttings properties - URL urlObject = new URL(baseUrl); + URL urlObject = new URL(poolKey.url); // webdavresource is unable to create the correct url type HttpURL httpURL; - if (baseUrl.startsWith("http://")) { - httpURL = new HttpURL(userName, password, + if (poolKey.url.startsWith("http://")) { + httpURL = new HttpURL(poolKey.userName, poolKey.password, urlObject.getHost(), urlObject.getPort()); - } else if (baseUrl.startsWith("https://")) { - httpURL = new HttpsURL(userName, password, + } else if (poolKey.url.startsWith("https://")) { + httpURL = new HttpsURL(poolKey.userName, poolKey.password, urlObject.getHost(), urlObject.getPort()); } else { - throw new IllegalArgumentException("Invalid URL: " + baseUrl); + throw new IllegalArgumentException("Invalid URL: " + poolKey.url); } wdr = new WebdavResource(httpURL, WebdavResource.NOACTION, 0); @@ -230,10 +262,10 @@ public class ExchangeSession { // get webmail root url // providing credentials // manually follow redirect - HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, baseUrl); + HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, poolKey.url); if (!isBasicAuthentication) { - method = formLogin(httpClient, method, userName, password); + method = formLogin(httpClient, method, poolKey.userName, poolKey.password); // reexecute method with new base URL // method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, baseUrl); } @@ -250,7 +282,7 @@ public class ExchangeSession { String queryString = method.getQueryString(); if (queryString != null && queryString.contains("reason=2")) { method.releaseConnection(); - if (userName != null && userName.contains("\\")) { + if (poolKey.userName != null && poolKey.userName.contains("\\")) { throw new HttpException("Authentication failed: invalid user or password"); } else { throw new HttpException("Authentication failed: invalid user or password, " + @@ -261,7 +293,7 @@ public class ExchangeSession { mailPath = getMailPath(method); if (mailPath == null) { - throw new HttpException(baseUrl + " not found in body, authentication failed: password expired ?"); + throw new HttpException(poolKey.url + " not found in body, authentication failed: password expired ?"); } // got base http mailbox http url @@ -343,12 +375,14 @@ public class ExchangeSession { /** * Close session. * This will only close http client, not the actual Exchange session - * - * @throws IOException if unable to close Webdav context */ - public void close() throws IOException { - LOGGER.debug("Session " + this + " closed"); - wdr.close(); + void close() { + try { + wdr.close(); + LOGGER.debug("Session " + this + " closed"); + } catch (IOException e) { + LOGGER.warn("Exception closing session", e); + } } /** @@ -422,14 +456,9 @@ public class ExchangeSession { public Message getMessage(String messageUrl) throws IOException { - // TODO switch according to Log4J log level - //wdr.setDebug(4); - //wdr.propfindMethod(messageUrl, 0); Enumeration messageEnum = wdr.propfindMethod(messageUrl, 0, MESSAGE_REQUEST_PROPERTIES); - //wdr.setDebug(0); - // 201 created in some cases ?!? - if ((wdr.getStatusCode() != HttpURLConnection.HTTP_OK && wdr.getStatusCode() != HttpURLConnection.HTTP_CREATED) + if ((wdr.getStatusCode() != HttpURLConnection.HTTP_OK) || !messageEnum.hasMoreElements()) { throw new IOException("Unable to get message: " + wdr.getStatusCode() + " " + wdr.getStatusMessage()); @@ -771,7 +800,7 @@ public class ExchangeSession { " FROM Scope('SHALLOW TRAVERSAL OF \"" + calendarUrl + "\"')\n" + " WHERE NOT \"urn:schemas:calendar:instancetype\" = 1\n" + " AND \"DAV:contentclass\" = 'urn:content-classes:appointment'\n" + - " AND \"urn:schemas:calendar:dtstart\" > '2008/11/01 00:00:00'\n" + +// " AND \"urn:schemas:calendar:dtstart\" > '2008/11/01 00:00:00'\n" + " ORDER BY \"urn:schemas:calendar:dtstart\" ASC\n" + " \n" + ""; @@ -965,10 +994,10 @@ public class ExchangeSession { } results = XMLStreamUtil.getElementContentsAsMap(getMethod.getResponseBodyAsStream(), "item", "AN"); // add detailed information, only if few results - if (results.size() <=10) { - for (Map person : results.values()) { - galLookup(person); - } + if (results.size() <= 10) { + for (Map person : results.values()) { + galLookup(person); + } } } finally { getMethod.releaseConnection(); diff --git a/src/java/davmail/exchange/ExchangeSessionFactory.java b/src/java/davmail/exchange/ExchangeSessionFactory.java index 178d72d9..5f95efe9 100644 --- a/src/java/davmail/exchange/ExchangeSessionFactory.java +++ b/src/java/davmail/exchange/ExchangeSessionFactory.java @@ -11,12 +11,69 @@ import java.io.IOException; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; -import java.util.Enumeration; +import java.util.*; /** * Create ExchangeSession instances. */ public class ExchangeSessionFactory { + private static final Object LOCK = new Object(); + private static final Map poolMap = new HashMap(); + + static class PoolKey { + public String url; + public String userName; + public String password; + + public PoolKey(String url, String userName, String password) { + this.url = url; + this.userName = userName; + this.password = password; + } + + @Override + public boolean equals(Object object) { + return object == this || + object instanceof PoolKey && + ((PoolKey) object).url.equals(this.url) && + ((PoolKey) object).userName.equals(this.userName) && + ((PoolKey) object).password.equals(this.password); + } + + @Override + public int hashCode() { + return url.hashCode() + userName.hashCode() + password.hashCode(); + } + } + + static class ExchangeSessionStack extends Stack { + // 15 minutes expire delay + protected static final long EXPIRE_DELAY = 1000 * 60 * 15; + protected long timestamp = System.currentTimeMillis(); + + @Override + public ExchangeSession pop() throws EmptyStackException { + timestamp = System.currentTimeMillis(); + return super.pop(); + } + + @Override + public ExchangeSession push(ExchangeSession session) { + timestamp = System.currentTimeMillis(); + return super.push(session); + } + + public boolean isExpired() { + return (System.currentTimeMillis() - timestamp) > EXPIRE_DELAY; + } + + public void clean() { + while (!isEmpty()) { + pop().close(); + } + } + } + /** * Create authenticated Exchange session * @@ -27,8 +84,29 @@ public class ExchangeSessionFactory { */ public static ExchangeSession getInstance(String userName, String password) throws IOException { try { - ExchangeSession session = new ExchangeSession(); - session.login(userName, password); + String baseUrl = Settings.getProperty("davmail.url"); + PoolKey poolKey = new PoolKey(baseUrl, userName, password); + + ExchangeSession session = null; + synchronized (LOCK) { + Stack sessionStack = poolMap.get(poolKey); + if (sessionStack != null && !sessionStack.isEmpty()) { + session = sessionStack.pop(); + ExchangeSession.LOGGER.debug("Got session " + session + " from pool"); + } + } + + if (session != null && session.isExpired()) { + ExchangeSession.LOGGER.debug("Session " + session + " expired"); + session.close(); + session = null; + } + + if (session == null) { + session = new ExchangeSession(poolKey); + session.login(); + ExchangeSession.LOGGER.debug("Created new session: " + session); + } return session; } catch (IOException e) { if (checkNetwork()) { @@ -39,6 +117,41 @@ public class ExchangeSessionFactory { } } + /** + * Close (or pool) session. + * + * @param session exchange session + */ + public static void close(ExchangeSession session) { + synchronized (LOCK) { + if (session != null) { + PoolKey poolKey = session.getPoolKey(); + ExchangeSessionStack sessionStack = poolMap.get(poolKey); + if (sessionStack == null) { + sessionStack = new ExchangeSessionStack(); + poolMap.put(poolKey, sessionStack); + } + // keep httpClient, but close HTTP connection + session.close(); + sessionStack.push(session); + ExchangeSession.LOGGER.debug("Pooled session: " + session); + } + + // clean pool + List toDeleteKeys = new ArrayList(); + for (Map.Entry entry : poolMap.entrySet()) { + if (entry.getValue().isExpired()) { + ExchangeSession.LOGGER.debug("Session pool for " + entry.getKey().userName + " expired"); + entry.getValue().clean(); + toDeleteKeys.add(entry.getKey()); + } + } + for (PoolKey toDeleteKey : toDeleteKeys) { + poolMap.remove(toDeleteKey); + } + } + } + public static void checkConfig() throws IOException { String url = Settings.getProperty("davmail.url"); @@ -49,7 +162,6 @@ public class ExchangeSessionFactory { testMethod.setFollowRedirects(false); testMethod.setDoAuthentication(false); int status = httpClient.executeMethod(testMethod); - testMethod.releaseConnection(); ExchangeSession.LOGGER.debug("Test configuration status: " + status); if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED && status != HttpStatus.SC_MOVED_TEMPORARILY) { diff --git a/src/java/davmail/ldap/LdapConnection.java b/src/java/davmail/ldap/LdapConnection.java index e881d9f0..02806904 100644 --- a/src/java/davmail/ldap/LdapConnection.java +++ b/src/java/davmail/ldap/LdapConnection.java @@ -178,7 +178,7 @@ public class LdapConnection extends AbstractConnection { } else if (operation == LDAP_REQ_UNBIND) { if (session != null) { - session.close(); + ExchangeSessionFactory.close(session); session = null; } } else if (operation == LDAP_REQ_SEARCH) { diff --git a/src/test/davmail/exchange/TestExchangeSession.java b/src/test/davmail/exchange/TestExchangeSession.java index f6e21525..58ce387c 100644 --- a/src/test/davmail/exchange/TestExchangeSession.java +++ b/src/test/davmail/exchange/TestExchangeSession.java @@ -16,11 +16,11 @@ public class TestExchangeSession { Settings.setConfigFilePath(argv[currentArg++]); Settings.load(); - ExchangeSession session = new ExchangeSession(); + ExchangeSession session; // test auth try { ExchangeSessionFactory.checkConfig(); - session.login(argv[currentArg++], argv[currentArg++]); + session = ExchangeSessionFactory.getInstance(argv[currentArg++], argv[currentArg++]); ExchangeSession.Folder folder = session.selectFolder(argv[currentArg++]); String messageName;