1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-02 00:25:10 -04:00

Fixes Issue 1551

Fixes Issue 1577

Issue 1551: 
Some IMAP servers send untagged EXPUNGEs to IDLEing
clients without ever haven't sent an untagged FETCH.  The untagged
EXPUNGEs are harder to deal with because they don't have a UID.  So,
if the user has elected to have the IDLE connection start with a poll,
we can maintain a map of message sequence numbers to UIDs that we can
use to figure out which message to delete.  To mitigate the risk of
the map falling out of date, we do a UID SEARCH UID before removing
the local copy of the message, just to make sure the message is really
gone from the server.  If we detect an error, do another poll to
resync the map.

Issue 1577:
Restore the removal of notifications for an account when the account's
unread message count goes to 0.
This commit is contained in:
Daniel Applebaum 2010-05-15 19:35:07 +00:00
parent 1502660826
commit b51bce6ebf
7 changed files with 228 additions and 79 deletions

View File

@ -929,7 +929,7 @@ public class MessagingController implements Runnable
LocalStore localStore = account.getLocalStore(); LocalStore localStore = account.getLocalStore();
LocalFolder localFolder = localStore.getFolder(folder); LocalFolder localFolder = localStore.getFolder(folder);
localFolder.setVisibleLimit(localFolder.getVisibleLimit() + account.getDisplayCount()); localFolder.setVisibleLimit(localFolder.getVisibleLimit() + account.getDisplayCount());
synchronizeMailbox(account, folder, listener); synchronizeMailbox(account, folder, listener, null);
} }
catch (MessagingException me) catch (MessagingException me)
{ {
@ -962,14 +962,15 @@ public class MessagingController implements Runnable
* @param account * @param account
* @param folder * @param folder
* @param listener * @param listener
* @param providedRemoteFolder TODO
*/ */
public void synchronizeMailbox(final Account account, final String folder, final MessagingListener listener) public void synchronizeMailbox(final Account account, final String folder, final MessagingListener listener, final Folder providedRemoteFolder)
{ {
putBackground("synchronizeMailbox", listener, new Runnable() putBackground("synchronizeMailbox", listener, new Runnable()
{ {
public void run() public void run()
{ {
synchronizeMailboxSynchronous(account, folder, listener); synchronizeMailboxSynchronous(account, folder, listener, providedRemoteFolder);
} }
}); });
} }
@ -981,8 +982,9 @@ public class MessagingController implements Runnable
* @param folder * @param folder
* *
* TODO Break this method up into smaller chunks. * TODO Break this method up into smaller chunks.
* @param providedRemoteFolder TODO
*/ */
public void synchronizeMailboxSynchronous(final Account account, final String folder, final MessagingListener listener) private void synchronizeMailboxSynchronous(final Account account, final String folder, final MessagingListener listener, Folder providedRemoteFolder)
{ {
Folder remoteFolder = null; Folder remoteFolder = null;
LocalFolder tLocalFolder = null; LocalFolder tLocalFolder = null;
@ -1052,14 +1054,22 @@ public class MessagingController implements Runnable
localUidMap.put(message.getUid(), message); localUidMap.put(message.getUid(), message);
} }
if (providedRemoteFolder != null)
{
if (K9.DEBUG)
Log.v(K9.LOG_TAG, "SYNC: using providedRemoteFolder " + folder);
remoteFolder = providedRemoteFolder;
}
else
{
if (K9.DEBUG) if (K9.DEBUG)
Log.v(K9.LOG_TAG, "SYNC: About to get remote store for " + folder); Log.v(K9.LOG_TAG, "SYNC: About to get remote store for " + folder);
Store remoteStore = account.getRemoteStore(); Store remoteStore = account.getRemoteStore();
if (K9.DEBUG) if (K9.DEBUG)
Log.v(K9.LOG_TAG, "SYNC: About to get remote folder " + folder); Log.v(K9.LOG_TAG, "SYNC: About to get remote folder " + folder);
remoteFolder = remoteStore.getFolder(folder); remoteFolder = remoteStore.getFolder(folder);
/* /*
@ -1092,7 +1102,6 @@ public class MessagingController implements Runnable
} }
} }
} }
/* /*
* Synchronization process: * Synchronization process:
Open the folder Open the folder
@ -1119,7 +1128,7 @@ public class MessagingController implements Runnable
Log.v(K9.LOG_TAG, "SYNC: About to open remote folder " + folder); Log.v(K9.LOG_TAG, "SYNC: About to open remote folder " + folder);
remoteFolder.open(OpenMode.READ_WRITE); remoteFolder.open(OpenMode.READ_WRITE);
}
if (Account.EXPUNGE_ON_POLL.equals(account.getExpungePolicy())) if (Account.EXPUNGE_ON_POLL.equals(account.getExpungePolicy()))
{ {
if (K9.DEBUG) if (K9.DEBUG)
@ -1290,7 +1299,7 @@ public class MessagingController implements Runnable
} }
finally finally
{ {
if (remoteFolder != null) if (providedRemoteFolder == null && remoteFolder != null)
{ {
remoteFolder.close(); remoteFolder.close();
} }
@ -1360,7 +1369,11 @@ public class MessagingController implements Runnable
for (Message message : messages) for (Message message : messages)
{ {
if (isMessageSuppressed(account, folder, message) == false) if (message.isSet(Flag.DELETED))
{
syncFlagMessages.add(message);
}
else if (isMessageSuppressed(account, folder, message) == false)
{ {
Message localMessage = localFolder.getMessage(message.getUid()); Message localMessage = localFolder.getMessage(message.getUid());
@ -1756,16 +1769,23 @@ public class MessagingController implements Runnable
*/ */
if (remoteFolder.supportsFetchingFlags()) if (remoteFolder.supportsFetchingFlags())
{ {
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "SYNC: About to sync flags for " Log.d(K9.LOG_TAG, "SYNC: About to sync flags for "
+ syncFlagMessages.size() + " remote messages for folder " + folder); + syncFlagMessages.size() + " remote messages for folder " + folder);
fp.clear(); fp.clear();
fp.add(FetchProfile.Item.FLAGS); fp.add(FetchProfile.Item.FLAGS);
remoteFolder.fetch(syncFlagMessages.toArray(new Message[0]), fp, null);
List<Message> undeletedMessages = new LinkedList<Message>();
for (Message message : syncFlagMessages)
{
if (message.isSet(Flag.DELETED) == false)
{
undeletedMessages.add(message);
}
}
remoteFolder.fetch(undeletedMessages.toArray(new Message[0]), fp, null);
for (Message remoteMessage : syncFlagMessages) for (Message remoteMessage : syncFlagMessages)
{ {
Message localMessage = localFolder.getMessage(remoteMessage.getUid()); Message localMessage = localFolder.getMessage(remoteMessage.getUid());
@ -4243,7 +4263,7 @@ public class MessagingController implements Runnable
} }
try try
{ {
synchronizeMailboxSynchronous(account, folder.getName(), listener); synchronizeMailboxSynchronous(account, folder.getName(), listener, null);
} }
finally finally
{ {
@ -4286,6 +4306,19 @@ public class MessagingController implements Runnable
if (K9.DEBUG) if (K9.DEBUG)
Log.v(K9.LOG_TAG, "Clearing notification flag for " + account.getDescription()); Log.v(K9.LOG_TAG, "Clearing notification flag for " + account.getDescription());
account.setRingNotified(false); account.setRingNotified(false);
try
{
AccountStats stats = account.getStats(context);
int unreadMessageCount = stats.unreadMessageCount;
if (unreadMessageCount == 0)
{
notifyAccountCancel(context, account);
}
}
catch (MessagingException e)
{
Log.e(K9.LOG_TAG, "Unable to getUnreadMessageCount for account: " + account, e);
}
} }
} }
); );
@ -4429,6 +4462,17 @@ public class MessagingController implements Runnable
*/ */
private boolean notifyAccount(Context context, Account account, Message message) private boolean notifyAccount(Context context, Account account, Message message)
{ {
int unreadMessageCount = 0;
try
{
AccountStats stats = account.getStats(context);
unreadMessageCount = stats.unreadMessageCount;
}
catch (MessagingException e)
{
Log.e(K9.LOG_TAG, "Unable to getUnreadMessageCount for account: " + account, e);
}
// Do not notify if the user does not have notifications // Do not notify if the user does not have notifications
// enabled or if the message has been read // enabled or if the message has been read
if (!account.isNotifyNewMail() || message.isSet(Flag.SEEN)) if (!account.isNotifyNewMail() || message.isSet(Flag.SEEN))
@ -4504,16 +4548,6 @@ public class MessagingController implements Runnable
messageNotice.append(context.getString(R.string.notification_new_title)); messageNotice.append(context.getString(R.string.notification_new_title));
} }
int unreadMessageCount = 0;
try
{
AccountStats stats = account.getStats(context);
unreadMessageCount = stats.unreadMessageCount;
}
catch (MessagingException e)
{
Log.e(K9.LOG_TAG, "Unable to getUnreadMessageCount for account: " + account, e);
}
NotificationManager notifMgr = NotificationManager notifMgr =
(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);

