diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 1b2b47a6b..023d19f60 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -117,6 +117,7 @@ public class MessagingController implements Runnable { private static final String PENDING_COMMAND_MOVE_OR_COPY = "com.fsck.k9.MessagingController.moveOrCopy"; private static final String PENDING_COMMAND_MOVE_OR_COPY_BULK = "com.fsck.k9.MessagingController.moveOrCopyBulk"; + private static final String PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW = "com.fsck.k9.MessagingController.moveOrCopyBulkNew"; private static final String PENDING_COMMAND_EMPTY_TRASH = "com.fsck.k9.MessagingController.emptyTrash"; private static final String PENDING_COMMAND_SET_FLAG_BULK = "com.fsck.k9.MessagingController.setFlagBulk"; private static final String PENDING_COMMAND_SET_FLAG = "com.fsck.k9.MessagingController.setFlag"; @@ -1900,6 +1901,8 @@ public class MessagingController implements Runnable { } else if (PENDING_COMMAND_MARK_ALL_AS_READ.equals(command.command)) { processPendingMarkAllAsRead(command, account); } else if (PENDING_COMMAND_MOVE_OR_COPY_BULK.equals(command.command)) { + processPendingMoveOrCopyOld2(command, account); + } else if (PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW.equals(command.command)) { processPendingMoveOrCopy(command, account); } else if (PENDING_COMMAND_MOVE_OR_COPY.equals(command.command)) { processPendingMoveOrCopyOld(command, account); @@ -2079,16 +2082,72 @@ public class MessagingController implements Runnable { return; } PendingCommand command = new PendingCommand(); - command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK; + command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW; int length = 3 + uids.length; command.arguments = new String[length]; command.arguments[0] = srcFolder; command.arguments[1] = destFolder; command.arguments[2] = Boolean.toString(isCopy); - System.arraycopy(uids, 0, command.arguments, 3, uids.length); + command.arguments[3] = Boolean.toString(false); + System.arraycopy(uids, 0, command.arguments, 4, uids.length); queuePendingCommand(account, command); } + + private void queueMoveOrCopy(Account account, String srcFolder, String destFolder, boolean isCopy, String uids[], Map uidMap) { + if (uidMap == null || uidMap.isEmpty()) { + queueMoveOrCopy(account, srcFolder, destFolder, isCopy, uids); + } else { + if (account.getErrorFolderName().equals(srcFolder)) { + return; + } + PendingCommand command = new PendingCommand(); + command.command = PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW; + + int length = 4 + uidMap.keySet().size() + uidMap.values().size(); + command.arguments = new String[length]; + command.arguments[0] = srcFolder; + command.arguments[1] = destFolder; + command.arguments[2] = Boolean.toString(isCopy); + command.arguments[3] = Boolean.toString(true); + System.arraycopy(uidMap.keySet().toArray(), 0, command.arguments, 4, uidMap.keySet().size()); + System.arraycopy(uidMap.values().toArray(), 0, command.arguments, 4 + uidMap.keySet().size(), uidMap.values().size()); + queuePendingCommand(account, command); + } + } + + /** + * Convert pending command to new format and call + * {@link #processPendingMoveOrCopy(PendingCommand, Account)}. + * + *

+ * TODO: This method is obsolete and is only for transition from K-9 4.0 to K-9 4.2 + * Eventually, it should be removed. + *

+ * + * @param command + * Pending move/copy command in old format. + * @param account + * The account the pending command belongs to. + * + * @throws MessagingException + * In case of an error. + */ + private void processPendingMoveOrCopyOld2(PendingCommand command, Account account) + throws MessagingException { + PendingCommand newCommand = new PendingCommand(); + int len = command.arguments.length; + newCommand.command = PENDING_COMMAND_MOVE_OR_COPY_BULK_NEW; + newCommand.arguments = new String[len + 1]; + newCommand.arguments[0] = command.arguments[0]; + newCommand.arguments[1] = command.arguments[1]; + newCommand.arguments[2] = command.arguments[2]; + newCommand.arguments[3] = Boolean.toString(false); + System.arraycopy(command.arguments, 3, newCommand.arguments, 4, len - 3); + + processPendingMoveOrCopy(newCommand, account); + } + /** * Process a pending trash message command. * @@ -2100,6 +2159,7 @@ public class MessagingController implements Runnable { throws MessagingException { Folder remoteSrcFolder = null; Folder remoteDestFolder = null; + LocalFolder localDestFolder = null; try { String srcFolder = command.arguments[0]; if (account.getErrorFolderName().equals(srcFolder)) { @@ -2107,14 +2167,42 @@ public class MessagingController implements Runnable { } String destFolder = command.arguments[1]; String isCopyS = command.arguments[2]; + String hasNewUidsS = command.arguments[3]; + + boolean hasNewUids = false; + if (hasNewUidsS != null) { + hasNewUids = Boolean.parseBoolean(hasNewUidsS); + } + Store remoteStore = account.getRemoteStore(); remoteSrcFolder = remoteStore.getFolder(srcFolder); + Store localStore = account.getLocalStore(); + localDestFolder = (LocalFolder) localStore.getFolder(destFolder); List messages = new ArrayList(); - for (int i = 3; i < command.arguments.length; i++) { - String uid = command.arguments[i]; - if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { - messages.add(remoteSrcFolder.getMessage(uid)); + + /* + * We split up the localUidMap into two parts while sending the command, here we assemble it back. + */ + Map localUidMap = new HashMap(); + if (hasNewUids) { + int offset = (command.arguments.length - 4) / 2; + + for (int i = 4; i < 4 + offset; i++) { + localUidMap.put(command.arguments[i], command.arguments[i + offset]); + + String uid = command.arguments[i]; + if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { + messages.add(remoteSrcFolder.getMessage(uid)); + } + } + + } else { + for (int i = 4; i < command.arguments.length; i++) { + String uid = command.arguments[i]; + if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { + messages.add(remoteSrcFolder.getMessage(uid)); + } } } @@ -2135,6 +2223,8 @@ public class MessagingController implements Runnable { Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy: source folder = " + srcFolder + ", " + messages.size() + " messages, destination folder = " + destFolder + ", isCopy = " + isCopy); + Map remoteUidMap = null; + if (!isCopy && destFolder.equals(account.getTrashFolderName())) { if (K9.DEBUG) Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy doing special case for deleting message"); @@ -2148,9 +2238,9 @@ public class MessagingController implements Runnable { remoteDestFolder = remoteStore.getFolder(destFolder); if (isCopy) { - remoteSrcFolder.copyMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); + remoteUidMap = remoteSrcFolder.copyMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); } else { - remoteSrcFolder.moveMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); + remoteUidMap = remoteSrcFolder.moveMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); } } if (!isCopy && Account.EXPUNGE_IMMEDIATELY.equals(account.getExpungePolicy())) { @@ -2159,12 +2249,32 @@ public class MessagingController implements Runnable { remoteSrcFolder.expunge(); } + + /* + * This next part is used to bring the local UIDs of the local destination folder + * upto speed with the remote UIDs of remote destionation folder. + */ + if (!localUidMap.isEmpty() && remoteUidMap != null && !remoteUidMap.isEmpty()) { + Set remoteSrcUids = remoteUidMap.keySet(); + Iterator remoteSrcUidsIterator = remoteSrcUids.iterator(); + + while (remoteSrcUidsIterator.hasNext()) { + String remoteSrcUid = remoteSrcUidsIterator.next(); + String localDestUid = localUidMap.get(remoteSrcUid); + String newUid = remoteUidMap.get(remoteSrcUid); + + Message localDestMessage = localDestFolder.getMessage(localDestUid); + localDestMessage.setUid(newUid); + localDestFolder.changeUid((LocalMessage)localDestMessage); + for (MessagingListener l : getListeners()) { + l.messageUidChanged(account, destFolder, localDestUid, newUid); + } + } + } } finally { closeFolder(remoteSrcFolder); closeFolder(remoteDestFolder); } - - } private void queueSetFlag(final Account account, final String folderName, final String newState, final String flag, final String[] uids) { @@ -3281,6 +3391,7 @@ public class MessagingController implements Runnable { private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, final Message[] inMessages, final String destFolder, final boolean isCopy, MessagingListener listener) { try { + Map uidMap = new HashMap(); Store localStore = account.getLocalStore(); Store remoteStore = account.getRemoteStore(); if (!isCopy && (!remoteStore.isMoveCapable() || !localStore.isMoveCapable())) { @@ -3323,7 +3434,7 @@ public class MessagingController implements Runnable { fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.BODY); localSrcFolder.fetch(messages, fp, null); - localSrcFolder.copyMessages(messages, localDestFolder); + uidMap = localSrcFolder.copyMessages(messages, localDestFolder); if (unreadCountAffected) { // If this copy operation changes the unread count in the destination @@ -3334,7 +3445,7 @@ public class MessagingController implements Runnable { } } } else { - localSrcFolder.moveMessages(messages, localDestFolder); + uidMap = localSrcFolder.moveMessages(messages, localDestFolder); for (Map.Entry entry : origUidMap.entrySet()) { String origUid = entry.getKey(); Message message = entry.getValue(); @@ -3356,7 +3467,7 @@ public class MessagingController implements Runnable { } } - queueMoveOrCopy(account, srcFolder, destFolder, isCopy, origUidMap.keySet().toArray(EMPTY_STRING_ARRAY)); + queueMoveOrCopy(account, srcFolder, destFolder, isCopy, origUidMap.keySet().toArray(EMPTY_STRING_ARRAY), uidMap); } processPendingCommands(account); @@ -3436,6 +3547,7 @@ public class MessagingController implements Runnable { } Store localStore = account.getLocalStore(); localFolder = localStore.getFolder(folder); + Map uidMap = null; if (folder.equals(account.getTrashFolderName()) || K9.FOLDER_NONE.equals(account.getTrashFolderName())) { if (K9.DEBUG) Log.d(K9.LOG_TAG, "Deleting messages in trash folder or trash set to -None-, not copying"); @@ -3450,7 +3562,7 @@ public class MessagingController implements Runnable { if (K9.DEBUG) Log.d(K9.LOG_TAG, "Deleting messages in normal folder, moving"); - localFolder.moveMessages(messages, localTrashFolder); + uidMap = localFolder.moveMessages(messages, localTrashFolder); } } @@ -3483,7 +3595,7 @@ public class MessagingController implements Runnable { if (folder.equals(account.getTrashFolderName())) { queueSetFlag(account, folder, Boolean.toString(true), Flag.DELETED.toString(), uids); } else { - queueMoveOrCopy(account, folder, account.getTrashFolderName(), false, uids); + queueMoveOrCopy(account, folder, account.getTrashFolderName(), false, uids, uidMap); } processPendingCommands(account); } else if (account.getDeletePolicy() == Account.DELETE_POLICY_MARK_AS_READ) { diff --git a/src/com/fsck/k9/mail/Folder.java b/src/com/fsck/k9/mail/Folder.java index 0f5b96060..e3dafc3e5 100644 --- a/src/com/fsck/k9/mail/Folder.java +++ b/src/com/fsck/k9/mail/Folder.java @@ -1,6 +1,7 @@ package com.fsck.k9.mail; import java.util.Date; +import java.util.Map; import android.util.Log; import com.fsck.k9.Account; @@ -102,11 +103,15 @@ public abstract class Folder { public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException; - public abstract void appendMessages(Message[] messages) throws MessagingException; + public abstract Map appendMessages(Message[] messages) throws MessagingException; - public void copyMessages(Message[] msgs, Folder folder) throws MessagingException {} + public Map copyMessages(Message[] msgs, Folder folder) throws MessagingException { + return null; + } - public void moveMessages(Message[] msgs, Folder folder) throws MessagingException {} + public Map moveMessages(Message[] msgs, Folder folder) throws MessagingException { + return null; + } public void delete(Message[] msgs, String trashFolderName) throws MessagingException { for (Message message : msgs) { diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index cc3681706..6a77db634 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -61,6 +61,7 @@ import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.controller.MessageRetrievalListener; +import com.fsck.k9.helper.StringUtils; import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; @@ -89,6 +90,7 @@ import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.store.ImapResponseParser.ImapList; import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; +import com.fsck.k9.mail.store.imap.ImapUtility; import com.fsck.k9.mail.transport.imap.ImapSettings; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; @@ -1061,53 +1063,127 @@ public class ImapStore extends Store { } } + /** + * Copies the given messages to the specified folder. + * + *

+ * Note: + * Only the UIDs of the given {@link Message} instances are used. It is assumed that all + * UIDs represent valid messages in this folder. + *

+ * + * @param messages + * The messages to copy to the specfied folder. + * @param folder + * The name of the target folder. + * + * @return The mapping of original message UIDs to the new server UIDs. + */ @Override - public void copyMessages(Message[] messages, Folder folder) throws MessagingException { + public Map copyMessages(Message[] messages, Folder folder) + throws MessagingException { if (!(folder instanceof ImapFolder)) { throw new MessagingException("ImapFolder.copyMessages passed non-ImapFolder"); } - if (messages.length == 0) - return; + if (messages.length == 0) { + return null; + } ImapFolder iFolder = (ImapFolder)folder; checkOpen(); + String[] uids = new String[messages.length]; for (int i = 0, count = messages.length; i < count; i++) { uids[i] = messages[i].getUid(); } + try { String remoteDestName = encodeString(encodeFolderName(iFolder.getPrefixedName())); if (!exists(remoteDestName)) { - /* - * If the remote trash folder doesn't exist we try to create it. - */ - if (K9.DEBUG) - Log.i(K9.LOG_TAG, "IMAPMessage.copyMessages: attempting to create remote '" + remoteDestName + "' folder for " + getLogId()); + // If the remote trash folder doesn't exist we try to create it. + if (K9.DEBUG) { + Log.i(K9.LOG_TAG, "Attempting to create remote folder '" + remoteDestName + + "' for " + getLogId()); + } iFolder.create(FolderType.HOLDS_MESSAGES); } - if (exists(remoteDestName)) { - executeSimpleCommand(String.format("UID COPY %s %s", - Utility.combine(uids, ','), - remoteDestName)); - } else { - throw new MessagingException("IMAPMessage.copyMessages: remote destination folder " + folder.getName() - + " does not exist and could not be created for " + getLogId() - , true); + //TODO: Split this into multiple commands if the command exceeds a certain length. + mConnection.sendCommand(String.format("UID COPY %s %s", + Utility.combine(uids, ','), + remoteDestName), false); + ImapResponse response; + do { + response = mConnection.readResponse(); + handleUntaggedResponse(response); + } while (response.mTag == null); + + Map uidMap = null; + if (response.size() > 1) { + /* + * If the server supports UIDPLUS, then along with the COPY response it will + * return an COPYUID response code, e.g. + * + * 24 OK [COPYUID 38505 304,319:320 3956:3958] Success + * + * COPYUID is followed by UIDVALIDITY, the set of UIDs of copied messages from + * the source folder and the set of corresponding UIDs assigned to them in the + * destination folder. + * + * We can use the new UIDs included in this response to update our records. + */ + Object responseList = response.get(1); + + if (responseList instanceof ImapList) { + final ImapList copyList = (ImapList) responseList; + if (copyList.size() >= 4 && copyList.getString(0).equals("COPYUID")) { + List srcUids = ImapUtility.getImapSequenceValues( + copyList.getString(2)); + List destUids = ImapUtility.getImapSequenceValues( + copyList.getString(3)); + + if (srcUids != null && destUids != null) { + if (srcUids.size() == destUids.size()) { + Iterator srcUidsIterator = srcUids.iterator(); + Iterator destUidsIterator = destUids.iterator(); + uidMap = new HashMap(); + while (srcUidsIterator.hasNext() && + destUidsIterator.hasNext()) { + String srcUid = srcUidsIterator.next(); + String destUid = destUidsIterator.next(); + uidMap.put(srcUid, destUid); + } + } else { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Parse error: size of source UIDs " + + "list is not the same as size of destination " + + "UIDs list."); + } + } + } else { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Parsing of the sequence set failed."); + } + } + } + } } + + return uidMap; } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } } @Override - public void moveMessages(Message[] messages, Folder folder) throws MessagingException { + public Map moveMessages(Message[] messages, Folder folder) throws MessagingException { if (messages.length == 0) - return; - copyMessages(messages, folder); + return null; + Map uidMap = copyMessages(messages, folder); setFlags(messages, new Flag[] { Flag.DELETED }, true); + return uidMap; } @Override @@ -1851,20 +1927,30 @@ public class ImapStore extends Store { } /** - * Appends the given messages to the selected folder. This implementation also determines - * the new UID of the given message on the IMAP server and sets the Message's UID to the - * new server UID. + * Appends the given messages to the selected folder. + * + *

+ * This implementation also determines the new UIDs of the given messages on the IMAP + * server and changes the messages' UIDs to the new server UIDs. + *

+ * + * @param messages + * The messages to append to the folder. + * + * @return The mapping of original message UIDs to the new server UIDs. */ @Override - public void appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(Message[] messages) throws MessagingException { checkOpen(); try { + Map uidMap = new HashMap(); for (Message message : messages) { mConnection.sendCommand( String.format("APPEND %s (%s) {%d}", encodeString(encodeFolderName(getPrefixedName())), combineFlags(message.getFlags()), message.calculateSize()), false); + ImapResponse response; do { response = mConnection.readResponse(); @@ -1878,16 +1964,54 @@ public class ImapStore extends Store { } } while (response.mTag == null); - String newUid = getUidFromMessageId(message); - if (K9.DEBUG) - Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); + if (response.size() > 1) { + /* + * If the server supports UIDPLUS, then along with the APPEND response it + * will return an APPENDUID response code, e.g. + * + * 11 OK [APPENDUID 2 238268] APPEND completed + * + * We can use the UID included in this response to update our records. + */ + Object responseList = response.get(1); - if (newUid != null) { - message.setUid(newUid); + if (responseList instanceof ImapList) { + ImapList appendList = (ImapList) responseList; + if (appendList.size() >= 3 && + appendList.getString(0).equals("APPENDUID")) { + + String newUid = appendList.getString(2); + + if (!StringUtils.isNullOrEmpty(newUid)) { + message.setUid(newUid); + uidMap.put(message.getUid(), newUid); + continue; + } + } + } } + /* + * This part is executed in case the server does not support UIDPLUS or does + * not implement the APPENDUID response code. + */ + String newUid = getUidFromMessageId(message); + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); + } + if (!StringUtils.isNullOrEmpty(newUid)) { + uidMap.put(message.getUid(), newUid); + message.setUid(newUid); + } } + + /* + * We need uidMap to be null if new UIDs are not available to maintain consistency + * with the behavior of other similar methods (copyMessages, moveMessages) which + * return null. + */ + return (uidMap.size() == 0) ? null : uidMap; } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 7a5c13472..f9d769511 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -1928,21 +1928,23 @@ public class LocalStore extends Store implements Serializable { } @Override - public void copyMessages(Message[] msgs, Folder folder) throws MessagingException { + public Map copyMessages(Message[] msgs, Folder folder) throws MessagingException { if (!(folder instanceof LocalFolder)) { throw new MessagingException("copyMessages called with incorrect Folder"); } - ((LocalFolder) folder).appendMessages(msgs, true); + return ((LocalFolder) folder).appendMessages(msgs, true); } @Override - public void moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException { + public Map moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException { if (!(destFolder instanceof LocalFolder)) { throw new MessagingException("moveMessages called with non-LocalFolder"); } final LocalFolder lDestFolder = (LocalFolder)destFolder; + final Map uidMap = new HashMap(); + try { database.execute(false, new DbCallback() { @Override @@ -1968,7 +1970,10 @@ public class LocalStore extends Store implements Serializable { Log.d(K9.LOG_TAG, "Updating folder_id to " + lDestFolder.getId() + " for message with UID " + message.getUid() + ", id " + lMessage.getId() + " currently in folder " + getName()); - message.setUid(K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString()); + String newUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); + message.setUid(newUid); + + uidMap.put(oldUID, newUid); db.execSQL("UPDATE messages " + "SET folder_id = ?, uid = ? " + "WHERE id = ?", new Object[] { lDestFolder.getId(), @@ -1976,6 +1981,11 @@ public class LocalStore extends Store implements Serializable { lMessage.getId() }); + /* + * Add a placeholder message so we won't download the original + * message again if we synchronize before the remote move is + * complete. + */ LocalMessage placeHolder = new LocalMessage(oldUID, LocalFolder.this); placeHolder.setFlagInternal(Flag.DELETED, true); placeHolder.setFlagInternal(Flag.SEEN, true); @@ -1987,6 +1997,7 @@ public class LocalStore extends Store implements Serializable { return null; } }); + return uidMap; } catch (WrappedException e) { throw(MessagingException) e.getCause(); } @@ -2032,8 +2043,8 @@ public class LocalStore extends Store implements Serializable { * message, retrieve the appropriate local message instance first (if it already exists). */ @Override - public void appendMessages(Message[] messages) throws MessagingException { - appendMessages(messages, false); + public Map appendMessages(Message[] messages) throws MessagingException { + return appendMessages(messages, false); } public void destroyMessages(final Message[] messages) throws MessagingException { @@ -2069,10 +2080,12 @@ public class LocalStore extends Store implements Serializable { * message, retrieve the appropriate local message instance first (if it already exists). * @param messages * @param copy + * @return Map uidMap of srcUids -> destUids */ - private void appendMessages(final Message[] messages, final boolean copy) throws MessagingException { + private Map appendMessages(final Message[] messages, final boolean copy) throws MessagingException { open(OpenMode.READ_WRITE); try { + final Map uidMap = new HashMap(); database.execute(true, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { @@ -2085,11 +2098,26 @@ public class LocalStore extends Store implements Serializable { long oldMessageId = -1; String uid = message.getUid(); if (uid == null || copy) { - uid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); - if (!copy) { - message.setUid(uid); + /* + * Create a new message in the database + */ + String randomLocalUid = K9.LOCAL_UID_PREFIX + + UUID.randomUUID().toString(); + + if (copy) { + // Save mapping: source UID -> target UID + uidMap.put(uid, randomLocalUid); + } else { + // Modify the Message instance to reference the new UID + message.setUid(randomLocalUid); } + + // The message will be saved with the newly generated UID + uid = randomLocalUid; } else { + /* + * Replace an existing message in the database + */ LocalMessage oldMessage = (LocalMessage) getMessage(uid); if (oldMessage != null) { @@ -2169,6 +2197,7 @@ public class LocalStore extends Store implements Serializable { return null; } }); + return uidMap; } catch (WrappedException e) { throw(MessagingException) e.getCause(); } diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index 56d9b5fd8..5c6c3482c 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -24,6 +24,7 @@ import java.util.LinkedList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; public class Pop3Store extends Store { public static final String STORE_TYPE = "POP3"; @@ -889,7 +890,8 @@ public class Pop3Store extends Store { } @Override - public void appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(Message[] messages) throws MessagingException { + return null; } @Override diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java index 2d1f4818c..ab334f244 100644 --- a/src/com/fsck/k9/mail/store/WebDavStore.java +++ b/src/com/fsck/k9/mail/store/WebDavStore.java @@ -1334,13 +1334,15 @@ public class WebDavStore extends Store { } @Override - public void copyMessages(Message[] messages, Folder folder) throws MessagingException { + public Map copyMessages(Message[] messages, Folder folder) throws MessagingException { moveOrCopyMessages(messages, folder.getName(), false); + return null; } @Override - public void moveMessages(Message[] messages, Folder folder) throws MessagingException { + public Map moveMessages(Message[] messages, Folder folder) throws MessagingException { moveOrCopyMessages(messages, folder.getName(), true); + return null; } @Override @@ -1915,8 +1917,9 @@ public class WebDavStore extends Store { } @Override - public void appendMessages(Message[] messages) throws MessagingException { + public Map appendMessages(Message[] messages) throws MessagingException { appendWebDavMessages(messages); + return null; } public Message[] appendWebDavMessages(Message[] messages) throws MessagingException { diff --git a/src/com/fsck/k9/mail/store/imap/ImapUtility.java b/src/com/fsck/k9/mail/store/imap/ImapUtility.java new file mode 100644 index 000000000..d0ac2b31a --- /dev/null +++ b/src/com/fsck/k9/mail/store/imap/ImapUtility.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 The K-9 Dog Walkers + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fsck.k9.mail.store.imap; + +import android.util.Log; + +import com.fsck.k9.K9; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility methods for use with IMAP. + */ +public class ImapUtility { + /** + * Gets all of the values in a sequence set per RFC 3501. + * + *

+ * Any ranges are expanded into a list of individual numbers. + *

+ * + *
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ * + * @param set + * The sequence set string as received by the server. + * + * @return The list of IDs as strings in this sequence set. If the set is invalid, an empty + * list is returned. + */ + public static List getImapSequenceValues(String set) { + ArrayList list = new ArrayList(); + if (set != null) { + String[] setItems = set.split(","); + for (String item : setItems) { + if (item.indexOf(':') == -1) { + // simple item + try { + Integer.parseInt(item); // Don't need the value; just ensure it's valid + list.add(item); + } catch (NumberFormatException e) { + Log.d(K9.LOG_TAG, "Invalid UID value", e); + } + } else { + // range + list.addAll(getImapRangeValues(item)); + } + } + } + + return list; + } + + /** + * Expand the given number range into a list of individual numbers. + * + *
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ * + * @param range + * The range string as received by the server. + * + * @return The list of IDs as strings in this range. If the range is not valid, an empty list + * is returned. + */ + public static List getImapRangeValues(String range) { + ArrayList list = new ArrayList(); + try { + if (range != null) { + int colonPos = range.indexOf(':'); + if (colonPos > 0) { + int first = Integer.parseInt(range.substring(0, colonPos)); + int second = Integer.parseInt(range.substring(colonPos + 1)); + if (first < second) { + for (int i = first; i <= second; i++) { + list.add(Integer.toString(i)); + } + } else { + for (int i = first; i >= second; i--) { + list.add(Integer.toString(i)); + } + } + } + } + } catch (NumberFormatException e) { + Log.d(K9.LOG_TAG, "Invalid range value", e); + } + + return list; + } +} diff --git a/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java b/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java new file mode 100644 index 000000000..496d6922e --- /dev/null +++ b/tests/src/com/fsck/k9/mail/store/imap/ImapUtilityTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012 The K-9 Dog Walkers + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fsck.k9.mail.store.imap; + +import java.util.List; +import android.test.MoreAsserts; +import junit.framework.TestCase; + +public class ImapUtilityTest extends TestCase { + /** + * Test getting elements of an IMAP sequence set. + */ + public void testGetImapSequenceValues() { + String[] expected; + List actual; + + // Test valid sets + expected = new String[] {"1"}; + actual = ImapUtility.getImapSequenceValues("1"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"1", "3", "2"}; + actual = ImapUtility.getImapSequenceValues("1,3,2"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"4", "5", "6"}; + actual = ImapUtility.getImapSequenceValues("4:6"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"9", "8", "7"}; + actual = ImapUtility.getImapSequenceValues("9:7"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"1", "2", "3", "4", "9", "8", "7"}; + actual = ImapUtility.getImapSequenceValues("1,2:4,9:7"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + // Test partially invalid sets + expected = new String[] { "1", "5" }; + actual = ImapUtility.getImapSequenceValues("1,x,5"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] { "1", "2", "3" }; + actual = ImapUtility.getImapSequenceValues("a:d,1:3"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + // Test invalid sets + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues(""); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues(null); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues("a"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapSequenceValues("1:x"); + MoreAsserts.assertEquals(expected, actual.toArray()); + } + + /** + * Test getting elements of an IMAP range. + */ + public void testGetImapRangeValues() { + String[] expected; + List actual; + + // Test valid ranges + expected = new String[] {"1", "2", "3"}; + actual = ImapUtility.getImapRangeValues("1:3"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[] {"16", "15", "14"}; + actual = ImapUtility.getImapRangeValues("16:14"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + // Test in-valid ranges + expected = new String[0]; + actual = ImapUtility.getImapRangeValues(""); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues(null); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("a"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("6"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("1:3,6"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("1:x"); + MoreAsserts.assertEquals(expected, actual.toArray()); + + expected = new String[0]; + actual = ImapUtility.getImapRangeValues("1:*"); + MoreAsserts.assertEquals(expected, actual.toArray()); + } +}