Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CONDSTORE and QRESYNC #2607

Closed
wants to merge 90 commits into from

Conversation

rhari991
Copy link

@rhari991 rhari991 commented Jun 27, 2017

This PR does several things

  • Moves the folder synchronization code from MessagingController into separate classes depending on the protocol (ImapSyncInteractor for IMAP accounts and LegacySyncInteractor for POP3 and WebDAV accounts)
  • Modifies the MessagingControllerPushReceiver so that a normal synchronization is performed when untagged responses are received through IDLE, instead of response-specific actions. This synchronization is done through a separate connection instead of reusing the connection through which IDLE responses are received.
  • Adds support for the CONDSTORE and QRESYNC IMAP extensions as described in RFC 7162.

public List<ImapMessage> getChangedMessagesUsingCondstore(long modseq) throws MessagingException {
checkOpen();
if (!supportsModSeq()) {
throw new MessagingException(String.format(Locale.US, "Folder %s does not support modification " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put the string on a new line so you don't have to build a new one every time.

@rhari991 rhari991 self-assigned this Jun 30, 2017
@rhari991 rhari991 added type: architecture Architecture of the project and high level design being worked on and removed type: architecture Architecture of the project and high level design being worked on labels Jun 30, 2017
@rhari991
Copy link
Author

rhari991 commented Jul 6, 2017

Currently, the sync process is as follows :

  1. Get the UIDs of the most recent n messages on the server, where n is the number of messages displayed to the user (via UID SEARCH). From these, messages which arrived on a date earlier than the earliest poll date are removed.

  2. Delete local messages which are not present in the above group. Then check and update whether more messages are present in the mailbox (via one or more UID SEARCH).

  3. Divide the messages into two groups - one for messages that need to be downloaded fully (if not present locally) and one for syncing flags (if it is present locally).

  4. The unsynced messages are fetched.
    a) Initially, the flags and envelope for all unsynced messages are fetched.
    b) The messages are then divided into large and small message groups depending on the maximum auto-download message size.
    c) Download all the small messages fully (using grouped UID FETCH requests).
    d) Download partial/sane bodies for large messages (one UID FETCH per message, and multiple if structure is present).

  5. The flags for all the messages present in the local store are fetched and updated.

When CONDSTORE is used :

  • In step 1, the UIDs for all messages that are present/going to be present in the mailbox are fetched. This is necessary in order to sync remote deletions and some use cases such as the user pressing the "Load more messages" button (historical data needs to be fetched). Because of these reasons, this step cannot be optimized using the previously stored mod-sequence.

  • Related to the point above, one optimization could be to remove the "Load more messages" button and instead automatically download more messages if the user scrolls to the bottom of the message list. This should feel more natural, and will free us from having to make a network request to check if more messages are available.

  • For step 5, the optimization that can be done is to only fetch flags for those messages whose contents have changed. This is done by sending an extra UID SEARCH command with the previously known mod sequence and then syncing the flags for the returned message uids only.

  • An extra UID FETCH command also needs to be issued every time we get a response through IDLE in order to update the highest modseq value.

Thoughts ? @Valodim @philipwhiuk @cketti

@Valodim
Copy link
Contributor

Valodim commented Jul 6, 2017

description matches with my understanding of the algorithm.

Regarding the other points:

  • Fetching all UIDs for finding deleted messages - shouldn't that be solved by QRESYNC?

  • I don't understand how a fetch-on-scroll mechanism would free us from that network request. Either way, it would also be too much of a semantic shift to bring in here.

  • Regarding IDLE, I think we are not doing us any favors trying to be clever there. Just open a connection, then when anything happens just trigger the usual sync process.

