Skip to content

Commit

Permalink
Merge pull request thunderbird#2924 from k9mail/imap_refactoring_refa…
Browse files Browse the repository at this point in the history
…ctored

'Refactor IMAP commands' refactored
  • Loading branch information
Harikrishnan Rajan committed Nov 10, 2017
2 parents fa9fc2d + 97b4c06 commit 231cab3
Show file tree
Hide file tree
Showing 18 changed files with 884 additions and 368 deletions.
Expand Up @@ -3,6 +3,7 @@

class Capabilities {
public static final String IDLE = "IDLE";
public static final String CONDSTORE = "CONDSTORE";
public static final String SASL_IR = "SASL-IR";
public static final String AUTH_XOAUTH2 = "AUTH=XOAUTH2";
public static final String AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
Expand Down
Expand Up @@ -14,4 +14,8 @@ class Commands {
public static final String LOGIN = "LOGIN";
public static final String LIST = "LIST";
public static final String NOOP = "NOOP";
public static final String UID_SEARCH = "UID SEARCH";
public static final String UID_STORE = "UID STORE";
public static final String UID_FETCH = "UID FETCH";
public static final String UID_COPY = "UID COPY";
}
@@ -0,0 +1,94 @@
package com.fsck.k9.mail.store.imap;


import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;


class IdGrouper {
static GroupedIds groupIds(Set<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw new IllegalArgumentException("groupId() must be called with non-empty set of ids");
}

if (ids.size() < 2) {
return new GroupedIds(ids, Collections.<ContiguousIdGroup>emptyList());
}

TreeSet<Long> orderedIds = new TreeSet<Long>(ids);
Iterator<Long> orderedIdIterator = orderedIds.iterator();
Long previousId = orderedIdIterator.next();

TreeSet<Long> remainingIds = new TreeSet<Long>();
remainingIds.add(previousId);
List<ContiguousIdGroup> idGroups = new ArrayList<>();
long currentIdGroupStart = -1L;
long currentIdGroupEnd = -1L;
while (orderedIdIterator.hasNext()) {
Long currentId = orderedIdIterator.next();
if (previousId + 1L == currentId) {
if (currentIdGroupStart == -1L) {
remainingIds.remove(previousId);
currentIdGroupStart = previousId;
currentIdGroupEnd = currentId;
} else {
currentIdGroupEnd = currentId;
}
} else {
if (currentIdGroupStart != -1L) {
idGroups.add(new ContiguousIdGroup(currentIdGroupStart, currentIdGroupEnd));
currentIdGroupStart = -1L;
}
remainingIds.add(currentId);
}

previousId = currentId;
}

if (currentIdGroupStart != -1L) {
idGroups.add(new ContiguousIdGroup(currentIdGroupStart, currentIdGroupEnd));
}

return new GroupedIds(remainingIds, idGroups);
}


static class GroupedIds {
public final Set<Long> ids;
public final List<ContiguousIdGroup> idGroups;


GroupedIds(Set<Long> ids, List<ContiguousIdGroup> idGroups) {
if (ids.isEmpty() && idGroups.isEmpty()) {
throw new IllegalArgumentException("Must have at least one id");
}

this.ids = ids;
this.idGroups = idGroups;
}
}

static class ContiguousIdGroup {
public final long start;
public final long end;


ContiguousIdGroup(long start, long end) {
if (start >= end) {
throw new IllegalArgumentException("start >= end");
}

this.start = start;
this.end = end;
}

@Override
public String toString() {
return start + ":" + end;
}
}
}
@@ -0,0 +1,67 @@
package com.fsck.k9.mail.store.imap;


import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import com.fsck.k9.mail.store.imap.IdGrouper.ContiguousIdGroup;
import com.fsck.k9.mail.store.imap.IdGrouper.GroupedIds;


class ImapCommandSplitter {
static List<String> splitCommand(String prefix, String suffix, GroupedIds groupedIds, int lengthLimit) {
List<String> commands = new ArrayList<>();
Set<Long> workingIdSet = new TreeSet<>(groupedIds.ids);
List<ContiguousIdGroup> workingIdGroups = new ArrayList<>(groupedIds.idGroups);

int suffixLength = suffix.length();
int staticCommandLength = prefix.length() + suffixLength + 2;
while (!workingIdSet.isEmpty() || !workingIdGroups.isEmpty()) {
StringBuilder commandBuilder = new StringBuilder(prefix).append(' ');
int length = staticCommandLength;
while (length < lengthLimit) {
if (!workingIdSet.isEmpty()) {
Long id = workingIdSet.iterator().next();
String idString = Long.toString(id);

length += idString.length() + 1;
if (length >= lengthLimit) {
break;
}

commandBuilder.append(idString).append(',');
workingIdSet.remove(id);
} else if (!workingIdGroups.isEmpty()) {
ContiguousIdGroup idGroup = workingIdGroups.iterator().next();
String idGroupString = idGroup.toString();

length += idGroupString.length() + 1;
if (length >= lengthLimit) {
break;
}

commandBuilder.append(idGroupString).append(',');
workingIdGroups.remove(idGroup);
} else {
break;
}
}

if (suffixLength != 0) {
// Replace the last comma with a space
commandBuilder.setCharAt(commandBuilder.length() - 1, ' ');
commandBuilder.append(suffix);
} else {
// Remove last comma
commandBuilder.setLength(commandBuilder.length() - 1);
}

String command = commandBuilder.toString();
commands.add(command);
}

return commands;
}
}
Expand Up @@ -17,6 +17,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
Expand All @@ -41,6 +42,7 @@
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import com.fsck.k9.mail.store.imap.IdGrouper.GroupedIds;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream;
import javax.net.ssl.SSLException;
Expand All @@ -60,6 +62,18 @@
class ImapConnection {
private static final int BUFFER_SIZE = 1024;

/* The below limits are 20 octets less than the recommended limits, in order to compensate for
* the length of the command tag, the space after the tag and the CRLF at the end of the command
* (these are not taken into account when calculating the length of the command). For more
* information, refer to section 4 of RFC 7162.
*
* The length limit for servers supporting the CONDSTORE extension is large in order to support
* the QRESYNC parameter to the SELECT/EXAMINE commands, which accept a list of known message
* sequence numbers as well as their corresponding UIDs.
*/
private static final int LENGTH_LIMIT_WITHOUT_CONDSTORE = 980;
private static final int LENGTH_LIMIT_WITH_CONDSTORE = 8172;


private final ConnectivityManager connectivityManager;
private final OAuth2TokenProvider oauthTokenProvider;
Expand All @@ -77,6 +91,7 @@ class ImapConnection {
private Exception stacktraceForClose;
private boolean open = false;
private boolean retryXoauth2WithNewToken = true;
private int lineLengthLimit;


public ImapConnection(ImapSettings settings, TrustedSocketFactory socketFactory,
Expand Down Expand Up @@ -683,6 +698,10 @@ protected boolean hasCapability(String capability) {
return capabilities.contains(capability.toUpperCase(Locale.US));
}

public boolean isCondstoreCapable() {
return hasCapability(Capabilities.CONDSTORE);
}

protected boolean isIdleCapable() {
if (K9MailLib.isDebug()) {
Timber.v("Connection %s has %d capabilities", getLogId(), capabilities.size());
Expand Down Expand Up @@ -734,6 +753,21 @@ public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive
}
}

List<ImapResponse> executeCommandWithIdSet(String commandPrefix, String commandSuffix, Set<Long> ids)
throws IOException, MessagingException {

GroupedIds groupedIds = IdGrouper.groupIds(ids);
List<String> splitCommands = ImapCommandSplitter.splitCommand(
commandPrefix, commandSuffix, groupedIds, getLineLengthLimit());

List<ImapResponse> responses = new ArrayList<>();
for (String splitCommand : splitCommands) {
responses.addAll(executeSimpleCommand(splitCommand));
}

return responses;
}

public List<ImapResponse> readStatusResponse(String tag, String commandToLog, UntaggedHandler untaggedHandler)
throws IOException, NegativeImapResponseException {
return responseParser.readStatusResponse(tag, commandToLog, getLogId(), untaggedHandler);
Expand Down Expand Up @@ -843,4 +877,8 @@ private ImapResponse readContinuationResponse(String tag) throws IOException, Me

return response;
}

int getLineLengthLimit() {
return isCondstoreCapable() ? LENGTH_LIMIT_WITH_CONDSTORE : LENGTH_LIMIT_WITHOUT_CONDSTORE;
}
}

0 comments on commit 231cab3

Please sign in to comment.