[geary/wip/721790-gmail-delete] First pass through
- From: Jim Nelson <jnelson src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/721790-gmail-delete] First pass through
- Date: Fri, 9 Jan 2015 21:10:21 +0000 (UTC)
commit ac49f46ab101dc99211647c0e53492d2c7630079
Author: Jim Nelson <jim yorba org>
Date: Fri Jan 9 13:09:02 2015 -0800
First pass through
This collects the COPYUID UIDs and opens a special one-shot connection
to the Trash folder to delete them. This is faster than doing a full
synchronized open of the Folder.
.../gmail/imap-engine-gmail-folder.vala | 42 +++++++-
.../imap-engine/imap-engine-generic-account.vala | 25 ++++
.../imap-engine/imap-engine-minimal-folder.vala | 19 +++-
.../replay-ops/imap-engine-copy-email.vala | 10 ++-
.../replay-ops/imap-engine-create-email.vala | 2 +-
.../replay-ops/imap-engine-move-email.vala | 2 +-
.../replay-ops/imap-engine-remove-email.vala | 3 +-
src/engine/imap/api/imap-account.vala | 31 +++++-
src/engine/imap/api/imap-folder.vala | 64 +++++++++--
src/engine/imap/command/imap-message-set.vala | 124 ++++++++++++++++++++
src/engine/imap/parameter/imap-list-parameter.vala | 38 ++++++
.../imap/parameter/imap-number-parameter.vala | 8 +-
.../imap/parameter/imap-string-parameter.vala | 18 +++
.../imap/response/imap-response-code-type.vala | 1 +
src/engine/imap/response/imap-response-code.vala | 20 +++
src/engine/imap/transport/imap-deserializer.vala | 2 +
src/engine/util/util-collection.vala | 4 +
17 files changed, 391 insertions(+), 22 deletions(-)
---
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
index e8839c0..6f73415 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
@@ -5,7 +5,7 @@
*/
private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archive,
- FolderSupport.Create {
+ FolderSupport.Create, FolderSupport.Remove {
public GmailFolder(GmailAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
base (account, remote, local, local_folder, special_folder_type);
@@ -21,5 +21,45 @@ private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archiv
Cancellable? cancellable = null) throws Error {
yield expunge_email_async(email_ids, cancellable);
}
+
+ public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+ Cancellable? cancellable = null) throws Error {
+ // Gmail offers no direct path via IMAP to delete a message from the server ... must move
+ // the message to the Trash folder, then delete it there
+
+ // Get path to Trash folder
+ Geary.Folder? trash = account.get_special_folder(SpecialFolderType.TRASH);
+ if (trash == null)
+ throw new EngineError.NOT_FOUND("%s: Trash folder not found for removal", to_string());
+
+ // Copy to Trash, collect UIDs (note that copying to Trash is like a move; the copied
+ // messages are removed from all labels)
+ Gee.Set<Imap.UID>? uids = yield copy_email_uids_async(email_ids, trash.path, cancellable);
+ if (uids == null || uids.size == 0)
+ return;
+
+ debug("COPIED, %d UIDS", uids.size);
+
+ // For speed reasons, use a detched Imap.Folder object to delete moved emails; this is a
+ // separate connection and is not synchronized with the database, but also avoids a full
+ // folder normalization, which can be a heavyweight operation
+ Imap.Folder imap_trash = yield ((GenericAccount) account).fetch_detached_folder_async(trash.path,
+ cancellable);
+
+ debug("FETCHED DETACHED FOLDER");
+
+ yield imap_trash.open_async(cancellable);
+ try {
+ debug("REMOVING FROM TRASH");
+ yield imap_trash.remove_email_async(Imap.MessageSet.uid_sparse(uids), cancellable);
+ debug("REMOVED FROM TRASH");
+ } finally {
+ try {
+ yield imap_trash.close_async(null);
+ } catch (Error err) {
+ // ignored
+ }
+ }
+ }
}
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 50f6001..f60dbe7 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -526,6 +526,31 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable));
}
+ /**
+ * Returns an Imap.Folder that is not connected (is detached) to a MinimalFolder or any other
+ * ImapEngine container.
+ *
+ * This is useful for one-shot operations that need to bypass the heavyweight synchronziation
+ * routines inside MinimalFolder. This also means that operations performed on this Folder will
+ * not be reflected in the local database unless there's a separate connection to the server
+ * that is notified or detects these changes.
+ *
+ * It is not recommended this object be held open long-term, or that its status or notifications
+ * be directly written to the database unless you know exactly what you're doing. ''Caveat
+ * implementor.''
+ */
+ internal async Imap.Folder fetch_detached_folder_async(Geary.FolderPath path, Cancellable? cancellable)
+ throws Error {
+ check_open();
+
+ if (local_only.has_key(path)) {
+ throw new EngineError.NOT_FOUND("%s: path %s points to local-only folder, not IMAP",
+ to_string(), path.to_string());
+ }
+
+ return yield remote.fetch_unrecycled_folder_async(path, cancellable);
+ }
+
private Gee.HashMap<Geary.SpecialFolderType, Gee.ArrayList<string>> get_mailbox_search_names() {
Gee.HashMap<Geary.SpecialFolderType, string> mailbox_search_names
= new Gee.HashMap<Geary.SpecialFolderType, string>();
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 45e49f0..bc6a0ea 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -1236,19 +1236,30 @@ private class Geary.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.Folde
replay_queue.schedule(mark);
yield mark.wait_for_ready_async(cancellable);
}
-
+
public virtual async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
- check_open("copy_email_async");
- check_ids("copy_email_async", to_copy);
+ yield copy_email_uids_async(to_copy, destination, cancellable);
+ }
+
+ /**
+ * Returns the UIDs for the copies messages in the destination folder.
+ */
+ internal async Gee.Set<Imap.UID>? copy_email_uids_async(Gee.List<Geary.EmailIdentifier> to_copy,
+ Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
+ check_open("copy_email_uids_async");
+ check_ids("copy_email_uids_async", to_copy);
// watch for copying to this folder, which is treated as a no-op
if (destination.equal_to(path))
- return;
+ return null;
CopyEmail copy = new CopyEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_copy, destination);
replay_queue.schedule(copy);
+
yield copy.wait_for_ready_async(cancellable);
+
+ return copy.destination_uids;
}
public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
index 0d68888..f054780 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
@@ -5,6 +5,8 @@
*/
private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation {
+ public Gee.Set<Imap.UID> destination_uids = new Gee.HashSet<Imap.UID>();
+
private MinimalFolder engine;
private Gee.HashSet<ImapDB.EmailIdentifier> to_copy = new Gee.HashSet<ImapDB.EmailIdentifier>();
private Geary.FolderPath destination;
@@ -46,8 +48,12 @@ private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation
if (uids != null && uids.size > 0) {
Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(uids);
- foreach (Imap.MessageSet msg_set in msg_sets)
- yield engine.remote_folder.copy_email_async(msg_set, destination, cancellable);
+ foreach (Imap.MessageSet msg_set in msg_sets) {
+ Gee.Map<Imap.UID, Imap.UID>? src_dst_uids = yield engine.remote_folder.copy_email_async(
+ msg_set, destination, cancellable);
+ if (src_dst_uids != null)
+ destination_uids.add_all(src_dst_uids.values);
+ }
}
return ReplayOperation.Status.COMPLETED;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
index 3104c4c..3750ff0 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
@@ -54,7 +54,7 @@ private class Geary.ImapEngine.CreateEmail : Geary.ImapEngine.SendReplayOperatio
// operation atomic.
if (cancellable.is_cancelled()) {
yield engine.remote_folder.remove_email_async(
- new Imap.MessageSet.uid(((ImapDB.EmailIdentifier) created_id).uid), null);
+ new Imap.MessageSet.uid(((ImapDB.EmailIdentifier) created_id).uid).to_list(), null);
throw new IOError.CANCELLED("CreateEmail op cancelled after create");
}
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
index 236932b..4fc13b2 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
@@ -71,7 +71,7 @@ private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation
ImapDB.EmailIdentifier.to_uids(moved_ids));
foreach (Imap.MessageSet msg_set in msg_sets) {
yield engine.remote_folder.copy_email_async(msg_set, destination, null);
- yield engine.remote_folder.remove_email_async(msg_set, null);
+ yield engine.remote_folder.remove_email_async(msg_set.to_list(), null);
}
return ReplayOperation.Status.COMPLETED;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
index 234ab36..f1ee2d3 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
@@ -62,8 +62,7 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
// that the signal has already been fired.
Gee.List<Imap.MessageSet> msg_sets = Imap.MessageSet.uid_sparse(
ImapDB.EmailIdentifier.to_uids(removed_ids));
- foreach (Imap.MessageSet msg_set in msg_sets)
- yield engine.remote_folder.remove_email_async(msg_set, cancellable);
+ yield engine.remote_folder.remove_email_async(msg_sets, cancellable);
return ReplayOperation.Status.COMPLETED;
}
diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala
index 9da0272..b7de768 100644
--- a/src/engine/imap/api/imap-account.vala
+++ b/src/engine/imap/api/imap-account.vala
@@ -245,7 +245,7 @@ private class Geary.Imap.Account : BaseObject {
Imap.Folder folder;
if (!mailbox_info.attrs.is_no_select) {
- StatusData status = yield fetch_status_async(path, StatusDataType.all(), cancellable);
+ StatusData status = yield fetch_status_async(folder_path, StatusDataType.all(), cancellable);
folder = new Imap.Folder(folder_path, session_mgr, status, mailbox_info);
} else {
@@ -257,6 +257,35 @@ private class Geary.Imap.Account : BaseObject {
return folder;
}
+ /**
+ * Returns an Imap.Folder that is not stored long-term in the Imap.Account object.
+ *
+ * This means the Imap.Folder is not re-used or used by multiple users or containers. This is
+ * useful for one-shot operations on the server.
+ */
+ public async Imap.Folder fetch_unrecycled_folder_async(FolderPath path, Cancellable? cancellable)
+ throws Error {
+ check_open();
+
+ MailboxInformation? mailbox_info = path_to_mailbox.get(path);
+ if (mailbox_info == null)
+ throw_not_found(path);
+
+ // construct canonical folder path
+ FolderPath folder_path = mailbox_info.get_path(inbox_specifier);
+
+ Imap.Folder folder;
+ if (!mailbox_info.attrs.is_no_select) {
+ StatusData status = yield fetch_status_async(folder_path, StatusDataType.all(), cancellable);
+
+ folder = new Imap.Folder(folder_path, session_mgr, status, mailbox_info);
+ } else {
+ folder = new Imap.Folder.unselectable(folder_path, session_mgr, mailbox_info);
+ }
+
+ return folder;
+ }
+
internal void folders_removed(Gee.Collection<FolderPath> paths) {
foreach (FolderPath path in paths) {
if (path_to_mailbox.has_key(path))
diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala
index 82f316a..b4b6ab1 100644
--- a/src/engine/imap/api/imap-folder.vala
+++ b/src/engine/imap/api/imap-folder.vala
@@ -626,7 +626,8 @@ private class Geary.Imap.Folder : BaseObject {
return map;
}
- public async void remove_email_async(MessageSet msg_set, Cancellable? cancellable) throws Error {
+ public async void remove_email_async(Gee.List<MessageSet> msg_sets, Cancellable? cancellable)
+ throws Error {
check_open();
Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
@@ -634,8 +635,14 @@ private class Geary.Imap.Folder : BaseObject {
Gee.List<Command> cmds = new Gee.ArrayList<Command>();
- StoreCommand store_cmd = new StoreCommand(msg_set, flags, true, false);
- cmds.add(store_cmd);
+ // Build STORE command for all MessageSets, see if all are UIDs so we can use UID EXPUNGE
+ bool all_uid = true;
+ foreach (MessageSet msg_set in msg_sets) {
+ if (!msg_set.is_uid)
+ all_uid = false;
+
+ cmds.add(new StoreCommand(msg_set, flags, true, false));
+ }
// TODO: Only use old-school EXPUNGE when closing folder (or rely on CLOSE to do that work
// for us). See:
@@ -644,10 +651,12 @@ private class Geary.Imap.Folder : BaseObject {
// However, current client implementation doesn't properly close INBOX when application
// shuts down, which means deleted messages return at application start. See:
// http://redmine.yorba.org/issues/6865
- if (msg_set.is_uid && session.capabilities.supports_uidplus())
- cmds.add(new ExpungeCommand.uid(msg_set));
- else
+ if (all_uid && session.capabilities.supports_uidplus()) {
+ foreach (MessageSet msg_set in msg_sets)
+ cmds.add(new ExpungeCommand.uid(msg_set));
+ } else {
cmds.add(new ExpungeCommand());
+ }
yield exec_commands_async(cmds, null, null, cancellable);
}
@@ -675,15 +684,52 @@ private class Geary.Imap.Folder : BaseObject {
yield exec_commands_async(cmds, null, null, cancellable);
}
- public async void copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
+ // Returns a mapping of the source UID to the destination UID. If the MessageSet is not for
+ // UIDs, then null is returned. If the server doesn't support COPYUID, null is returned.
+ public async Gee.Map<UID, UID>? copy_email_async(MessageSet msg_set, FolderPath destination,
Cancellable? cancellable) throws Error {
check_open();
CopyCommand cmd = new CopyCommand(msg_set,
new MailboxSpecifier.from_folder_path(destination, null));
- yield exec_commands_async(Geary.iterate<Command>(cmd).to_array_list(), null,
- null, cancellable);
+ Gee.Map<Command, StatusResponse>? responses = yield exec_commands_async(
+ Geary.iterate<Command>(cmd).to_array_list(), null, null, cancellable);
+
+ if (!responses.has_key(cmd))
+ return null;
+
+ StatusResponse response = responses.get(cmd);
+ if (response.response_code != null && msg_set.is_uid) {
+ Gee.List<UID>? src_uids = null;
+ Gee.List<UID>? dst_uids = null;
+ try {
+ response.response_code.get_copyuid(null, out src_uids, out dst_uids);
+ } catch (ImapError ierr) {
+ debug("Unable to retrieve COPYUID UIDs: %s", ierr.message);
+ }
+
+ if (!Collection.is_empty(src_uids) && !Collection.is_empty(dst_uids)) {
+ Gee.Map<UID, UID> copyuids = new Gee.HashMap<UID, UID>();
+ int ctr = 0;
+ for (;;) {
+ UID? src_uid = (ctr < src_uids.size) ? src_uids[ctr] : null;
+ UID? dst_uid = (ctr < dst_uids.size) ? dst_uids[ctr] : null;
+
+ if (src_uid != null && dst_uid != null)
+ copyuids.set(src_uid, dst_uid);
+ else
+ break;
+
+ ctr++;
+ }
+
+ if (copyuids.size > 0)
+ return copyuids;
+ }
+ }
+
+ return null;
}
public async Gee.SortedSet<Imap.UID>? search_async(SearchCriteria criteria, Cancellable? cancellable)
diff --git a/src/engine/imap/command/imap-message-set.vala b/src/engine/imap/command/imap-message-set.vala
index 85af92a..e8399d4 100644
--- a/src/engine/imap/command/imap-message-set.vala
+++ b/src/engine/imap/command/imap-message-set.vala
@@ -20,6 +20,8 @@ public class Geary.Imap.MessageSet : BaseObject {
// etc.)
private const int MAX_SPARSE_VALUES_PER_SET = 50;
+ private delegate void ParserCallback(int64 value) throws ImapError;
+
/**
* True if the { link MessageSet} was created with a UID or a UID range.
*
@@ -109,6 +111,121 @@ public class Geary.Imap.MessageSet : BaseObject {
}
/**
+ * Parses a string representing a { link MessageSet} into a List of { link SequenceNumber}s.
+ *
+ * See the note at { link parse_uid} about limitations of this method.
+ *
+ * Returns null if the string or parsed set is empty.
+ *
+ * @see uid_parse
+ */
+ public static Gee.List<SequenceNumber>? parse(string str) throws ImapError {
+ Gee.List<SequenceNumber> seq_nums = new Gee.ArrayList<SequenceNumber>();
+ parse_string(str, (value) => { seq_nums.add(new SequenceNumber.checked(value)); });
+
+ return seq_nums.size > 0 ? seq_nums : null;
+ }
+
+ /**
+ * Parses a string representing a { link MessageSet} into a List of { link UID}s.
+ *
+ * Note that this is currently designed for parsing message set responses from the server,
+ * specifically for COPYUID, which has some limitations in what may be returned. Notably, the
+ * asterisk ("*") symbol may not be returned. Thus, this method does not properly parse
+ * the full range of message set notation and can't even be trusted to reverse-parse the output
+ * of this class. A full implementation might be considered later.
+ *
+ * Because COPYUID returns values in the order copied, this method returns a List, not a Set,
+ * of values. They are in the order received (and properly deal with ranges in backwards
+ * order, i.e. "12:10"). This means duplicates may be encountered multiple times if the server
+ * returns those values.
+ *
+ * Returns null if the string or parsed set is empty.
+ */
+ public static Gee.List<UID>? uid_parse(string str) throws ImapError {
+ Gee.List<UID> uids = new Gee.ArrayList<UID>();
+ parse_string(str, (value) => { uids.add(new UID.checked(value)); });
+
+ return uids.size > 0 ? uids : null;
+ }
+
+ private static void parse_string(string str, ParserCallback cb) throws ImapError {
+ StringBuilder acc = new StringBuilder();
+ int64 start_range = -1;
+ bool in_range = false;
+
+ unichar ch;
+ int index = 0;
+ while (str.get_next_char(ref index, out ch)) {
+ // if number, add to accumulator
+ if (ch.isdigit()) {
+ acc.append_unichar(ch);
+
+ continue;
+ }
+
+ // look for special characters and deal with them
+ switch (ch) {
+ case ':':
+ // range separator
+ if (in_range)
+ throw new ImapError.INVALID("Bad range specifier in message set \"%s\"", str);
+
+ in_range = true;
+
+ // store current accumulated value as start of range
+ start_range = int64.parse(acc.str);
+ acc = new StringBuilder();
+ break;
+
+ case ',':
+ // number separator
+
+ // if in range, treat as end-of-range
+ if (in_range) {
+ // don't be forgiving here
+ if (String.is_empty(acc.str))
+ throw new ImapError.INVALID("Bad range specifier in message set \"%s\"", str);
+
+ process_range(start_range, int64.parse(acc.str), cb);
+ in_range = false;
+ } else {
+ // Be forgiving here
+ if (String.is_empty(acc.str))
+ continue;
+
+ cb(int64.parse(acc.str));
+ }
+
+ // reset accumulator
+ acc = new StringBuilder();
+ break;
+
+ default:
+ // unknown character, treat with great violence
+ throw new ImapError.INVALID("Bad character '%s' in message set \"%s\"",
+ ch.to_string(), str);
+ }
+ }
+
+ // report last bit remaining in accumulator
+ if (!String.is_empty(acc.str)) {
+ if (in_range)
+ process_range(start_range, int64.parse(acc.str), cb);
+ else
+ cb(int64.parse(acc.str));
+ } else if (in_range) {
+ throw new ImapError.INVALID("Incomplete range specifier in message set \"%s\"", str);
+ }
+ }
+
+ private static void process_range(int64 start, int64 end, ParserCallback cb) throws ImapError {
+ int64 count_by = (start <= end) ? 1 : -1;
+ for (int64 ctr = start; ctr != end + count_by; ctr += count_by)
+ cb(ctr);
+ }
+
+ /**
* Convert a collection of { link SequenceNumber}s into a list of { link MessageSet}s.
*
* Although this could return a single MessageSet, large collections could create an IMAP
@@ -247,6 +364,13 @@ public class Geary.Imap.MessageSet : BaseObject {
return new UnquotedStringParameter(value);
}
+ /**
+ * Returns the { link MessageSet} in a Gee.List.
+ */
+ public Gee.List<MessageSet> to_list() {
+ return iterate<MessageSet>(this).to_array_list();
+ }
+
public string to_string() {
return "%s::%s".printf(is_uid ? "UID" : "pos", value);
}
diff --git a/src/engine/imap/parameter/imap-list-parameter.vala
b/src/engine/imap/parameter/imap-list-parameter.vala
index e561102..4e57d95 100644
--- a/src/engine/imap/parameter/imap-list-parameter.vala
+++ b/src/engine/imap/parameter/imap-list-parameter.vala
@@ -325,6 +325,44 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
}
//
+ // Number retrieval
+ //
+
+ /**
+ * Returns a { link NumberParameter} at index, null if not of that type.
+ *
+ * @see get_if
+ */
+ public NumberParameter? get_if_number(int index) {
+ return (NumberParameter?) get_if(index, typeof(NumberParameter));
+ }
+
+ /**
+ * Returns a { link NumberParameter} at index.
+ *
+ * Like { link get_as_string}, this method will attempt some coercion. In this case,
+ * { link QuotedStringParameter} and { link UnquotedStringParameter}s will be converted to
+ * NumberParameter, if appropriate.
+ */
+ public NumberParameter get_as_number(int index) throws ImapError {
+ Parameter param = get_required(index);
+
+ NumberParameter? numberp = param as NumberParameter;
+ if (numberp != null)
+ return numberp;
+
+ StringParameter? stringp = param as StringParameter;
+ if (stringp != null) {
+ numberp = stringp.coerce_to_number_parameter();
+ if (numberp != null)
+ return numberp;
+ }
+
+ throw new ImapError.TYPE_ERROR("Parameter %d not of type number or string (is %s)", index,
+ param.get_type().name());
+ }
+
+ //
// List retrieval
//
diff --git a/src/engine/imap/parameter/imap-number-parameter.vala
b/src/engine/imap/parameter/imap-number-parameter.vala
index e32c3cc..260db0d 100644
--- a/src/engine/imap/parameter/imap-number-parameter.vala
+++ b/src/engine/imap/parameter/imap-number-parameter.vala
@@ -5,7 +5,8 @@
*/
/**
- * A representation of a numerical { link Parameter} in an IMAP { link Command}.
+ * A representation of a numerical { link Parameter} in an IMAP { link Command} or
+ * { link ServerResponse}.
*
* See [[http://tools.ietf.org/html/rfc3501#section-4.2]]
*/
@@ -86,6 +87,11 @@ public class Geary.Imap.NumberParameter : UnquotedStringParameter {
has_nonzero = true;
}
+ // watch for negative but no numeric portion
+ if (is_negative && str.length == 1)
+ return false;
+
+ // no such thing as negative zero
if (is_negative && !has_nonzero)
is_negative = false;
diff --git a/src/engine/imap/parameter/imap-string-parameter.vala
b/src/engine/imap/parameter/imap-string-parameter.vala
index 01b6f0b..a1978cf 100644
--- a/src/engine/imap/parameter/imap-string-parameter.vala
+++ b/src/engine/imap/parameter/imap-string-parameter.vala
@@ -188,5 +188,23 @@ public abstract class Geary.Imap.StringParameter : Geary.Imap.Parameter {
return int64.parse(ascii).clamp(clamp_min, clamp_max);
}
+
+ /**
+ * Attempts to coerce a { link StringParameter} into a { link NumberParameter}.
+ *
+ * Returns null if unsuitable for a NumberParameter.
+ *
+ * @see NumberParameter.is_ascii_number
+ */
+ public NumberParameter? coerce_to_number_parameter() {
+ NumberParameter? numberp = this as NumberParameter;
+ if (numberp != null)
+ return numberp;
+
+ if (NumberParameter.is_ascii_numeric(ascii, null))
+ return new NumberParameter.from_ascii(ascii);
+
+ return null;
+ }
}
diff --git a/src/engine/imap/response/imap-response-code-type.vala
b/src/engine/imap/response/imap-response-code-type.vala
index 3bb0c84..c5c52be 100644
--- a/src/engine/imap/response/imap-response-code-type.vala
+++ b/src/engine/imap/response/imap-response-code-type.vala
@@ -23,6 +23,7 @@ public class Geary.Imap.ResponseCodeType : BaseObject, Gee.Hashable<ResponseCode
public const string BADCHARSET = "badcharset";
public const string CAPABILITY = "capability";
public const string CLIENTBUG = "clientbug";
+ public const string COPYUID = "copyuid";
public const string MYRIGHTS = "myrights";
public const string NEWNAME = "newname";
public const string NONEXISTANT = "nonexistant";
diff --git a/src/engine/imap/response/imap-response-code.vala
b/src/engine/imap/response/imap-response-code.vala
index fbd8ce8..e8887e2 100644
--- a/src/engine/imap/response/imap-response-code.vala
+++ b/src/engine/imap/response/imap-response-code.vala
@@ -89,6 +89,26 @@ public class Geary.Imap.ResponseCode : Geary.Imap.ListParameter {
return capabilities;
}
+ /**
+ * Parses the { link ResponseCode} into UIDPLUS' COPYUID response, if possible.
+ *
+ * Note that the { link UID}s are returned from the server in the order the messages
+ * were copied.
+ *
+ * See [[http://tools.ietf.org/html/rfc4315#section-3]]
+ *
+ * @throws ImapError.INVALID if not COPYUID.
+ */
+ public void get_copyuid(out UIDValidity uidvalidity, out Gee.List<UID>? source_uids,
+ out Gee.List<UID>? destination_uids) throws ImapError {
+ if (!get_response_code_type().is_value(ResponseCodeType.COPYUID))
+ throw new ImapError.INVALID("Not COPYUID response code: %s", to_string());
+
+ uidvalidity = new UIDValidity.checked(get_as_number(1).as_int64());
+ source_uids = MessageSet.uid_parse(get_as_string(2).ascii);
+ destination_uids = MessageSet.uid_parse(get_as_string(3).ascii);
+ }
+
public override string to_string() {
return "[%s]".printf(stringize_list());
}
diff --git a/src/engine/imap/transport/imap-deserializer.vala
b/src/engine/imap/transport/imap-deserializer.vala
index 63cfd2f..3f6be64 100644
--- a/src/engine/imap/transport/imap-deserializer.vala
+++ b/src/engine/imap/transport/imap-deserializer.vala
@@ -449,6 +449,8 @@ public class Geary.Imap.Deserializer : BaseObject {
if (quoted)
save_parameter(new QuotedStringParameter(str));
+ else if (NumberParameter.is_ascii_numeric(str, null))
+ save_parameter(new NumberParameter.from_ascii(str));
else
save_parameter(new UnquotedStringParameter(str));
diff --git a/src/engine/util/util-collection.vala b/src/engine/util/util-collection.vala
index 294de11..dc9f43d 100644
--- a/src/engine/util/util-collection.vala
+++ b/src/engine/util/util-collection.vala
@@ -8,6 +8,10 @@ namespace Geary.Collection {
public delegate uint8 ByteTransformer(uint8 b);
+public inline bool is_empty(Gee.Collection? c) {
+ return c == null || c.size == 0;
+}
+
// A substitute for ArrayList<G>.wrap() for compatibility with older versions of Gee.
public Gee.ArrayList<G> array_list_wrap<G>(G[] a, owned Gee.EqualDataFunc<G>? equal_func = null) {
Gee.ArrayList<G> list = new Gee.ArrayList<G>(equal_func);
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]