final String queryString, final String[] placeHolders
final MessageRetrievalListener<LocalMessage> listener,
final LocalFolder folder,
final String queryString, final String[] placeHolders
) throws MessagingException {
final List<LocalMessage> messages = new ArrayList<>();
final int j = database.execute(false, new DbCallback<Integer>() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Valodim @cketti Any idea why we first query 10 rows and then the remaining ones in this method ?

@rhari991 rhari991 removed their assignment Aug 13, 2017
@rhari991 rhari991 mentioned this pull request Aug 13, 2017
@rhari991
Copy link
Author

There are 7 tests in MessagingControllerTest (which test the sendPendingMessagesSynchronous method) and 8 tests in MigrationTest that are failing in the Travis build. These are failing for me when I run them on the master branch too. Does this happen with you guys too or am I missing something ? @Valodim @philipwhiuk @cketti

When running MigrationTest , I get this exception for all the failed tests

`android.content.res.Resources$NotFoundException: Unable to find resource ID #0x7f07018e in packages [android, org.robolectric.default]

at org.robolectric.shadows.ShadowAssetManager.getResName(ShadowAssetManager.java:868)
at org.robolectric.shadows.ShadowAssetManager.resolveResourceValue(ShadowAssetManager.java:676)
at org.robolectric.shadows.ShadowAssetManager.resolve(ShadowAssetManager.java:636)
at org.robolectric.shadows.ShadowAssetManager.getAndResolve(ShadowAssetManager.java:630)
at org.robolectric.shadows.ShadowAssetManager.getResourceText(ShadowAssetManager.java:226)
at android.content.res.AssetManager.getResourceText(AssetManager.java)
at android.content.res.Resources.getText(Resources.java:295)
at android.content.res.Resources.getString(Resources.java:385)
at android.content.Context.getString(Context.java:377)
at com.fsck.k9.Account.<init>(Account.java:331)
at com.fsck.k9.Preferences.newAccount(Preferences.java:113)
at com.fsck.k9.mailstore.MigrationTest.getNewAccount(MigrationTest.java:725)
at com.fsck.k9.mailstore.MigrationTest.setUp(MigrationTest.java:49)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:24)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:339)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:259)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:41)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:199)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:262)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)`

The related line, Account.java:331 seems to take a String resource which has not been mocked yet ?

For the failing tests in MessagingControllerTest, the assertions are failing.

@philipwhiuk
Copy link
Contributor

philipwhiuk commented Aug 13, 2017

I believe this occurs because IntelliJ (and thus Android Studio)'s default Run Configuration is based on the project working directory not the module working directory ( / instead of /k9mail ). Hence it can't find the resources.

When I run the tests for master via /gradlew test they pass.

When I run your branch's tests in the same way they fail. A lot of the failures are due to this:

java.lang.NullPointerException
	at com.fsck.k9.mail.store.imap.ImapStore.isStoreUriImap(ImapStore.java:140)
	at com.fsck.k9.controller.MessagingController.synchronizeMailboxSynchronous(MessagingController.java:757)
	at com.fsck.k9.controller.MessagingControllerTest.synchronizeMailboxSynchronous_withAccountSupportingFetchingFlags_shouldFetchUnsychronizedMessagesListAndFlags(MessagingControllerTest.java:793)
        at ...

which should be fairly trivial to fix.

@rhari991
Copy link
Author

@philipwhiuk thanks for the tip, setting the JUnit working directory did the trick

@@ -3,6 +3,7 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of making this a super object in terms of configuration. ImapConfig would be a good name for a class that could house the new stuff being added here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return hasCapability(Capabilities.CONDSTORE);
}

boolean isQresyncCapable() throws IOException, MessagingException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is isCondstoreCapable public and this not?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isCondstoreCapable is used in the FolderSelectedStateCommand.getCommandLengthLimit() method, which is in the selectedstate subpackage.

if (response.hasOpenMode()) {
this.mode = response.getOpenMode();
}
if (response == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This null check is pointless. Line 182 would throw an NPE before it got here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}
} catch (Exception e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception is a very broad catch case. Try to narrow this down a bit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny, the ConcurrentHashMap.put() method does not throw any checked exceptions, so this try/catch block is useless. I'm not sure why this was there in the first place. I moved around a lot of original code without modifying it.

}
}

private void handleExpungeResponses(List<ImapResponse> imapResponses) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really 'handle' the response - it just searches them for a higher mod seq.

updateHighestModSequenceFromResponses.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

new MessageRetrievalListener<ImapMessage>() {
@Override
public void messageStarted(String uid, int number, int ofTotal) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think anything is required here ?

if (folderName.equals(account.getTrashFolderName()) ||
folderName.equals(account.getSentFolderName()) ||
folderName.equals(account.getDraftsFolderName())) {
if (!remoteFolder.exists()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not combine these if's to reduce the nesting a bit?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -74,6 +75,8 @@
private static final long serialVersionUID = -1973296520918624767L;
private static final int MAX_BODY_SIZE_FOR_DATABASE = 16 * 1024;
static final long INVALID_MESSAGE_PART_ID = -1;
private static final int INVALID_UID_VALIDITY = -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redefining the same constants as in the library. Maybe they should be visible if you need them here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

mFolderId = cursor.getInt(LocalStore.FOLDER_ID_INDEX);
mName = cursor.getString(LocalStore.FOLDER_NAME_INDEX);
mVisibleLimit = cursor.getInt(LocalStore.FOLDER_VISIBLE_LIMIT_INDEX);

//The index check here is because of an issue that occurs while upgrading the db to V55 - at that point, the
//uid_validity and highest_mod_Seq columns do not exist yet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why we have DatabaseMigration...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that the open(Cursor) method is called when upgrading to V55, but the uid_validity and highest_mod_seq columns do not exist yet. They are added only in V61. There does not seem to be a reliable way to recreate the open(Cursor) method in MigrationTo61 .


catch (Exception e) {
if (!"X_BAD_FLAG".equals(flag)) {
Timber.w("Unable to parse flag %s", flag);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not check for X_BAD_FLAG first?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@rhari991
Copy link
Author

@philipwhiuk made the changes

@rhari991 rhari991 force-pushed the condstore-support branch 2 times, most recently from 8e03dc0 to 7831ffe Compare August 22, 2017 18:23
@rhari991 rhari991 force-pushed the gsoc-2017-rhari991 branch 4 times, most recently from 39156ee to 524d8ba Compare September 8, 2017 15:32
@rhari991 rhari991 force-pushed the condstore-support branch 2 times, most recently from 48d146b to 33dc02c Compare September 10, 2017 06:37
@rhari991
Copy link
Author

@Valodim @philipwhiuk I'm not sure why/how the CertificationException is showing up, could you have a look ?

@Valodim
Copy link
Contributor

Valodim commented Sep 12, 2017

possibly related to #2675?

@cketti cketti closed this Dec 28, 2022
@cketti cketti deleted the condstore-support branch January 28, 2023 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants