1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-11 12:05:06 -05:00

Merge branch 'uidplus'

This commit is contained in:
cketti 2012-02-29 14:09:41 +01:00
commit 5591865f17
8 changed files with 573 additions and 60 deletions

View File

@ -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 = "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 = "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_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_BULK = "com.fsck.k9.MessagingController.setFlagBulk";
private static final String PENDING_COMMAND_SET_FLAG = "com.fsck.k9.MessagingController.setFlag"; 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)) { } else if (PENDING_COMMAND_MARK_ALL_AS_READ.equals(command.command)) {
processPendingMarkAllAsRead(command, account); processPendingMarkAllAsRead(command, account);
} else if (PENDING_COMMAND_MOVE_OR_COPY_BULK.equals(command.command)) { } 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); processPendingMoveOrCopy(command, account);
} else if (PENDING_COMMAND_MOVE_OR_COPY.equals(command.command)) { } else if (PENDING_COMMAND_MOVE_OR_COPY.equals(command.command)) {
processPendingMoveOrCopyOld(command, account); processPendingMoveOrCopyOld(command, account);
@ -2079,16 +2082,72 @@ public class MessagingController implements Runnable {
return; return;
} }
PendingCommand command = new PendingCommand(); 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; int length = 3 + uids.length;
command.arguments = new String[length]; command.arguments = new String[length];
command.arguments[0] = srcFolder; command.arguments[0] = srcFolder;
command.arguments[1] = destFolder; command.arguments[1] = destFolder;
command.arguments[2] = Boolean.toString(isCopy); 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); queuePendingCommand(account, command);
} }
private void queueMoveOrCopy(Account account, String srcFolder, String destFolder, boolean isCopy, String uids[], Map<String, String> 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)}.
*
* <p>
* 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.
* </p>
*
* @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. * Process a pending trash message command.
* *
@ -2100,6 +2159,7 @@ public class MessagingController implements Runnable {
throws MessagingException { throws MessagingException {
Folder remoteSrcFolder = null; Folder remoteSrcFolder = null;
Folder remoteDestFolder = null; Folder remoteDestFolder = null;
LocalFolder localDestFolder = null;
try { try {
String srcFolder = command.arguments[0]; String srcFolder = command.arguments[0];
if (account.getErrorFolderName().equals(srcFolder)) { if (account.getErrorFolderName().equals(srcFolder)) {
@ -2107,17 +2167,45 @@ public class MessagingController implements Runnable {
} }
String destFolder = command.arguments[1]; String destFolder = command.arguments[1];
String isCopyS = command.arguments[2]; String isCopyS = command.arguments[2];
String hasNewUidsS = command.arguments[3];
boolean hasNewUids = false;
if (hasNewUidsS != null) {
hasNewUids = Boolean.parseBoolean(hasNewUidsS);
}
Store remoteStore = account.getRemoteStore(); Store remoteStore = account.getRemoteStore();
remoteSrcFolder = remoteStore.getFolder(srcFolder); remoteSrcFolder = remoteStore.getFolder(srcFolder);
Store localStore = account.getLocalStore();
localDestFolder = (LocalFolder) localStore.getFolder(destFolder);
List<Message> messages = new ArrayList<Message>(); List<Message> messages = new ArrayList<Message>();
for (int i = 3; i < command.arguments.length; i++) {
/*
* We split up the localUidMap into two parts while sending the command, here we assemble it back.
*/
Map<String, String> localUidMap = new HashMap<String, String>();
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]; String uid = command.arguments[i];
if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) { if (!uid.startsWith(K9.LOCAL_UID_PREFIX)) {
messages.add(remoteSrcFolder.getMessage(uid)); 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));
}
}
}
boolean isCopy = false; boolean isCopy = false;
if (isCopyS != null) { if (isCopyS != null) {
isCopy = Boolean.parseBoolean(isCopyS); isCopy = Boolean.parseBoolean(isCopyS);
@ -2135,6 +2223,8 @@ public class MessagingController implements Runnable {
Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy: source folder = " + srcFolder Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy: source folder = " + srcFolder
+ ", " + messages.size() + " messages, destination folder = " + destFolder + ", isCopy = " + isCopy); + ", " + messages.size() + " messages, destination folder = " + destFolder + ", isCopy = " + isCopy);
Map <String, String> remoteUidMap = null;
if (!isCopy && destFolder.equals(account.getTrashFolderName())) { if (!isCopy && destFolder.equals(account.getTrashFolderName())) {
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "processingPendingMoveOrCopy doing special case for deleting message"); 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); remoteDestFolder = remoteStore.getFolder(destFolder);
if (isCopy) { if (isCopy) {
remoteSrcFolder.copyMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder); remoteUidMap = remoteSrcFolder.copyMessages(messages.toArray(EMPTY_MESSAGE_ARRAY), remoteDestFolder);
} else { } 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())) { if (!isCopy && Account.EXPUNGE_IMMEDIATELY.equals(account.getExpungePolicy())) {
@ -2159,12 +2249,32 @@ public class MessagingController implements Runnable {
remoteSrcFolder.expunge(); 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<String> remoteSrcUids = remoteUidMap.keySet();
Iterator<String> 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 { } finally {
closeFolder(remoteSrcFolder); closeFolder(remoteSrcFolder);
closeFolder(remoteDestFolder); closeFolder(remoteDestFolder);
} }
} }
private void queueSetFlag(final Account account, final String folderName, final String newState, final String flag, final String[] uids) { 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, private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, final Message[] inMessages,
final String destFolder, final boolean isCopy, MessagingListener listener) { final String destFolder, final boolean isCopy, MessagingListener listener) {
try { try {
Map<String, String> uidMap = new HashMap<String, String>();
Store localStore = account.getLocalStore(); Store localStore = account.getLocalStore();
Store remoteStore = account.getRemoteStore(); Store remoteStore = account.getRemoteStore();
if (!isCopy && (!remoteStore.isMoveCapable() || !localStore.isMoveCapable())) { if (!isCopy && (!remoteStore.isMoveCapable() || !localStore.isMoveCapable())) {
@ -3323,7 +3434,7 @@ public class MessagingController implements Runnable {
fp.add(FetchProfile.Item.ENVELOPE); fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.BODY); fp.add(FetchProfile.Item.BODY);
localSrcFolder.fetch(messages, fp, null); localSrcFolder.fetch(messages, fp, null);
localSrcFolder.copyMessages(messages, localDestFolder); uidMap = localSrcFolder.copyMessages(messages, localDestFolder);
if (unreadCountAffected) { if (unreadCountAffected) {
// If this copy operation changes the unread count in the destination // If this copy operation changes the unread count in the destination
@ -3334,7 +3445,7 @@ public class MessagingController implements Runnable {
} }
} }
} else { } else {
localSrcFolder.moveMessages(messages, localDestFolder); uidMap = localSrcFolder.moveMessages(messages, localDestFolder);
for (Map.Entry<String, Message> entry : origUidMap.entrySet()) { for (Map.Entry<String, Message> entry : origUidMap.entrySet()) {
String origUid = entry.getKey(); String origUid = entry.getKey();
Message message = entry.getValue(); 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); processPendingCommands(account);
@ -3436,6 +3547,7 @@ public class MessagingController implements Runnable {
} }
Store localStore = account.getLocalStore(); Store localStore = account.getLocalStore();
localFolder = localStore.getFolder(folder); localFolder = localStore.getFolder(folder);
Map<String, String> uidMap = null;
if (folder.equals(account.getTrashFolderName()) || K9.FOLDER_NONE.equals(account.getTrashFolderName())) { if (folder.equals(account.getTrashFolderName()) || K9.FOLDER_NONE.equals(account.getTrashFolderName())) {
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Deleting messages in trash folder or trash set to -None-, not copying"); 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) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Deleting messages in normal folder, moving"); 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())) { if (folder.equals(account.getTrashFolderName())) {
queueSetFlag(account, folder, Boolean.toString(true), Flag.DELETED.toString(), uids); queueSetFlag(account, folder, Boolean.toString(true), Flag.DELETED.toString(), uids);
} else { } else {
queueMoveOrCopy(account, folder, account.getTrashFolderName(), false, uids); queueMoveOrCopy(account, folder, account.getTrashFolderName(), false, uids, uidMap);
} }
processPendingCommands(account); processPendingCommands(account);
} else if (account.getDeletePolicy() == Account.DELETE_POLICY_MARK_AS_READ) { } else if (account.getDeletePolicy() == Account.DELETE_POLICY_MARK_AS_READ) {

View File

@ -1,6 +1,7 @@
package com.fsck.k9.mail; package com.fsck.k9.mail;
import java.util.Date; import java.util.Date;
import java.util.Map;
import android.util.Log; import android.util.Log;
import com.fsck.k9.Account; import com.fsck.k9.Account;
@ -102,11 +103,15 @@ public abstract class Folder {
public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener) public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener)
throws MessagingException; throws MessagingException;
public abstract void appendMessages(Message[] messages) throws MessagingException; public abstract Map<String, String> appendMessages(Message[] messages) throws MessagingException;
public void copyMessages(Message[] msgs, Folder folder) throws MessagingException {} public Map<String, String> copyMessages(Message[] msgs, Folder folder) throws MessagingException {
return null;
}
public void moveMessages(Message[] msgs, Folder folder) throws MessagingException {} public Map<String, String> moveMessages(Message[] msgs, Folder folder) throws MessagingException {
return null;
}
public void delete(Message[] msgs, String trashFolderName) throws MessagingException { public void delete(Message[] msgs, String trashFolderName) throws MessagingException {
for (Message message : msgs) { for (Message message : msgs) {

View File

@ -61,6 +61,7 @@ import com.fsck.k9.Account;
import com.fsck.k9.K9; import com.fsck.k9.K9;
import com.fsck.k9.R; import com.fsck.k9.R;
import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.StringUtils;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager;
import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; 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.internet.MimeUtility;
import com.fsck.k9.mail.store.ImapResponseParser.ImapList; import com.fsck.k9.mail.store.ImapResponseParser.ImapList;
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; 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.fsck.k9.mail.transport.imap.ImapSettings;
import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream; import com.jcraft.jzlib.ZOutputStream;
@ -1061,53 +1063,127 @@ public class ImapStore extends Store {
} }
} }
/**
* Copies the given messages to the specified folder.
*
* <p>
* <strong>Note:</strong>
* Only the UIDs of the given {@link Message} instances are used. It is assumed that all
* UIDs represent valid messages in this folder.
* </p>
*
* @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 @Override
public void copyMessages(Message[] messages, Folder folder) throws MessagingException { public Map<String, String> copyMessages(Message[] messages, Folder folder)
throws MessagingException {
if (!(folder instanceof ImapFolder)) { if (!(folder instanceof ImapFolder)) {
throw new MessagingException("ImapFolder.copyMessages passed non-ImapFolder"); throw new MessagingException("ImapFolder.copyMessages passed non-ImapFolder");
} }
if (messages.length == 0) if (messages.length == 0) {
return; return null;
}
ImapFolder iFolder = (ImapFolder)folder; ImapFolder iFolder = (ImapFolder)folder;
checkOpen(); checkOpen();
String[] uids = new String[messages.length]; String[] uids = new String[messages.length];
for (int i = 0, count = messages.length; i < count; i++) { for (int i = 0, count = messages.length; i < count; i++) {
uids[i] = messages[i].getUid(); uids[i] = messages[i].getUid();
} }
try { try {
String remoteDestName = encodeString(encodeFolderName(iFolder.getPrefixedName())); String remoteDestName = encodeString(encodeFolderName(iFolder.getPrefixedName()));
if (!exists(remoteDestName)) { if (!exists(remoteDestName)) {
/* // If the remote trash folder doesn't exist we try to create it.
* 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 +
if (K9.DEBUG) "' for " + getLogId());
Log.i(K9.LOG_TAG, "IMAPMessage.copyMessages: attempting to create remote '" + remoteDestName + "' folder for " + getLogId()); }
iFolder.create(FolderType.HOLDS_MESSAGES); iFolder.create(FolderType.HOLDS_MESSAGES);
} }
if (exists(remoteDestName)) { //TODO: Split this into multiple commands if the command exceeds a certain length.
executeSimpleCommand(String.format("UID COPY %s %s", mConnection.sendCommand(String.format("UID COPY %s %s",
Utility.combine(uids, ','), Utility.combine(uids, ','),
remoteDestName)); remoteDestName), false);
} else { ImapResponse response;
throw new MessagingException("IMAPMessage.copyMessages: remote destination folder " + folder.getName() do {
+ " does not exist and could not be created for " + getLogId() response = mConnection.readResponse();
, true); handleUntaggedResponse(response);
} while (response.mTag == null);
Map<String, String> 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<String> srcUids = ImapUtility.getImapSequenceValues(
copyList.getString(2));
List<String> destUids = ImapUtility.getImapSequenceValues(
copyList.getString(3));
if (srcUids != null && destUids != null) {
if (srcUids.size() == destUids.size()) {
Iterator<String> srcUidsIterator = srcUids.iterator();
Iterator<String> destUidsIterator = destUids.iterator();
uidMap = new HashMap<String, String>();
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) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} }
} }
@Override @Override
public void moveMessages(Message[] messages, Folder folder) throws MessagingException { public Map<String, String> moveMessages(Message[] messages, Folder folder) throws MessagingException {
if (messages.length == 0) if (messages.length == 0)
return; return null;
copyMessages(messages, folder); Map<String, String> uidMap = copyMessages(messages, folder);
setFlags(messages, new Flag[] { Flag.DELETED }, true); setFlags(messages, new Flag[] { Flag.DELETED }, true);
return uidMap;
} }
@Override @Override
@ -1851,20 +1927,30 @@ public class ImapStore extends Store {
} }
/** /**
* Appends the given messages to the selected folder. This implementation also determines * Appends the given messages to the selected folder.
* the new UID of the given message on the IMAP server and sets the Message's UID to the *
* new server UID. * <p>
* 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.
* </p>
*
* @param messages
* The messages to append to the folder.
*
* @return The mapping of original message UIDs to the new server UIDs.
*/ */
@Override @Override
public void appendMessages(Message[] messages) throws MessagingException { public Map<String, String> appendMessages(Message[] messages) throws MessagingException {
checkOpen(); checkOpen();
try { try {
Map<String, String> uidMap = new HashMap<String, String>();
for (Message message : messages) { for (Message message : messages) {
mConnection.sendCommand( mConnection.sendCommand(
String.format("APPEND %s (%s) {%d}", String.format("APPEND %s (%s) {%d}",
encodeString(encodeFolderName(getPrefixedName())), encodeString(encodeFolderName(getPrefixedName())),
combineFlags(message.getFlags()), combineFlags(message.getFlags()),
message.calculateSize()), false); message.calculateSize()), false);
ImapResponse response; ImapResponse response;
do { do {
response = mConnection.readResponse(); response = mConnection.readResponse();
@ -1878,16 +1964,54 @@ public class ImapStore extends Store {
} }
} while (response.mTag == null); } while (response.mTag == null);
String newUid = getUidFromMessageId(message); if (response.size() > 1) {
if (K9.DEBUG) /*
Log.d(K9.LOG_TAG, "Got UID " + newUid + " for message for " + getLogId()); * 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) { 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); 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) { } catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe); throw ioExceptionHandler(mConnection, ioe);
} }

View File

@ -1928,21 +1928,23 @@ public class LocalStore extends Store implements Serializable {
} }
@Override @Override
public void copyMessages(Message[] msgs, Folder folder) throws MessagingException { public Map<String, String> copyMessages(Message[] msgs, Folder folder) throws MessagingException {
if (!(folder instanceof LocalFolder)) { if (!(folder instanceof LocalFolder)) {
throw new MessagingException("copyMessages called with incorrect Folder"); throw new MessagingException("copyMessages called with incorrect Folder");
} }
((LocalFolder) folder).appendMessages(msgs, true); return ((LocalFolder) folder).appendMessages(msgs, true);
} }
@Override @Override
public void moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException { public Map<String, String> moveMessages(final Message[] msgs, final Folder destFolder) throws MessagingException {
if (!(destFolder instanceof LocalFolder)) { if (!(destFolder instanceof LocalFolder)) {
throw new MessagingException("moveMessages called with non-LocalFolder"); throw new MessagingException("moveMessages called with non-LocalFolder");
} }
final LocalFolder lDestFolder = (LocalFolder)destFolder; final LocalFolder lDestFolder = (LocalFolder)destFolder;
final Map<String, String> uidMap = new HashMap<String, String>();
try { try {
database.execute(false, new DbCallback<Void>() { database.execute(false, new DbCallback<Void>() {
@Override @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 " 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.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[] { db.execSQL("UPDATE messages " + "SET folder_id = ?, uid = ? " + "WHERE id = ?", new Object[] {
lDestFolder.getId(), lDestFolder.getId(),
@ -1976,6 +1981,11 @@ public class LocalStore extends Store implements Serializable {
lMessage.getId() 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); LocalMessage placeHolder = new LocalMessage(oldUID, LocalFolder.this);
placeHolder.setFlagInternal(Flag.DELETED, true); placeHolder.setFlagInternal(Flag.DELETED, true);
placeHolder.setFlagInternal(Flag.SEEN, true); placeHolder.setFlagInternal(Flag.SEEN, true);
@ -1987,6 +1997,7 @@ public class LocalStore extends Store implements Serializable {
return null; return null;
} }
}); });
return uidMap;
} catch (WrappedException e) { } catch (WrappedException e) {
throw(MessagingException) e.getCause(); 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). * message, retrieve the appropriate local message instance first (if it already exists).
*/ */
@Override @Override
public void appendMessages(Message[] messages) throws MessagingException { public Map<String, String> appendMessages(Message[] messages) throws MessagingException {
appendMessages(messages, false); return appendMessages(messages, false);
} }
public void destroyMessages(final Message[] messages) throws MessagingException { 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). * message, retrieve the appropriate local message instance first (if it already exists).
* @param messages * @param messages
* @param copy * @param copy
* @return Map<String, String> uidMap of srcUids -> destUids
*/ */
private void appendMessages(final Message[] messages, final boolean copy) throws MessagingException { private Map<String, String> appendMessages(final Message[] messages, final boolean copy) throws MessagingException {
open(OpenMode.READ_WRITE); open(OpenMode.READ_WRITE);
try { try {
final Map<String, String> uidMap = new HashMap<String, String>();
database.execute(true, new DbCallback<Void>() { database.execute(true, new DbCallback<Void>() {
@Override @Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
@ -2085,11 +2098,26 @@ public class LocalStore extends Store implements Serializable {
long oldMessageId = -1; long oldMessageId = -1;
String uid = message.getUid(); String uid = message.getUid();
if (uid == null || copy) { if (uid == null || copy) {
uid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString(); /*
if (!copy) { * Create a new message in the database
message.setUid(uid); */
} String randomLocalUid = K9.LOCAL_UID_PREFIX +
UUID.randomUUID().toString();
if (copy) {
// Save mapping: source UID -> target UID
uidMap.put(uid, randomLocalUid);
} else { } 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); LocalMessage oldMessage = (LocalMessage) getMessage(uid);
if (oldMessage != null) { if (oldMessage != null) {
@ -2169,6 +2197,7 @@ public class LocalStore extends Store implements Serializable {
return null; return null;
} }
}); });
return uidMap;
} catch (WrappedException e) { } catch (WrappedException e) {
throw(MessagingException) e.getCause(); throw(MessagingException) e.getCause();
} }

View File

@ -24,6 +24,7 @@ import java.util.LinkedList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
public class Pop3Store extends Store { public class Pop3Store extends Store {
public static final String STORE_TYPE = "POP3"; public static final String STORE_TYPE = "POP3";
@ -889,7 +890,8 @@ public class Pop3Store extends Store {
} }
@Override @Override
public void appendMessages(Message[] messages) throws MessagingException { public Map<String, String> appendMessages(Message[] messages) throws MessagingException {
return null;
} }
@Override @Override

View File

@ -1334,13 +1334,15 @@ public class WebDavStore extends Store {
} }
@Override @Override
public void copyMessages(Message[] messages, Folder folder) throws MessagingException { public Map<String, String> copyMessages(Message[] messages, Folder folder) throws MessagingException {
moveOrCopyMessages(messages, folder.getName(), false); moveOrCopyMessages(messages, folder.getName(), false);
return null;
} }
@Override @Override
public void moveMessages(Message[] messages, Folder folder) throws MessagingException { public Map<String, String> moveMessages(Message[] messages, Folder folder) throws MessagingException {
moveOrCopyMessages(messages, folder.getName(), true); moveOrCopyMessages(messages, folder.getName(), true);
return null;
} }
@Override @Override
@ -1915,8 +1917,9 @@ public class WebDavStore extends Store {
} }
@Override @Override
public void appendMessages(Message[] messages) throws MessagingException { public Map<String, String> appendMessages(Message[] messages) throws MessagingException {
appendWebDavMessages(messages); appendWebDavMessages(messages);
return null;
} }
public Message[] appendWebDavMessages(Message[] messages) throws MessagingException { public Message[] appendWebDavMessages(Message[] messages) throws MessagingException {

View File

@ -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.
*
* <p>
* Any ranges are expanded into a list of individual numbers.
* </p>
*
* <pre>
* sequence-number = nz-number / "*"
* sequence-range = sequence-number ":" sequence-number
* sequence-set = (sequence-number / sequence-range) *("," sequence-set)
* </pre>
*
* @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<String> getImapSequenceValues(String set) {
ArrayList<String> list = new ArrayList<String>();
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.
*
* <pre>
* sequence-number = nz-number / "*"
* sequence-range = sequence-number ":" sequence-number
* sequence-set = (sequence-number / sequence-range) *("," sequence-set)
* </pre>
*
* @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<String> getImapRangeValues(String range) {
ArrayList<String> list = new ArrayList<String>();
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;
}
}

View File

@ -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<String> 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<String> 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());
}
}