View File

@ -69,12 +69,15 @@ public class MessagingControllerPushReceiver implements PushReceiver
List<Message> messages) List<Message> messages)
{ {
controller.messagesArrived(account, folder, messages, true); controller.messagesArrived(account, folder, messages, true);
} }
public void messagesArrived(Folder folder, List<Message> messages) public void messagesArrived(Folder folder, List<Message> messages)
{ {
controller.messagesArrived(account, folder, messages, false); controller.messagesArrived(account, folder, messages, false);
} }
public void messagesRemoved(Folder folder, List<Message> messages)
{
controller.messagesArrived(account, folder, messages, true);
}
public void syncFolder(Folder folder) public void syncFolder(Folder folder)
{ {
@ -96,7 +99,7 @@ public class MessagingControllerPushReceiver implements PushReceiver
{ {
latch.countDown(); latch.countDown();
} }
}); }, folder);
if (K9.DEBUG) if (K9.DEBUG)
Log.v(K9.LOG_TAG, "syncFolder(" + folder.getName() + ") about to await latch release"); Log.v(K9.LOG_TAG, "syncFolder(" + folder.getName() + ") about to await latch release");

View File

@ -202,7 +202,7 @@ public class FolderList extends K9ListActivity
wakeLock.release(); wakeLock.release();
} }
}; };
MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, folder.name, listener); MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, folder.name, listener, null);
sendMail(mAccount); sendMail(mAccount);
} }

