From 09de5233916362f9999b5470ef882e3cc5370746 Mon Sep 17 00:00:00 2001 From: mguessan Date: Fri, 23 Jan 2009 00:59:41 +0000 Subject: [PATCH] Progress on IMAP implementation : list folders, some fetch orders git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@301 3d1905a2-6b24-0410-a738-b14d5a86fcbd --- .../davmail/exchange/ExchangeSession.java | 152 +++++++++++------ src/java/davmail/imap/ImapConnection.java | 155 ++++++++++++++---- src/java/davmail/pop/PopConnection.java | 2 +- .../davmail/exchange/TestExchangeSession.java | 2 +- 4 files changed, 229 insertions(+), 82 deletions(-) diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java index d18dfc3a..74094b95 100644 --- a/src/java/davmail/exchange/ExchangeSession.java +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -81,7 +81,6 @@ public class ExchangeSession { */ private String mailPath; private String email; - private String currentFolderUrl; private WebdavResource wdr = null; private final ExchangeSessionFactory.PoolKey poolKey; @@ -367,8 +366,6 @@ public class ExchangeSession { wdr.setPath(mailPath); getWellKnownFolders(); // set current folder to Inbox - selectFolder("INBOX"); - wdr.setPath(URIUtil.getPath(inboxUrl)); } catch (AuthenticationException exc) { @@ -437,19 +434,6 @@ public class ExchangeSession { ); } - /** - * Create message in current folder - * - * @param messageName message name - * @param bcc blind carbon copy header - * @param messageBody mail body - * @param allowOverwrite allow existing message overwrite - * @throws java.io.IOException when unable to create message - */ - public void createMessage(String messageName, String bcc, String messageBody, boolean allowOverwrite) throws IOException { - createMessage(currentFolderUrl, messageName, bcc, messageBody, allowOverwrite); - } - /** * Create message in specified folder. * Will overwrite an existing message with same subject in the same folder @@ -550,13 +534,14 @@ public class ExchangeSession { } - public List getAllMessages() throws IOException { + public List getAllMessages(String folderName) throws IOException { + String folderUrl = getFolderPath(folderName); List messages = new ArrayList(); String searchRequest = "Select \"DAV:uid\", \"http://schemas.microsoft.com/mapi/proptag/x0e080003\"" + - " FROM Scope('SHALLOW TRAVERSAL OF \"" + currentFolderUrl + "\"')\n" + + " FROM Scope('SHALLOW TRAVERSAL OF \"" + folderUrl + "\"')\n" + " WHERE \"DAV:ishidden\" = False AND \"DAV:isfolder\" = False\n" + " ORDER BY \"urn:schemas:httpmail:date\" ASC"; - Enumeration folderEnum = DavGatewayHttpClientFacade.executeSearchMethod(wdr.retrieveSessionInstance(), currentFolderUrl, searchRequest); + Enumeration folderEnum = DavGatewayHttpClientFacade.executeSearchMethod(wdr.retrieveSessionInstance(), folderUrl, searchRequest); while (folderEnum.hasMoreElements()) { ResponseEntity entity = (ResponseEntity) folderEnum.nextElement(); @@ -567,6 +552,65 @@ public class ExchangeSession { return messages; } + public List getSubFolders(String folderName) throws IOException { + List folders = new ArrayList(); + String searchRequest = "Select \"DAV:nosubs\", \"DAV:hassubs\"," + + " \"DAV:hassubs\",\"urn:schemas:httpmail:unreadcount\"" + + " FROM Scope('SHALLOW TRAVERSAL OF \"" + getFolderPath(folderName) + "\"')\n" + + " WHERE \"DAV:ishidden\" = False AND \"DAV:isfolder\" = True \n"; + Enumeration folderEnum = DavGatewayHttpClientFacade.executeSearchMethod(wdr.retrieveSessionInstance(), mailPath, searchRequest); + + while (folderEnum.hasMoreElements()) { + ResponseEntity entity = (ResponseEntity) folderEnum.nextElement(); + folders.add(buildFolder(entity)); + } + return folders; + } + + protected Folder buildFolder(ResponseEntity entity) throws URIException { + String href = URIUtil.decode(entity.getHref()); + Folder folder = new Folder(); + Enumeration enumeration = entity.getProperties(); + while (enumeration.hasMoreElements()) { + Property property = (Property) enumeration.nextElement(); + if ("hassubs".equals(property.getLocalName())) { + folder.hasChildren = "1".equals(property.getPropertyAsString()); + } + if ("nosubs".equals(property.getLocalName())) { + folder.noInferiors = "1".equals(property.getPropertyAsString()); + } + if ("objectcount".equals(property.getLocalName())) { + folder.objectCount = Integer.parseInt(property.getPropertyAsString()); + } + if ("unreadcount".equals(property.getLocalName())) { + folder.unreadCount = Integer.parseInt(property.getPropertyAsString()); + } + + } + if (href.endsWith("/")) { + href = href.substring(0, href.length()-1); + } + + // replace well known folder names + if (href.startsWith(inboxUrl)) { + folder.folderUrl = href.replaceFirst(inboxUrl, "INBOX"); + } else if (href.startsWith(sentitemsUrl)) { + folder.folderUrl = href.replaceFirst(sentitemsUrl, "Sent"); + } else if (href.startsWith(draftsUrl)) { + folder.folderUrl = href.replaceFirst(draftsUrl, "Drafts"); + } else if (href.startsWith(deleteditemsUrl)) { + folder.folderUrl = href.replaceFirst(deleteditemsUrl, "Trash"); + } else { + int index = href.indexOf(mailPath.substring(0, mailPath.length()-1)); + if (index >= 0) { + folder.folderUrl = href.substring(index + mailPath.length()); + } else { + throw new URIException("Invalid folder url: " + folder.folderUrl); + } + } + return folder; + } + /** * Delete oldest messages in trash. * keepDelay is the number of days to keep messages in trash before delete @@ -676,6 +720,25 @@ public class ExchangeSession { } + public String getFolderPath(String folderName) { + String folderPath; + if (folderName.startsWith("INBOX")) { + folderPath = folderName.replaceFirst("INBOX", inboxUrl); + } else if (folderName.startsWith("Trash")) { + folderPath = folderName.replaceFirst("Trash", deleteditemsUrl); + } else if (folderName.startsWith("Drafts")) { + folderPath = folderName.replaceFirst("Drafts", draftsUrl); + } else if (folderName.startsWith("Sent")) { + folderPath = folderName.replaceFirst("Sent", sentitemsUrl); + // absolute folder path + } else if (folderName != null && folderName.startsWith("/")) { + folderPath = folderName; + } else { + folderPath = mailPath + folderName; + } + return folderPath; + } + /** * Select current folder. * Folder name can be logical names INBOX, DRAFTS or TRASH (translated to local names), @@ -685,51 +748,40 @@ public class ExchangeSession { * @return Folder object * @throws IOException when unable to change folder */ - public Folder selectFolder(String folderName) throws IOException { + public Folder getFolder(String folderName) throws IOException { Folder folder = new Folder(); - folder.folderUrl = null; - if ("INBOX".equals(folderName)) { - folder.folderUrl = inboxUrl; - } else if ("TRASH".equals(folderName)) { - folder.folderUrl = deleteditemsUrl; - } else if ("DRAFTS".equals(folderName)) { - folder.folderUrl = draftsUrl; - // absolute folder path - } else if (folderName != null && folderName.startsWith("/")) { - folder.folderUrl = folderName; - } else { - folder.folderUrl = mailPath + folderName; - } + folder.folderUrl = getFolderPath(folderName); Vector reqProps = new Vector(); + reqProps.add("DAV:hassubs"); + reqProps.add("DAV:nosubs"); + reqProps.add("DAV:objectcount"); reqProps.add("urn:schemas:httpmail:unreadcount"); - reqProps.add("DAV:childcount"); Enumeration folderEnum = wdr.propfindMethod(folder.folderUrl, 0, reqProps); if (folderEnum.hasMoreElements()) { ResponseEntity entity = (ResponseEntity) folderEnum.nextElement(); - Enumeration propertiesEnum = entity.getProperties(); - while (propertiesEnum.hasMoreElements()) { - Property prop = (Property) propertiesEnum.nextElement(); - if ("unreadcount".equals(prop.getLocalName())) { - folder.unreadCount = Integer.parseInt(prop.getPropertyAsString()); - } - if ("childcount".equals(prop.getLocalName())) { - folder.childCount = Integer.parseInt(prop.getPropertyAsString()); - } - } - - } else { - throw new IOException("Folder not found: " + folder.folderUrl); - } - currentFolderUrl = folder.folderUrl; + folder = buildFolder(entity); + } return folder; } public static class Folder { public String folderUrl; - public int childCount; + public int objectCount; public int unreadCount; + public boolean hasChildren; + public boolean noInferiors; + + public String getFlags() { + if (noInferiors) { + return "\\NoInferiors"; + } else if (hasChildren) { + return "\\HasChildren"; + } else { + return "\\HasNoChildren"; + } + } } public class Message { diff --git a/src/java/davmail/imap/ImapConnection.java b/src/java/davmail/imap/ImapConnection.java index da850bca..051741b3 100644 --- a/src/java/davmail/imap/ImapConnection.java +++ b/src/java/davmail/imap/ImapConnection.java @@ -1,6 +1,8 @@ package davmail.imap; import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.SocketException; import java.util.StringTokenizer; import java.util.List; import java.io.IOException; @@ -9,6 +11,8 @@ import davmail.AbstractConnection; import davmail.tray.DavGatewayTray; import davmail.exchange.ExchangeSession; import davmail.exchange.ExchangeSessionFactory; +import com.sun.mail.imap.protocol.BASE64MailboxEncoder; +import com.sun.mail.imap.protocol.BASE64MailboxDecoder; /** * Dav Gateway smtp connection implementation. @@ -18,6 +22,9 @@ public class ImapConnection extends AbstractConnection { protected static final int INITIAL = 0; protected static final int AUTHENTICATED = 1; + ExchangeSession.Folder currentFolder; + List messages; + // Initialize the streams and start the thread public ImapConnection(Socket clientSocket) { super("ImapConnection", clientSocket, null); @@ -27,7 +34,7 @@ public class ImapConnection extends AbstractConnection { String line; StringTokenizer tokens; try { - sendClient("* OK Davmail Imap Server ready"); + sendClient("* OK [CAPABILITY IMAP4REV1 AUTH=LOGIN] IMAP4rev1 DavMail server ready"); for (; ;) { line = readClient(); // unable to read line, connection closed ? @@ -35,7 +42,17 @@ public class ImapConnection extends AbstractConnection { break; } - tokens = new StringTokenizer(line); + tokens = new StringTokenizer(line) { + public String nextToken() { + StringBuilder nextToken = new StringBuilder(); + nextToken.append(super.nextToken()); + while (hasMoreTokens() && nextToken.length() > 0 && nextToken.charAt(0) == '"' + && nextToken.charAt(nextToken.length() - 1) != '"') { + nextToken.append(' ').append(super.nextToken()); + } + return nextToken.toString(); + } + }; if (tokens.hasMoreTokens()) { String commandId = tokens.nextToken(); if (tokens.hasMoreTokens()) { @@ -43,11 +60,10 @@ public class ImapConnection extends AbstractConnection { if ("LOGOUT".equalsIgnoreCase(command)) { sendClient("* BYE Closing connection"); - sendClient(commandId + " OK Completed"); break; } if ("capability".equalsIgnoreCase(command)) { - sendClient("* CAPABILITY IMAP4REV1"); + sendClient("* CAPABILITY IMAP4REV1 AUTH=LOGIN"); sendClient(commandId + " OK CAPABILITY completed"); } else if ("login".equalsIgnoreCase(command)) { parseCredentials(tokens); @@ -60,45 +76,103 @@ public class ImapConnection extends AbstractConnection { sendClient(commandId + " NO LOGIN failed"); state = INITIAL; } + } else if ("AUTHENTICATE".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens()) { + String authenticationMethod = tokens.nextToken(); + if ("LOGIN".equalsIgnoreCase(authenticationMethod)) { + sendClient("+ " + base64Encode("Username:")); + userName = base64Decode(readClient()); + sendClient("+ " + base64Encode("Password:")); + password = base64Decode(readClient()); + try { + session = ExchangeSessionFactory.getInstance(userName, password); + sendClient(commandId + " OK Authenticated"); + state = AUTHENTICATED; + } catch (Exception e) { + DavGatewayTray.error(e); + sendClient(commandId + " NO LOGIN failed"); + state = INITIAL; + } + } else { + sendClient(commandId + " NO unsupported authentication method"); + } + } else { + sendClient(commandId + " BAD authentication method required"); + } } else { if (state != AUTHENTICATED) { sendClient(commandId + " BAD command authentication required"); } else { if ("lsub".equalsIgnoreCase(command)) { - /* TODO : implement - 2 lsub "" "*" -* LSUB () "/" INBOX/sent-mail -* LSUB () "/" Trash -* LSUB () "/" INBOX/spam -* LSUB () "/" Envoy&AOk-s -* LSUB () "/" Drafts -2 OK LSUB completed - */ sendClient(commandId + " OK LSUB completed"); } else if ("list".equalsIgnoreCase(command)) { - /* TODO : implement - */ - sendClient(commandId + " OK LIST completed"); - } else if ("select".equalsIgnoreCase(command)) { if (tokens.hasMoreTokens()) { - String folderName = removeQuotes(tokens.nextToken()); - ExchangeSession.Folder folder = session.selectFolder(folderName); - sendClient("* " + folder.childCount + " EXISTS"); - sendClient("* " + folder.unreadCount + " RECENT"); - // TODO : implement, compute session message ids - //sendClient("* [UNSEEN 1] first unseen message in inbox"); - sendClient(commandId + " OK [READ-WRITE] SELECT completed"); + String folderContext = BASE64MailboxDecoder.decode(removeQuotes(tokens.nextToken())); + if (tokens.hasMoreTokens()) { + String folderQuery = folderContext + BASE64MailboxDecoder.decode(removeQuotes(tokens.nextToken())); + if (folderQuery.endsWith("%")) { + List folders = session.getSubFolders(folderQuery.substring(0, folderQuery.length() - 1)); + for (ExchangeSession.Folder folder : folders) { + sendClient("* LIST (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderUrl) + "\""); + } + sendClient(commandId + " OK LIST completed"); + } else { + ExchangeSession.Folder folder = session.getFolder(folderQuery); + if (folder != null) { + sendClient("* LIST (" + folder.getFlags() + ") \"/\" \"" + BASE64MailboxEncoder.encode(folder.folderUrl) + "\""); + sendClient(commandId + " OK LIST completed"); + } else { + sendClient(commandId + " NO Folder not found"); + } + } + } else { + sendClient(commandId + " BAD missing folder argument"); + } + } else { + sendClient(commandId + " BAD missing folder argument"); + } + } else if ("select".equalsIgnoreCase(command) || "examine".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens()) { + String folderName = BASE64MailboxDecoder.decode(removeQuotes(tokens.nextToken())); + currentFolder = session.getFolder(folderName); + messages = session.getAllMessages(currentFolder.folderUrl); + sendClient("* " + currentFolder.objectCount + " EXISTS"); + sendClient("* " + currentFolder.objectCount + " RECENT"); + sendClient("* OK [UIDVALIDITY " + System.currentTimeMillis() + "]"); + sendClient("* OK [UIDNEXT " + (currentFolder.objectCount + 1) + "]"); + sendClient("* FLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen)"); + sendClient("* OK [PERMANENTFLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen)]"); + sendClient("* [UNSEEN 1] first unseen message in inbox"); + sendClient(commandId + " OK [READ-WRITE] " + command + " completed"); } else { sendClient(commandId + " BAD command unrecognized"); } + } else if ("close".equalsIgnoreCase(command)) { + currentFolder = null; + messages = null; + sendClient(commandId + " OK CLOSE unrecognized"); + } else if ("create".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens()) { + String folderName = BASE64MailboxDecoder.decode(removeQuotes(tokens.nextToken())); + if (session.getFolder(folderName) != null) { + sendClient(commandId + " OK folder already exists"); + } else { + // TODO + sendClient(commandId + " NO unsupported"); + } + } else { + sendClient(commandId + " BAD missing create argument"); + } } else if ("uid".equalsIgnoreCase(command)) { if (tokens.hasMoreTokens() && "fetch".equalsIgnoreCase(tokens.nextToken())) { if (tokens.hasMoreTokens()) { String parameter = tokens.nextToken(); + if (currentFolder == null) { + sendClient(commandId + " NO no folder selected"); + } if ("1:*".equals(parameter)) { - List messages = session.getAllMessages(); - for (ExchangeSession.Message message : messages) { - sendClient("* FETCH (UID " + message.uid + " FLAGS ())"); + for (int i = 0; i < currentFolder.objectCount; i++) { + sendClient("* FETCH (UID " + (i + 1) + " FLAGS (\\Recent))"); } sendClient(commandId + " OK UID FETCH completed"); } else { @@ -110,6 +184,22 @@ public class ImapConnection extends AbstractConnection { } else { sendClient(commandId + " BAD command unrecognized"); } + } else if ("fetch".equalsIgnoreCase(command)) { + if (tokens.hasMoreTokens()) { + int messageIndex = Integer.parseInt(tokens.nextToken()); + ExchangeSession.Message message = messages.get(messageIndex - 1); + if (tokens.hasMoreTokens()) { + String parameters = tokens.nextToken(); + if ("(BODYSTRUCTURE)".equals(parameters)) { + sendClient("* "+messageIndex+" FETCH (BODYSTRUCTURE (\"TEXT\" \"PLAIN\" (\"CHARSET\" \"windows-1252\") NIL NIL \"QUOTED-PRINTABLE\" "+message.size+" 50 NIL NIL NIL NIL))"); + sendClient(commandId + " OK FETCH completed"); + } else { + sendClient("* "+messageIndex+" 1 FETCH (BODY[TEXT]<0> {" + message.size + "}"); + message.write(os); + sendClient(commandId + " OK FETCH completed"); + } + } + } } else { sendClient(commandId + " BAD command unrecognized"); } @@ -126,6 +216,13 @@ public class ImapConnection extends AbstractConnection { os.flush(); + } catch (SocketTimeoutException e) { + DavGatewayTray.debug("Closing connection on timeout"); + try { + sendClient("* BYE Closing connection"); + } catch (IOException e1) { + DavGatewayTray.debug("Exception closing connection on timeout"); + } } catch (IOException e) { DavGatewayTray.error("Exception handling client", e); } finally { @@ -136,6 +233,7 @@ public class ImapConnection extends AbstractConnection { /** * Decode IMAP credentials + * * @param tokens tokens * @throws java.io.IOException on error */ @@ -168,8 +266,5 @@ public class ImapConnection extends AbstractConnection { return result; } - public void sendMessage(StringBuffer buffer) { - // TODO implement - } } diff --git a/src/java/davmail/pop/PopConnection.java b/src/java/davmail/pop/PopConnection.java index a9ee3eaf..58862157 100644 --- a/src/java/davmail/pop/PopConnection.java +++ b/src/java/davmail/pop/PopConnection.java @@ -109,7 +109,7 @@ public class PopConnection extends AbstractConnection { password = line.substring("PASS".length() + 1); try { session = ExchangeSessionFactory.getInstance(userName, password); - messages = session.getAllMessages(); + messages = session.getAllMessages("INBOX"); sendOK("PASS"); state = AUTHENTICATED; } catch (SocketException e) { diff --git a/src/test/davmail/exchange/TestExchangeSession.java b/src/test/davmail/exchange/TestExchangeSession.java index 85d09528..b552e38a 100644 --- a/src/test/davmail/exchange/TestExchangeSession.java +++ b/src/test/davmail/exchange/TestExchangeSession.java @@ -26,7 +26,7 @@ public class TestExchangeSession { ExchangeSessionFactory.checkConfig(); session = ExchangeSessionFactory.getInstance(argv[currentArg++], argv[currentArg++]); - ExchangeSession.Folder folder = session.selectFolder(argv[currentArg++]); + ExchangeSession.Folder folder = session.getFolder(argv[currentArg++]); String messageName; messageName = URIUtil.decode(argv[currentArg]);