IMAP: Check folder ctag and reload messages as needed, ignore RECENT search PARAMETER, allow quoted and unquoted message id on search

git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@480 3d1905a2-6b24-0410-a738-b14d5a86fcbd
This commit is contained in:
mguessan 2009-03-27 10:31:52 +00:00
parent d3aa687124
commit 72a8ad7374
2 changed files with 159 additions and 107 deletions

View File

@ -69,6 +69,7 @@ public class ExchangeSession {
FOLDER_PROPERTIES.add("DAV:hassubs"); FOLDER_PROPERTIES.add("DAV:hassubs");
FOLDER_PROPERTIES.add("DAV:nosubs"); FOLDER_PROPERTIES.add("DAV:nosubs");
FOLDER_PROPERTIES.add("urn:schemas:httpmail:unreadcount"); FOLDER_PROPERTIES.add("urn:schemas:httpmail:unreadcount");
FOLDER_PROPERTIES.add("http://schemas.microsoft.com/repl/contenttag");
} }
protected static final Vector<String> CONTENT_TAG = new Vector<String>(); protected static final Vector<String> CONTENT_TAG = new Vector<String>();
@ -114,6 +115,7 @@ public class ExchangeSession {
* The session is not actually established until a call to login() * The session is not actually established until a call to login()
* *
* @param poolKey session pool key * @param poolKey session pool key
* @throws java.io.IOException on error
*/ */
ExchangeSession(ExchangeSessionFactory.PoolKey poolKey) throws IOException { ExchangeSession(ExchangeSessionFactory.PoolKey poolKey) throws IOException {
this.poolKey = poolKey; this.poolKey = poolKey;
@ -659,6 +661,9 @@ public class ExchangeSession {
if ("unreadcount".equals(property.getLocalName())) { if ("unreadcount".equals(property.getLocalName())) {
folder.unreadCount = Integer.parseInt(property.getPropertyAsString()); folder.unreadCount = Integer.parseInt(property.getPropertyAsString());
} }
if ("contenttag".equals(property.getLocalName())) {
folder.contenttag = property.getPropertyAsString();
}
} }
if (href.endsWith("/")) { if (href.endsWith("/")) {
href = href.substring(0, href.length() - 1); href = href.substring(0, href.length() - 1);
@ -843,6 +848,27 @@ public class ExchangeSession {
return folder; return folder;
} }
/**
* Check folder ctag and reload messages as needed
*
* @param currentFolder current folder
* @return current folder or new refreshed folder
* @throws IOException on error
*/
public Folder refreshFolder(Folder currentFolder) throws IOException {
Folder newFolder = getFolder(currentFolder.folderName);
if (currentFolder.contenttag == null || !currentFolder.contenttag.equals(newFolder.contenttag)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Contenttag changed on " + currentFolder.folderName + " "
+ currentFolder.contenttag + " => " + newFolder.contenttag + ", reloading messages");
}
newFolder.loadMessages();
return newFolder;
} else {
return currentFolder;
}
}
public void createFolder(String folderName) throws IOException { public void createFolder(String folderName) throws IOException {
String folderPath = getFolderPath(folderName); String folderPath = getFolderPath(folderName);
PropPatchMethod method = new PropPatchMethod(URIUtil.encodePath(folderPath)) { PropPatchMethod method = new PropPatchMethod(URIUtil.encodePath(folderPath)) {
@ -914,12 +940,14 @@ public class ExchangeSession {
LOGGER.debug("Deleted to :" + destination); LOGGER.debug("Deleted to :" + destination);
} }
public static class Folder { public class Folder {
public String folderUrl; public String folderUrl;
public int unreadCount; public int unreadCount;
public boolean hasChildren; public boolean hasChildren;
public boolean noInferiors; public boolean noInferiors;
public String folderName; public String folderName;
public String contenttag;
public ExchangeSession.MessageList messages;
public String getFlags() { public String getFlags() {
if (noInferiors) { if (noInferiors) {
@ -930,6 +958,26 @@ public class ExchangeSession {
return "\\HasNoChildren"; return "\\HasNoChildren";
} }
} }
public void loadMessages() throws IOException {
messages = getAllMessages(folderUrl);
}
public int size() {
return messages.size();
}
public long getUidNext() {
return messages.get(messages.size() - 1).getImapUid() + 1;
}
public long getImapUid(int index) {
return messages.get(index).getImapUid();
}
public Message get(int index) {
return messages.get(index);
}
} }
public class Message implements Comparable { public class Message implements Comparable {

View File

@ -28,7 +28,6 @@ import java.text.ParseException;
public class ImapConnection extends AbstractConnection { public class ImapConnection extends AbstractConnection {
ExchangeSession.Folder currentFolder; ExchangeSession.Folder currentFolder;
ExchangeSession.MessageList messages;
// Initialize the streams and start the thread // Initialize the streams and start the thread
public ImapConnection(Socket clientSocket) { public ImapConnection(Socket clientSocket) {
@ -136,14 +135,14 @@ public class ImapConnection extends AbstractConnection {
if (tokens.hasMoreTokens()) { if (tokens.hasMoreTokens()) {
String folderName = BASE64MailboxDecoder.decode(tokens.nextToken()); String folderName = BASE64MailboxDecoder.decode(tokens.nextToken());
currentFolder = session.getFolder(folderName); currentFolder = session.getFolder(folderName);
messages = session.getAllMessages(currentFolder.folderUrl); currentFolder.loadMessages();
sendClient("* " + messages.size() + " EXISTS"); sendClient("* " + currentFolder.size() + " EXISTS");
sendClient("* " + messages.size() + " RECENT"); sendClient("* " + currentFolder.size() + " RECENT");
sendClient("* OK [UIDVALIDITY 1]"); sendClient("* OK [UIDVALIDITY 1]");
if (messages.size() == 0) { if (currentFolder.size() == 0) {
sendClient("* OK [UIDNEXT " + 1 + "]"); sendClient("* OK [UIDNEXT " + 1 + "]");
} else { } else {
sendClient("* OK [UIDNEXT " + (messages.get(messages.size() - 1).getImapUid() + 1) + "]"); sendClient("* OK [UIDNEXT " + currentFolder.getUidNext() + "]");
} }
sendClient("* FLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk)"); sendClient("* FLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk)");
sendClient("* OK [PERMANENTFLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk \\*)]"); sendClient("* OK [PERMANENTFLAGS (\\Answered \\Deleted \\Draft \\Flagged \\Seen $Forwarded Junk \\*)]");
@ -190,6 +189,7 @@ public class ImapConnection extends AbstractConnection {
if (currentFolder == null) { if (currentFolder == null) {
sendClient(commandId + " NO no folder selected"); sendClient(commandId + " NO no folder selected");
} else { } else {
currentFolder = session.refreshFolder(currentFolder);
UIDRangeIterator uidRangeIterator = new UIDRangeIterator(tokens.nextToken()); UIDRangeIterator uidRangeIterator = new UIDRangeIterator(tokens.nextToken());
String parameters = null; String parameters = null;
if (tokens.hasMoreTokens()) { if (tokens.hasMoreTokens()) {
@ -248,6 +248,7 @@ public class ImapConnection extends AbstractConnection {
sendClient(commandId + " OK SEARCH completed"); sendClient(commandId + " OK SEARCH completed");
} else if ("store".equalsIgnoreCase(subcommand)) { } else if ("store".equalsIgnoreCase(subcommand)) {
currentFolder = session.refreshFolder(currentFolder);
UIDRangeIterator UIDRangeIterator = new UIDRangeIterator(tokens.nextToken()); UIDRangeIterator UIDRangeIterator = new UIDRangeIterator(tokens.nextToken());
String action = tokens.nextToken(); String action = tokens.nextToken();
String flags = tokens.nextToken(); String flags = tokens.nextToken();
@ -259,6 +260,7 @@ public class ImapConnection extends AbstractConnection {
sendClient(commandId + " OK STORE completed"); sendClient(commandId + " OK STORE completed");
} else if ("copy".equalsIgnoreCase(subcommand)) { } else if ("copy".equalsIgnoreCase(subcommand)) {
try { try {
currentFolder = session.refreshFolder(currentFolder);
UIDRangeIterator UIDRangeIterator = new UIDRangeIterator(tokens.nextToken()); UIDRangeIterator UIDRangeIterator = new UIDRangeIterator(tokens.nextToken());
String targetName = BASE64MailboxDecoder.decode(tokens.nextToken()); String targetName = BASE64MailboxDecoder.decode(tokens.nextToken());
while (UIDRangeIterator.hasNext()) { while (UIDRangeIterator.hasNext()) {
@ -360,9 +362,9 @@ public class ImapConnection extends AbstractConnection {
if (currentFolder != null) { if (currentFolder != null) {
DavGatewayTray.debug(command + " on " + currentFolder.folderName); DavGatewayTray.debug(command + " on " + currentFolder.folderName);
currentFolder = session.getFolder(currentFolder.folderName); currentFolder = session.getFolder(currentFolder.folderName);
messages = session.getAllMessages(currentFolder.folderUrl); currentFolder.loadMessages();
sendClient("* " + messages.size() + " EXISTS"); sendClient("* " + currentFolder.size() + " EXISTS");
sendClient("* " + messages.size() + " RECENT"); sendClient("* " + currentFolder.size() + " RECENT");
} }
sendClient(commandId + " OK " + command + " completed"); sendClient(commandId + " OK " + command + " completed");
} else if ("subscribe".equalsIgnoreCase(command) || "unsubscribe".equalsIgnoreCase(command)) { } else if ("subscribe".equalsIgnoreCase(command) || "unsubscribe".equalsIgnoreCase(command)) {
@ -373,26 +375,26 @@ public class ImapConnection extends AbstractConnection {
String folderName = BASE64MailboxDecoder.decode(encodedFolderName); String folderName = BASE64MailboxDecoder.decode(encodedFolderName);
ExchangeSession.Folder folder = session.getFolder(folderName); ExchangeSession.Folder folder = session.getFolder(folderName);
// must retrieve messages // must retrieve messages
ExchangeSession.MessageList localMessages = session.getAllMessages(folder.folderUrl); folder.loadMessages();
String parameters = tokens.nextToken(); String parameters = tokens.nextToken();
StringBuilder answer = new StringBuilder(); StringBuilder answer = new StringBuilder();
StringTokenizer parametersTokens = new StringTokenizer(parameters); StringTokenizer parametersTokens = new StringTokenizer(parameters);
while (parametersTokens.hasMoreTokens()) { while (parametersTokens.hasMoreTokens()) {
String token = parametersTokens.nextToken(); String token = parametersTokens.nextToken();
if ("MESSAGES".equalsIgnoreCase(token)) { if ("MESSAGES".equalsIgnoreCase(token)) {
answer.append("MESSAGES ").append(localMessages.size()).append(" "); answer.append("MESSAGES ").append(folder.size()).append(" ");
} }
if ("RECENT".equalsIgnoreCase(token)) { if ("RECENT".equalsIgnoreCase(token)) {
answer.append("RECENT ").append(localMessages.size()).append(" "); answer.append("RECENT ").append(folder.size()).append(" ");
} }
if ("UIDNEXT".equalsIgnoreCase(token)) { if ("UIDNEXT".equalsIgnoreCase(token)) {
if (localMessages.size() == 0) { if (folder.size() == 0) {
answer.append("UIDNEXT 1 "); answer.append("UIDNEXT 1 ");
} else { } else {
if (localMessages.size() == 0) { if (folder.size() == 0) {
answer.append("UIDNEXT 1 "); answer.append("UIDNEXT 1 ");
} else { } else {
answer.append("UIDNEXT ").append((localMessages.get(localMessages.size() - 1).getImapUid() + 1)).append(" "); answer.append("UIDNEXT ").append(folder.getUidNext()).append(" ");
} }
} }
@ -462,85 +464,87 @@ public class ImapConnection extends AbstractConnection {
private void handleFetch(ExchangeSession.Message message, int currentIndex, String parameters) throws IOException { private void handleFetch(ExchangeSession.Message message, int currentIndex, String parameters) throws IOException {
StringBuilder buffer = new StringBuilder(); StringBuilder buffer = new StringBuilder();
buffer.append("* ").append(currentIndex).append(" FETCH (UID ").append(message.getImapUid()); buffer.append("* ").append(currentIndex).append(" FETCH (UID ").append(message.getImapUid());
boolean bodystructure = false; if (parameters != null) {
StringTokenizer paramTokens = new StringTokenizer(parameters); boolean bodystructure = false;
while (paramTokens.hasMoreTokens()) { StringTokenizer paramTokens = new StringTokenizer(parameters);
String param = paramTokens.nextToken(); while (paramTokens.hasMoreTokens()) {
if ("FLAGS".equals(param)) { String param = paramTokens.nextToken();
buffer.append(" FLAGS (").append(message.getImapFlags()).append(")"); if ("FLAGS".equals(param)) {
} else if ("BODYSTRUCTURE".equals(param)) { buffer.append(" FLAGS (").append(message.getImapFlags()).append(")");
if (parameters.indexOf("BODY.") >= 0) { } else if ("BODYSTRUCTURE".equals(param)) {
// Apple Mail: send structure with body, need exact RFC822.SIZE if (parameters.indexOf("BODY.") >= 0) {
bodystructure = true; // Apple Mail: send structure with body, need exact RFC822.SIZE
} else { bodystructure = true;
// thunderbird : send BODYSTRUCTURE } else {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // thunderbird : send BODYSTRUCTURE
message.write(baos); ByteArrayOutputStream baos = new ByteArrayOutputStream();
appendBodyStructure(buffer, baos); message.write(baos);
} appendBodyStructure(buffer, baos);
} else if ("INTERNALDATE".equals(param) && message.date != null && message.date.length() > 0) { }
try { } else if ("INTERNALDATE".equals(param) && message.date != null && message.date.length() > 0) {
SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); try {
dateParser.setTimeZone(ExchangeSession.GMT_TIMEZONE); SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
Date date = dateParser.parse(message.date); dateParser.setTimeZone(ExchangeSession.GMT_TIMEZONE);
SimpleDateFormat dateFormatter = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.ENGLISH); Date date = dateParser.parse(message.date);
buffer.append(" INTERNALDATE \"").append(dateFormatter.format(date)).append("\""); SimpleDateFormat dateFormatter = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.ENGLISH);
} catch (ParseException e) { buffer.append(" INTERNALDATE \"").append(dateFormatter.format(date)).append("\"");
throw new IOException("Invalid date: " + message.date); } catch (ParseException e) {
} throw new IOException("Invalid date: " + message.date);
} else if ("BODY.PEEK[HEADER]".equals(param) || param.startsWith("BODY.PEEK[HEADER")) { }
ByteArrayOutputStream baos = new ByteArrayOutputStream(); } else if ("BODY.PEEK[HEADER]".equals(param) || param.startsWith("BODY.PEEK[HEADER")) {
PartOutputStream partOutputStream = new PartOutputStream(baos, true, false, 0); ByteArrayOutputStream baos = new ByteArrayOutputStream();
message.write(partOutputStream); PartOutputStream partOutputStream = new PartOutputStream(baos, true, false, 0);
baos.close(); message.write(partOutputStream);
buffer.append(" RFC822.SIZE ").append(partOutputStream.size); baos.close();
if ("BODY.PEEK[HEADER]".equals(param)) { buffer.append(" RFC822.SIZE ").append(partOutputStream.size);
buffer.append(" BODY[HEADER] {"); if ("BODY.PEEK[HEADER]".equals(param)) {
} else { buffer.append(" BODY[HEADER] {");
buffer.append(" BODY[HEADER.FIELDS ()] {"); } else {
} buffer.append(" BODY[HEADER.FIELDS ()] {");
buffer.append(baos.size()).append("}"); }
sendClient(buffer.toString()); buffer.append(baos.size()).append("}");
os.write(baos.toByteArray()); sendClient(buffer.toString());
os.flush(); os.write(baos.toByteArray());
buffer.setLength(0); os.flush();
} else if (param.startsWith("BODY[]") || param.startsWith("BODY.PEEK[]") || "BODY.PEEK[TEXT]".equals(param)) { buffer.setLength(0);
// parse buffer size } else if (param.startsWith("BODY[]") || param.startsWith("BODY.PEEK[]") || "BODY.PEEK[TEXT]".equals(param)) {
int startIndex = 0; // parse buffer size
int ltIndex = param.indexOf('<'); int startIndex = 0;
if (ltIndex >= 0) { int ltIndex = param.indexOf('<');
int dotIndex = param.indexOf('.', ltIndex); if (ltIndex >= 0) {
if (dotIndex >= 0) { int dotIndex = param.indexOf('.', ltIndex);
startIndex = Integer.parseInt(param.substring(ltIndex + 1, dotIndex)); if (dotIndex >= 0) {
// ignore buffer size startIndex = Integer.parseInt(param.substring(ltIndex + 1, dotIndex));
// ignore buffer size
}
} }
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
boolean writeHeaders = true; boolean writeHeaders = true;
int rfc822size; int rfc822size;
if ("BODY.PEEK[TEXT]".equals(param)) { if ("BODY.PEEK[TEXT]".equals(param)) {
writeHeaders = false; writeHeaders = false;
}
PartOutputStream bodyOutputStream = new PartOutputStream(baos, writeHeaders, true, startIndex);
message.write(bodyOutputStream);
rfc822size = bodyOutputStream.size;
baos.close();
DavGatewayTray.debug("Message RFC822 size: " + rfc822size + " buffer size:" + baos.size());
if (bodystructure) {
// Apple Mail: need to build full bodystructure
appendBodyStructure(buffer, baos);
}
buffer.append(" RFC822.SIZE ").append(rfc822size).append(" ").append("BODY[]");
// partial
if (startIndex > 0) {
buffer.append('<').append(startIndex).append('>');
}
buffer.append(" {").append(baos.size()).append("}");
sendClient(buffer.toString());
os.write(baos.toByteArray());
os.flush();
buffer.setLength(0);
} }
PartOutputStream bodyOutputStream = new PartOutputStream(baos, writeHeaders, true, startIndex);
message.write(bodyOutputStream);
rfc822size = bodyOutputStream.size;
baos.close();
DavGatewayTray.debug("Message RFC822 size: " + rfc822size + " buffer size:" + baos.size());
if (bodystructure) {
// Apple Mail: need to build full bodystructure
appendBodyStructure(buffer, baos);
}
buffer.append(" RFC822.SIZE ").append(rfc822size).append(" ").append("BODY[]");
// partial
if (startIndex > 0) {
buffer.append('<').append(startIndex).append('>');
}
buffer.append(" {").append(baos.size()).append("}");
sendClient(buffer.toString());
os.write(baos.toByteArray());
os.flush();
buffer.setLength(0);
} }
} }
buffer.append(")"); buffer.append(")");
@ -698,7 +702,7 @@ public class ImapConnection extends AbstractConnection {
} else if ("HEADER".equals(token)) { } else if ("HEADER".equals(token)) {
String headerName = tokens.nextToken().toLowerCase(); String headerName = tokens.nextToken().toLowerCase();
String value = tokens.nextToken(); String value = tokens.nextToken();
if ("message-id".equals(headerName)) { if ("message-id".equals(headerName) && !value.startsWith("<")) {
value = "<" + value + ">"; value = "<" + value + ">";
} }
conditions.append(operator).append("\"urn:schemas:mailheader:").append(headerName).append("\"='").append(value).append("'"); conditions.append(operator).append("\"urn:schemas:mailheader:").append(headerName).append("\"='").append(value).append("'");
@ -721,10 +725,10 @@ public class ImapConnection extends AbstractConnection {
} catch (ParseException e) { } catch (ParseException e) {
throw new IOException("Invalid search parameters"); throw new IOException("Invalid search parameters");
} }
} else if ("OLD".equals(token)) { } else if ("OLD".equals(token) || "RECENT".equals(token)) {
// ignore // ignore
} else { } else {
throw new IOException("Invalid search parameters"); throw new IOException("Invalid search parameter: " + token);
} }
} }
@ -763,9 +767,9 @@ public class ImapConnection extends AbstractConnection {
} }
protected void expunge(boolean silent) throws IOException { protected void expunge(boolean silent) throws IOException {
if (messages != null) { if (currentFolder.messages != null) {
int index = 0; int index = 0;
for (ExchangeSession.Message message : messages) { for (ExchangeSession.Message message : currentFolder.messages) {
index++; index++;
if (message.deleted) { if (message.deleted) {
message.delete(); message.delete();
@ -976,23 +980,23 @@ public class ImapConnection extends AbstractConnection {
} else { } else {
startUid = endUid = convertToLong(currentRange); startUid = endUid = convertToLong(currentRange);
} }
while (currentIndex < messages.size() && messages.get(currentIndex).getImapUid() < startUid) { while (currentIndex < currentFolder.size() && currentFolder.getImapUid(currentIndex) < startUid) {
currentIndex++; currentIndex++;
} }
} else { } else {
currentIndex = messages.size(); currentIndex = currentFolder.size();
} }
} }
public boolean hasNext() { public boolean hasNext() {
while (currentIndex < messages.size() && messages.get(currentIndex).getImapUid() > endUid) { while (currentIndex < currentFolder.size() && currentFolder.getImapUid(currentIndex) > endUid) {
skipToStartUid(); skipToStartUid();
} }
return currentIndex < messages.size(); return currentIndex < currentFolder.size();
} }
public ExchangeSession.Message next() { public ExchangeSession.Message next() {
ExchangeSession.Message message = messages.get(currentIndex++); ExchangeSession.Message message = currentFolder.get(currentIndex++);
long uid = message.getImapUid(); long uid = message.getImapUid();
if (uid < startUid || uid > endUid) { if (uid < startUid || uid > endUid) {
throw new RuntimeException("Message uid " + uid + " not in range " + startUid + ":" + endUid); throw new RuntimeException("Message uid " + uid + " not in range " + startUid + ":" + endUid);
@ -1034,23 +1038,23 @@ public class ImapConnection extends AbstractConnection {
} else { } else {
startUid = endUid = convertToLong(currentRange); startUid = endUid = convertToLong(currentRange);
} }
while (currentIndex < messages.size() && (currentIndex + 1) < startUid) { while (currentIndex < currentFolder.size() && (currentIndex + 1) < startUid) {
currentIndex++; currentIndex++;
} }
} else { } else {
currentIndex = messages.size(); currentIndex = currentFolder.size();
} }
} }
public boolean hasNext() { public boolean hasNext() {
while (currentIndex < messages.size() && (currentIndex + 1) > endUid) { while (currentIndex < currentFolder.size() && (currentIndex + 1) > endUid) {
skipToStartUid(); skipToStartUid();
} }
return currentIndex < messages.size(); return currentIndex < currentFolder.size();
} }
public ExchangeSession.Message next() { public ExchangeSession.Message next() {
return messages.get(currentIndex++); return currentFolder.get(currentIndex++);
} }
public void remove() { public void remove() {