View File

@ -1097,7 +1097,7 @@ public class MessageList
private void checkMail(Account account, String folderName) private void checkMail(Account account, String folderName)
{ {
mController.synchronizeMailbox(account, folderName, mAdapter.mListener); mController.synchronizeMailbox(account, folderName, mAdapter.mListener, null);
sendMail(account); sendMail(account);
} }

View File

@ -109,7 +109,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
store.checkSettings(); store.checkSettings();
MessagingController.getInstance(getApplication()).listFolders(mAccount, true, null); MessagingController.getInstance(getApplication()).listFolders(mAccount, true, null);
MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, K9.INBOX , null); MessagingController.getInstance(getApplication()).synchronizeMailbox(mAccount, K9.INBOX , null, null);
} }
if (mDestroyed) if (mDestroyed)

View File

@ -9,6 +9,7 @@ public interface PushReceiver
public void syncFolder(Folder folder); public void syncFolder(Folder folder);
public void messagesArrived(Folder folder, List<Message> mess); public void messagesArrived(Folder folder, List<Message> mess);
public void messagesFlagsChanged(Folder folder, List<Message> mess); public void messagesFlagsChanged(Folder folder, List<Message> mess);
public void messagesRemoved(Folder folder, List<Message> mess);
public String getPushState(String folderName); public String getPushState(String folderName);
public void pushError(String errorMessage, Exception e); public void pushError(String errorMessage, Exception e);
public void setPushActive(String folderName, boolean enabled); public void setPushActive(String folderName, boolean enabled);

View File

@ -39,6 +39,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -448,6 +449,8 @@ public class ImapStore extends Store
private OpenMode mMode; private OpenMode mMode;
private boolean mExists; private boolean mExists;
private ImapStore store = null; private ImapStore store = null;
Map<Integer, String> msgSeqUidMap = new ConcurrentHashMap<Integer, String>();
public ImapFolder(ImapStore nStore, String name) public ImapFolder(ImapStore nStore, String name)
{ {
@ -522,6 +525,7 @@ public class ImapStore extends Store
// 2 OK [READ-WRITE] Select completed. // 2 OK [READ-WRITE] Select completed.
try try
{ {
msgSeqUidMap.clear();
String command = String.format((mode == OpenMode.READ_WRITE ? "SELECT" : "EXAMINE") + " \"%s\"", String command = String.format((mode == OpenMode.READ_WRITE ? "SELECT" : "EXAMINE") + " \"%s\"",
encodeFolderName(getPrefixedName())); encodeFolderName(getPrefixedName()));
@ -967,6 +971,19 @@ public class ImapStore extends Store
return search(searcher, listener); return search(searcher, listener);
} }
protected Message[] getMessagesFromUids(final List<String> mesgUids, final boolean includeDeleted, final MessageRetrievalListener listener)
throws MessagingException
{
ImapSearcher searcher = new ImapSearcher()
{
public List<ImapResponse> search() throws IOException, MessagingException
{
return executeSimpleCommand(String.format("UID SEARCH UID %s" + (includeDeleted ? "" : " NOT DELETED"), Utility.combine(mesgUids.toArray(), ',')));
}
};
return search(searcher, listener);
}
private Message[] search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException private Message[] search(ImapSearcher searcher, MessageRetrievalListener listener) throws MessagingException
{ {
@ -1152,6 +1169,22 @@ public class ImapStore extends Store
{ {
ImapList fetchList = (ImapList)response.getKeyedValue("FETCH"); ImapList fetchList = (ImapList)response.getKeyedValue("FETCH");
String uid = fetchList.getKeyedString("UID"); String uid = fetchList.getKeyedString("UID");
int msgSeq = response.getNumber(0);
if (uid != null)
{
try
{
msgSeqUidMap.put(msgSeq, uid);
if (K9.DEBUG)
{
Log.v(K9.LOG_TAG, "Stored uid '" + uid + "' for msgSeq " + msgSeq + " into map " + msgSeqUidMap.toString());
}
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "Unable to store uid '" + uid + "' for msgSeq " + msgSeq);
}
}
Message message = messageMap.get(uid); Message message = messageMap.get(uid);
if (message == null) if (message == null)
@ -1371,7 +1404,7 @@ public class ImapStore extends Store
{ {
mMessageCount--; mMessageCount--;
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Got untagged EXPUNGE with value " + mMessageCount + " for " + getLogId()); Log.d(K9.LOG_TAG, "Got untagged EXPUNGE with mMessageCount " + mMessageCount + " for " + getLogId());
} }
// if (response.size() > 1) { // if (response.size() > 1) {
// Object bracketedObj = response.get(1); // Object bracketedObj = response.get(1);
@ -2588,6 +2621,7 @@ public class ImapStore extends Store
final AtomicBoolean doneSent = new AtomicBoolean(false); final AtomicBoolean doneSent = new AtomicBoolean(false);
final AtomicInteger delayTime = new AtomicInteger(NORMAL_DELAY_TIME); final AtomicInteger delayTime = new AtomicInteger(NORMAL_DELAY_TIME);
final AtomicInteger idleFailureCount = new AtomicInteger(0); final AtomicInteger idleFailureCount = new AtomicInteger(0);
final AtomicBoolean needsPoll = new AtomicBoolean(false);
List<ImapResponse> storedUntaggedResponses = new ArrayList<ImapResponse>(); List<ImapResponse> storedUntaggedResponses = new ArrayList<ImapResponse>();
public ImapFolderPusher(ImapStore store, String name, PushReceiver nReceiver) public ImapFolderPusher(ImapStore store, String name, PushReceiver nReceiver)
@ -2650,7 +2684,7 @@ public class ImapStore extends Store
Log.e(K9.LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e); Log.e(K9.LOG_TAG, "Unable to get oldUidNext for " + getLogId(), e);
} }
ImapConnection oldConnection = mConnection; ImapConnection oldConnection = mConnection;
List<ImapResponse> responses = internalOpen(OpenMode.READ_ONLY); internalOpen(OpenMode.READ_ONLY);
if (mConnection == null) if (mConnection == null)
{ {
receiver.pushError("Could not establish connection for IDLE", null); receiver.pushError("Could not establish connection for IDLE", null);
@ -2664,12 +2698,11 @@ public class ImapStore extends Store
throw new MessagingException("IMAP server is not IDLE capable:" + mConnection.toString()); throw new MessagingException("IMAP server is not IDLE capable:" + mConnection.toString());
} }
if (responses != null) if (mAccount.isPushPollOnConnect() && (mConnection != oldConnection || needsPoll.getAndSet(false) == true))
{
handleUntaggedResponses(responses);
}
if (mAccount.isPushPollOnConnect() && mConnection != oldConnection)
{ {
List<ImapResponse> untaggedResponses = new ArrayList<ImapResponse>(storedUntaggedResponses);
storedUntaggedResponses.clear();
processUntaggedResponses(untaggedResponses);
receiver.syncFolder(ImapFolderPusher.this); receiver.syncFolder(ImapFolderPusher.this);
} }
int startUid = oldUidNext; int startUid = oldUidNext;
@ -2842,10 +2875,11 @@ public class ImapStore extends Store
skipSync = true; skipSync = true;
} }
List<Integer> flagSyncMsgSeqs = new ArrayList<Integer>(); List<Integer> flagSyncMsgSeqs = new ArrayList<Integer>();
List<String> removeMsgUids = new LinkedList<String>();
for (ImapResponse response : responses) for (ImapResponse response : responses)
{ {
oldMessageCount += processUntaggedResponse(oldMessageCount, response, flagSyncMsgSeqs); oldMessageCount += processUntaggedResponse(oldMessageCount, response, flagSyncMsgSeqs, removeMsgUids);
} }
if (skipSync == false) if (skipSync == false)
{ {
@ -2865,6 +2899,10 @@ public class ImapStore extends Store
{ {
syncMessages(flagSyncMsgSeqs); syncMessages(flagSyncMsgSeqs);
} }
if (removeMsgUids.size() > 0)
{
removeMessages(removeMsgUids);
}
} }
private void syncMessages(int end, boolean newArrivals) throws MessagingException private void syncMessages(int end, boolean newArrivals) throws MessagingException
@ -2939,7 +2977,45 @@ public class ImapStore extends Store
} }
} }
protected int processUntaggedResponse(int oldMessageCount, ImapResponse response, List<Integer> flagSyncMsgSeqs) private void removeMessages(List<String> removeUids)
{
List<Message> messages = new ArrayList<Message>(removeUids.size());
try
{
Message[] existingMessages = getMessagesFromUids(removeUids, true, null);
for (Message existingMessage : existingMessages)
{
needsPoll.set(true);
msgSeqUidMap.clear();
String existingUid = existingMessage.getUid();
Log.w(K9.LOG_TAG, "Message with UID " + existingUid + " still exists on server, not expunging");
removeUids.remove(existingUid);
}
for (String uid : removeUids)
{
ImapMessage message = new ImapMessage(uid, this);
try
{
message.setFlagInternal(Flag.DELETED, true);
}
catch (MessagingException me)
{
Log.e(K9.LOG_TAG, "Unable to set DELETED flag on message " + message.getUid());
}
messages.add(message);
}
receiver.messagesRemoved(this, messages);
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "Cannot remove EXPUNGEd messages", e);
return;
}
}
protected int processUntaggedResponse(int oldMessageCount, ImapResponse response, List<Integer> flagSyncMsgSeqs, List<String> removeMsgUids)
{ {
super.handleUntaggedResponse(response); super.handleUntaggedResponse(response);
int messageCountDelta = 0; int messageCountDelta = 0;
@ -2950,7 +3026,9 @@ public class ImapStore extends Store
Object responseType = response.get(1); Object responseType = response.get(1);
if (ImapResponseParser.equalsIgnoreCase(responseType, "FETCH")) if (ImapResponseParser.equalsIgnoreCase(responseType, "FETCH"))
{ {
Log.i(K9.LOG_TAG, "Got FETCH " + response);
int msgSeq = response.getNumber(0); int msgSeq = response.getNumber(0);
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Got untagged FETCH for msgseq " + msgSeq + " for " + getLogId()); Log.d(K9.LOG_TAG, "Got untagged FETCH for msgseq " + msgSeq + " for " + getLogId());
@ -2984,6 +3062,39 @@ public class ImapStore extends Store
} }
} }
flagSyncMsgSeqs.addAll(newSeqs); flagSyncMsgSeqs.addAll(newSeqs);
List<Integer> msgSeqs = new ArrayList<Integer>(msgSeqUidMap.keySet());
Collections.sort(msgSeqs); // Have to do comparisons in order because of msgSeq reductions
for (Integer msgSeqNumI : msgSeqs)
{
if (K9.DEBUG)
{
Log.v(K9.LOG_TAG, "Comparing EXPUNGEd msgSeq " + msgSeq + " to " + msgSeqNumI);
}
int msgSeqNum = msgSeqNumI;
if (msgSeqNum == msgSeq)
{
String uid = msgSeqUidMap.get(msgSeqNum);
if (K9.DEBUG)
{
Log.d(K9.LOG_TAG, "Scheduling removal of UID " + uid + " because msgSeq " + msgSeqNum + " was expunged");
}
removeMsgUids.add(uid);
msgSeqUidMap.remove(msgSeqNum);
}
else if (msgSeqNum > msgSeq)
{
String uid = msgSeqUidMap.get(msgSeqNum);
if (K9.DEBUG)
{
Log.d(K9.LOG_TAG, "Reducing msgSeq for UID " + uid + " from " + msgSeqNum + " to " + (msgSeqNum - 1));
}
msgSeqUidMap.remove(msgSeqNum);
msgSeqUidMap.put(msgSeqNum-1, uid);
}
}
} }
} }
catch (Exception e) catch (Exception e)