Moving away from positional addressing. Greater emphasis on EmailIdentifiers for

addressing.

Positional addressing is a nightmare for a lot of reasons, especially keeping positions
up-to-date as the Folder mutates.  Now, positions are returned with the email but for
advisory reasons only, and do not keep up-to-date.  It is expected that a client will use
positional addressing to "bootstrap" the application's data store, then use EmailIdentifier
addressing to traverse the Folder's contents.
This commit is contained in:
Jim Nelson 2011-11-09 16:40:28 -08:00
parent 0c4d6ed8e9
commit 3f6d8eac5a
22 changed files with 602 additions and 372 deletions

View file

@ -200,7 +200,7 @@ public class MainWindow : Gtk.Window {
current_conversations.lazy_load(-1, -1, Geary.Folder.ListFlags.FAST, cancellable_folder);
}
public void on_scan_started(int low, int count) {
public void on_scan_started() {
debug("on scan started");
}

View file

@ -6,9 +6,7 @@
public int compare_email(Geary.Email aenvelope, Geary.Email benvelope) {
int diff = aenvelope.date.value.compare(benvelope.date.value);
if (diff != 0)
return diff;
// stabilize sort by using the mail's position, which is always unique in a folder
return aenvelope.location.position - benvelope.location.position;
// stabilize sort by using the mail's ordering, which is always unique in a folder
return (diff != 0) ? diff : aenvelope.id.compare(benvelope.id);
}

View file

@ -190,7 +190,13 @@ public class Geary.Conversations : Object {
private bool monitor_new = false;
private Cancellable? cancellable_monitor = null;
public virtual signal void scan_started(int low, int count) {
/**
* "scan-started" is fired whenever beginning to load messages into the Conversations object.
* If id is not null, then the scan is starting at an identifier and progressing according to
* count (see Geary.Folder.list_email_by_id_async()). Otherwise, the scan is using positional
* addressing and low is a valid one-based position (see Geary.Folder.list_email_async()).
*/
public virtual signal void scan_started(Geary.EmailIdentifier? id, int low, int count) {
}
public virtual signal void scan_error(Error err) {
@ -224,8 +230,8 @@ public class Geary.Conversations : Object {
folder.messages_appended.disconnect(on_folder_messages_appended);
}
protected virtual void notify_scan_started(int low, int count) {
scan_started(low, count);
protected virtual void notify_scan_started(Geary.EmailIdentifier? id, int low, int count) {
scan_started(id, low, count);
}
protected virtual void notify_scan_error(Error err) {
@ -254,11 +260,28 @@ public class Geary.Conversations : Object {
return conversations.read_only_view;
}
public bool monitor_new_messages(Cancellable? cancellable = null) {
if (monitor_new)
return false;
monitor_new = true;
cancellable_monitor = cancellable;
folder.messages_appended.connect(on_folder_messages_appended);
return true;
}
/**
* See Geary.Folder.list_email_async() for details of how these parameters operate. Instead
* of returning emails, this method will load the Conversations object with them sorted into
* Conversation objects.
*/
public async void load_async(int low, int count, Geary.Folder.ListFlags flags,
Cancellable? cancellable) throws Error {
notify_scan_started(low, count);
notify_scan_started(null, low, count);
try {
Gee.List<Email>? list = yield folder.list_email_async(low, count, required_fields, flags);
Gee.List<Email>? list = yield folder.list_email_async(low, count, required_fields, flags,
cancellable);
on_email_listed(list, null);
if (list != null)
on_email_listed(null, null);
@ -267,20 +290,45 @@ public class Geary.Conversations : Object {
}
}
public void lazy_load(int low, int count, Geary.Folder.ListFlags flags, Cancellable? cancellable)
throws Error {
notify_scan_started(low, count);
/**
* See Geary.Folder.lazy_list_email_async() for details of how these parameters operate. Instead
* of returning emails, this method will load the Conversations object with them sorted into
* Conversation objects.
*/
public void lazy_load(int low, int count, Geary.Folder.ListFlags flags, Cancellable? cancellable) {
notify_scan_started(null, low, count);
folder.lazy_list_email(low, count, required_fields, flags, on_email_listed, cancellable);
}
public bool monitor_new_messages(Cancellable? cancellable = null) {
if (monitor_new)
return false;
monitor_new = true;
cancellable_monitor = cancellable;
folder.messages_appended.connect(on_folder_messages_appended);
return true;
/**
* See Geary.Folder.list_email_by_id_async() for details of how these parameters operate. Instead
* of returning emails, this method will load the Conversations object with them sorted into
* Conversation objects.
*/
public async void load_by_id_async(Geary.EmailIdentifier initial_id, int count,
Geary.Folder.ListFlags flags, Cancellable? cancellable) throws Error {
notify_scan_started(initial_id, -1, count);
try {
Gee.List<Email>? list = yield folder.list_email_by_id_async(initial_id, count,
required_fields, flags, cancellable);
on_email_listed(list, null);
if (list != null)
on_email_listed(null, null);
} catch (Error err) {
on_email_listed(null, err);
}
}
/**
* See Geary.Folder.lazy_list_email_by_id() for details of how these parameters operate. Instead
* of returning emails, this method will load the Conversations object with them sorted into
* Conversation objects.
*/
public void lazy_load_by_id(Geary.EmailIdentifier initial_id, int count, Geary.Folder.ListFlags flags,
Cancellable? cancellable) {
notify_scan_started(initial_id, -1, count);
folder.lazy_list_email_by_id(initial_id, count, required_fields, flags, on_email_listed,
cancellable);
}
private void on_email_listed(Gee.List<Geary.Email>? emails, Error? err) {
@ -518,29 +566,27 @@ public class Geary.Conversations : Object {
}
private void on_folder_messages_appended() {
// Find highest position.
// Find highest identifier by ordering
// TODO: optimize.
int high = -1;
foreach (Conversation c in conversations)
foreach (Email e in c.get_pool())
if (e.location.position > high)
high = e.location.position;
Geary.EmailIdentifier? highest = null;
foreach (Conversation c in conversations) {
foreach (Email e in c.get_pool()) {
if (highest == null || (e.id.compare(highest) > 0))
highest = e.id;
}
}
if (high < 0) {
if (highest == null) {
debug("Unable to find highest message position in %s", folder.to_string());
return;
}
debug("Message(s) appended to %s, fetching email at %d and above", folder.to_string(),
high + 1);
debug("Message(s) appended to %s, fetching email at %s and above", folder.to_string(),
highest.to_string());
// Want to get the one *after* the highest position in the list
try {
lazy_load(high + 1, -1, Folder.ListFlags.NONE, cancellable_monitor);
} catch (Error e) {
warning("Error getting new mail: %s", e.message);
}
lazy_load_by_id(highest.next(), int.MAX, Folder.ListFlags.NONE, cancellable_monitor);
}
}

View file

@ -10,11 +10,41 @@
* message is located in; an EmailIdentifier cannot be used in another Folder to determine if the
* message is duplicated there. (Either EmailIdentifier will be expanded to allow for this or
* another system will be offered.)
*
* EmailIdentifier is Comparable because it can be used to compare against other EmailIdentifiers
* (in the same Folder) for sort order that corresponds to their position in the Folder. It does
* this through an ordering field that provides an integer that can be compared to other ordering
* fields in the same Folder that match the email's position within it. The ordering field may
* or may not be the actual unique identifier; in IMAP, for example, it is, while in other systems
* it may not be.
*/
public abstract class Geary.EmailIdentifier : Object, Geary.Equalable {
public abstract class Geary.EmailIdentifier : Object, Geary.Equalable, Geary.Comparable {
public abstract int64 ordering { get; protected set; }
public abstract EmailIdentifier next();
public abstract EmailIdentifier previous();
public abstract bool equals(Geary.Equalable other);
public virtual int compare(Geary.Comparable o) {
Geary.EmailIdentifier? other = o as Geary.EmailIdentifier;
if (other == null)
return -1;
if (this == other)
return 0;
int64 diff = ordering - other.ordering;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else
return 0;
}
public abstract string to_string();
}

View file

@ -1,135 +0,0 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* An EmailLocation represents an Email's position (cardinal ordering) in a Folder. Unlike other
* elements in an Email, it may change over time, depending on events within the Folder itself.
* It reports these changes via signals.
*
* An EmailLocation's position is a cardinal (1 to n) of an Email's ordering within the Folder.
* Its ordering is an opaque number that was used to determine this ordering -- it may be exactly
* equal to its position, or it may not. This value is particular to the message provider and
* should not be depended on.
*
* When the Folder closes, the EmailLocation will fire its "invalidated" signal and go into an
* invalidated state. Its position will be -1. The EmailLocation will never go back to a valid
* state; the owner should discard the EmailLocation (and, by extension, its Email) after the Folder
* has closed and fetch fresh ones once its re-opened.
*
* Note the differences between an EmailLocation, which gives a cardinal way of addressing mail in
* a folder, an an EmailIdentifier, which is unique and immutable, but is only addressable as a
* singleton, and not by a range.
*/
public class Geary.EmailLocation : Object {
/**
* Returns -1 if invalidated.
*/
public int position { get; private set; }
public int64 ordering { get; private set; }
private Geary.Folder folder;
private int local_adjustment;
public signal void position_altered(int old_position, int new_position);
public signal void position_deleted(int position);
/**
* "invalidated" is fired when the EmailLocation object is no longer valid due to the Folder
* closing. At this point, its position will be -1.
*/
public signal void invalidated();
public EmailLocation(Geary.Folder folder, int position, int64 ordering) {
init(folder, position, ordering, -1);
}
public EmailLocation.local(Geary.Folder folder, int position, int64 ordering, int local_adjustment) {
init(folder, position, ordering, local_adjustment);
}
~EmailLocation() {
invalidate(false);
}
private void init(Geary.Folder folder, int position, int64 ordering, int local_adjustment) {
assert(position >= 1);
this.folder = folder;
this.position = position;
this.ordering = ordering;
this.local_adjustment = local_adjustment;
folder.message_removed.connect(on_message_removed);
folder.opened.connect(on_folder_opened);
folder.closed.connect(on_folder_closed);
}
private void invalidate(bool signalled) {
if (position < 0)
return;
position = -1;
folder.message_removed.disconnect(on_message_removed);
folder.opened.disconnect(on_folder_opened);
folder.closed.disconnect(on_folder_closed);
if (signalled)
invalidated();
}
private void on_message_removed(int position, int total) {
// bail out if invalidated
if (position < 1)
return;
// if the removed position is greater than this one's, no change in this position
if (this.position < position)
return;
// if the same, can't adjust (adjust it to what?), but notify that this EmailLocation has
// been removed
if (this.position == position) {
position_deleted(position);
return;
}
// adjust this position downward
int old_position = this.position;
this.position--;
assert(this.position >= 1);
position_altered(old_position, this.position);
}
private void on_folder_closed() {
invalidate(true);
}
private void on_folder_opened(Geary.Folder.OpenState state, int count) {
// If no local_adjustment, nothing needs to be done; if the local_adjustment is greater or
// equal to the remote folder's count, that indicates messages have been removed, in which
// case let the adjustments occur via on_message_removed().
if (local_adjustment < 0 || count <= local_adjustment) {
local_adjustment = -1;
return;
}
int old_position = position;
position = count - local_adjustment;
assert(position >= 1);
// mark as completed, to prevent this from happening again
local_adjustment = -1;
if (position != old_position)
position_altered(old_position, position);
}
}

View file

@ -57,7 +57,25 @@ public class Geary.Email : Object {
}
}
public Geary.EmailLocation location { get; private set; }
/**
* position is the one-based addressing of the email in the folder, in the notion that messages
* are "stacked" from 1 to n, earliest to newest. "Earliest" and "newest" do not necessarily
* correspond to the emails' send or receive time, merely how they've been arranged on the stack.
*
* This value is only good at the time the Email is requested from the folder. Subsequent
* operations may change the Email's position in the folder (or simply remove it). This value
* is *not* updated to reflect this.
*
* This field is always returned, no matter what Fields are used to retrieve the Email.
*/
public int position { get; private set; }
/**
* id is a unique identifier for the Email in the Folder. It is guaranteed to be unique for
* as long as the Folder is open. Once closed, guarantees are no longer made.
*
* This field is always returned, no matter what Fields are used to retrieve the Email.
*/
public Geary.EmailIdentifier id { get; private set; }
// DATE
@ -94,13 +112,17 @@ public class Geary.Email : Object {
private Geary.RFC822.Message? message = null;
public Email(Geary.EmailLocation location, Geary.EmailIdentifier id) {
this.location = location;
public Email(int position, Geary.EmailIdentifier id) {
assert(position >= 1);
this.position = position;
this.id = id;
}
public void update_location(Geary.EmailLocation location) {
this.location = location;
internal void update_position(int position) {
assert(position >= 1);
this.position = position;
}
public void set_send_date(Geary.RFC822.Date date) {
@ -185,10 +207,10 @@ public class Geary.Email : Object {
public string to_string() {
StringBuilder builder = new StringBuilder();
builder.append_printf("[#%d/%s] ", location.position, id.to_string());
builder.append_printf("[#%d/%s] ", position, id.to_string());
if (date != null)
builder.append_printf("[%s]", date.to_string());
builder.append_printf("%s/", date.to_string());
return builder.str;
}

View file

@ -19,9 +19,9 @@ public interface Geary.Folder : Object {
FOLDER_CLOSED
}
public enum Direction {
BEFORE,
AFTER
public enum CountChangeReason {
ADDED,
REMOVED
}
[Flags]
@ -67,20 +67,21 @@ public interface Geary.Folder : Object {
* "message-removed" is fired when a message has been removed (deleted or moved) from the
* folder (and therefore old message position numbers may no longer be valid, i.e. those after
* the removed message).
*
* NOTE: It's possible for the remote server to report a message has been removed that is not
* known locally (and therefore the caller could not have record of). If this happens, this
* signal will *not* fire, although "email-count-changed" will.
*/
public signal void message_removed(int position, int total);
public signal void message_removed(Geary.EmailIdentifier id);
/**
* "positions-reordered" is fired when message positions on emails in the folder may no longer
* be valid, which may happen even if a message has not been removed. In other words, if a
* message is removed and it causes positions to change, "message-remove" will be fired followed
* by this signal.
* "email-count-changed" is fired when the total count of email in a folder has changed in any way.
*
* Although reordering may be rare (positions shifting is a better description), it is possible
* for messages in a folder to change positions completely. This signal covers both
* circumstances.
* Note that this signal will be fired alongside "messages-appended" or "message-removed".
* That is, do not use both signals to process email count changes; one will suffice.
* This signal will fire after those (although see the note at "message-removed").
*/
public signal void positions_reordered();
public signal void email_count_changed(int new_count, CountChangeReason reason);
/**
* This helper method should be called by implementors of Folder rather than firing the signal
@ -108,14 +109,14 @@ public interface Geary.Folder : Object {
* directly. This allows subclasses and superclasses the opportunity to inspect the email
* and update state before and/or after the signal has been fired.
*/
protected abstract void notify_positions_reordered();
protected abstract void notify_message_removed(Geary.EmailIdentifier id);
/**
* This helper method should be called by implementors of Folder rather than firing the signal
* directly. This allows subclasses and superclasses the opportunity to inspect the email
* and update state before and/or after the signal has been fired.
*/
protected abstract void notify_message_removed(int position, int total);
protected abstract void notify_email_count_changed(int new_count, CountChangeReason reason);
public abstract Geary.FolderPath get_path();
@ -267,7 +268,7 @@ public interface Geary.Folder : Object {
* messages are passed back to the caller in chunks as they're retrieved. When null is passed
* as the first parameter, all the messages have been fetched. If an Error occurs during
* processing, it's passed as the second parameter. There's no guarantee of the returned
* message's order.
* messages' order.
*
* The Folder must be opened prior to attempting this operation.
*/
@ -275,27 +276,73 @@ public interface Geary.Folder : Object {
Geary.Email.Field required_fields, ListFlags flags, EmailCallback cb,
Cancellable? cancellable = null);
/**
* Similar in contract to list_email_async(), but uses Geary.EmailIdentifier rather than
* positional addressing. This allows for a batch of messages to be listed from a starting
* identifier, going up and down the stack depending on the count parameter.
*
* The count parameter is exclusive of the Email at initial_id. That is, if count is one,
* two Emails may be returned: the one for initial_id and the next one. If count is zero,
* only the Email with the specified initial_id will be listed, making this method operate
* like fetch_email_async().
*
* There is no guarantee that a message with the initial_id will be returned however.
* (It is up to the implementation to deal with spans starting from a non-existant or
* unavailable EmailIdentifier.) To fetch email exclusive of the initial_id, use
* EmailIdentifier.next() or EmailIdentifier.previous().
*
* If count is positive, initial_id is the *lowest* identifier and the returned list is going
* up the stack (toward the most recently added). If the count is negative, initial_id is
* the *highest* identifier and the returned list is going down the stack (toward the earliest
* added).
*
* To fetch all available messages in one direction or another, use int.MIN or int.MAX.
*
* There's no guarantee of the returned messages' order.
*
* There is (currently) no sparse version of list_email_by_id_async().
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier initial_id,
int count, Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable = null)
throws Error;
/**
* Similar in contract to lazy_list_email_async(), but uses Geary.EmailIdentifier rather than
* positional addressing, much like list_email_by_id_async(). See that method for more
* information on its contract and how the count parameter works.
*
* Like the other "lazy" methods, this method will call EmailCallback while the operation is
* processing. This method does not block.
*
* There is (currently) no sparse version of lazy_list_email_by_id().
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, ListFlags flags, EmailCallback cb,
Cancellable? cancellable = null);
/**
* Returns a single email that fulfills the required_fields flag at the ordered position in
* the folder. If position is invalid for the folder's contents, an EngineError.NOT_FOUND
* the folder. If the email_id is invalid for the folder's contents, an EngineError.NOT_FOUND
* error is thrown. If the requested fields are not available, EngineError.INCOMPLETE_MESSAGE
* is thrown.
*
* The Folder must be opened prior to attempting this operation.
*
* position is one-based.
*/
public abstract async Geary.Email fetch_email_async(Geary.EmailIdentifier email_id,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
/**
* Removes the email at the supplied position from the folder. If the email position is
* invalid for any reason, EngineError.NOT_FOUND is thrown.
* Removes the email at the supplied position from the folder. If the email_id is invalid for
* any reason, EngineError.NOT_FOUND is thrown.
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async void remove_email_async(int position, Cancellable? cancellable = null)
throws Error;
public abstract async void remove_email_async(Geary.EmailIdentifier email_id,
Cancellable? cancellable = null) throws Error;
/**
* check_span_specifiers() verifies that the span specifiers match the requirements set by

View file

@ -5,10 +5,21 @@
*/
private class Geary.Imap.EmailIdentifier : Geary.EmailIdentifier {
public override int64 ordering { get; protected set; }
public Imap.UID uid { get; private set; }
public override Geary.EmailIdentifier next() {
return new Geary.Imap.EmailIdentifier(new Imap.UID((uid.value + 1).clamp(1, uint32.MAX)));
}
public override Geary.EmailIdentifier previous() {
return new Geary.Imap.EmailIdentifier(new Imap.UID((uid.value - 1).clamp(1, uint32.MAX)));
}
public EmailIdentifier(Imap.UID uid) {
this.uid = uid;
ordering = uid.value;
}
public override bool equals(Equalable o) {

View file

@ -1,16 +0,0 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/*
* The IMAP implementation of Geary.EmailLocation uses the email's UID to order the messages.
*/
private class Geary.Imap.EmailLocation : Geary.EmailLocation {
public EmailLocation(Geary.Folder folder, int position, Geary.Imap.UID uid) {
base (folder, position, uid.value);
}
}

View file

@ -1,24 +0,0 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private interface Geary.Imap.FolderExtensions : Geary.Folder {
/**
* Much like Geary.Folder.list_email_async(), but this list operation allows for a range of
* emails to be specified by their UID rather than position (message number). If low is null
* that indicates to search from the lowest UID (1) to high. Likewise, if high is null it
* indicates to search from low to the highest UID. Setting both to null will return all
* emails in the folder.
*
* Unlike list_email_async(), this call guarantees that the messages will be returned in UID
* order, from lowest to highest.
*
* The folder must be open before making this call.
*/
public abstract async Gee.List<Geary.Email>? list_email_uid_async(Geary.Imap.UID? low,
Geary.Imap.UID? high, Geary.Email.Field fields, Cancellable? cancellable = null)
throws Error;
}

View file

@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Geary.Imap.FolderExtensions {
private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
public const bool CASE_SENSITIVE = true;
private ClientSessionManager session_mgr;
@ -27,6 +27,10 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear
: new Imap.FolderProperties(0, 0, 0, null, null, info.attrs);
}
protected void notify_message_at_removed(int position, int total) {
message_at_removed(position, total);
}
public override Geary.FolderPath get_path() {
return path;
}
@ -92,7 +96,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear
private void on_expunged(MessageNumber expunged, int total) {
assert(mailbox != null);
notify_message_removed(expunged.value, total);
notify_message_at_removed(expunged.value, total);
}
public override async int get_email_count_async(Cancellable? cancellable = null) throws Error {
@ -128,14 +132,27 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear
return yield mailbox.list_set_async(this, new MessageSet.sparse(by_position), fields, cancellable);
}
public async Gee.List<Geary.Email>? list_email_uid_async(Geary.Imap.UID? low,
Geary.Imap.UID? high, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error {
public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier email_id,
int count, Geary.Email.Field fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null)
throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
MessageSet msg_set = (high != null)
? new MessageSet.uid_range((low != null) ? low : new Geary.Imap.UID(1), high)
: new MessageSet.uid_range_to_highest(low);
UID uid = ((Imap.EmailIdentifier) email_id).uid;
MessageSet msg_set;
if (count > 0) {
msg_set = (count == int.MAX)
? new MessageSet.uid_range_to_highest(uid)
: new MessageSet.uid_range_by_count(uid, count);
} else if (count < 0) {
msg_set = (count != int.MIN)
? new MessageSet.uid_range(new UID(1), uid)
: new MessageSet.uid_range_by_count(uid, count);
} else {
// count == 0
msg_set = new MessageSet.uid(uid);
}
return yield mailbox.list_set_async(this, msg_set, fields, cancellable);
}
@ -150,7 +167,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder, Gear
return yield mailbox.fetch_async(this, ((Imap.EmailIdentifier) id).uid, fields, cancellable);
}
public override async void remove_email_async(int position, Cancellable? cancellable = null)
public override async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null)
throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());

View file

@ -45,6 +45,37 @@ public class Geary.Imap.MessageSet {
value = "%d:*".printf(low_msg_num);
}
/**
* A positive count yields a range going from initial up the stack (toward the most recently
* added message). A negative count yields a range going from initial down the stack (toward
* the earliest added message). A count of zero yields a message range for one UID, initial.
*
* Underflows and overflows are accounted for by clamping the arithmetic result to the possible
* range of UID's.
*/
public MessageSet.uid_range_by_count(UID initial, int count) {
assert(initial.value > 0);
if (count == 0) {
MessageSet.uid(initial);
return;
}
int64 low, high;
if (count < 0) {
high = initial.value;
low = (high + count).clamp(1, uint32.MAX);
} else {
// count > 0
low = initial.value;
high = (low + count).clamp(1, uint32.MAX);
}
value = "%lld:%lld".printf(low, high);
is_uid = true;
}
public MessageSet.uid_range_to_highest(UID low) {
assert(low.value > 0);

View file

@ -78,9 +78,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
// see fields_to_fetch_data_types() for why this is guaranteed
assert(uid != null);
Geary.Email email = new Geary.Email(
new Geary.Imap.EmailLocation(folder, res.msg_num, uid),
new Geary.Imap.EmailIdentifier(uid));
Geary.Email email = new Geary.Email(res.msg_num, new Geary.Imap.EmailIdentifier(uid));
fetch_results_to_email(res, fields, email);
msgs.add(email);
@ -111,9 +109,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
if (results.length != 1)
throw new ImapError.SERVER_ERROR("Too many responses from server: %d", results.length);
Geary.Email email = new Geary.Email(
new Geary.Imap.EmailLocation(folder, results[0].msg_num, uid),
new Geary.Imap.EmailIdentifier(uid));
Geary.Email email = new Geary.Email(results[0].msg_num, new Geary.Imap.EmailIdentifier(uid));
fetch_results_to_email(results[0], fields, email);
return email;

View file

@ -17,12 +17,12 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
messages_appended(total);
}
protected virtual void notify_message_removed(int position, int total) {
message_removed(position, total);
protected virtual void notify_message_removed(Geary.EmailIdentifier id) {
message_removed(id);
}
protected virtual void notify_positions_reordered() {
positions_reordered();
protected virtual void notify_email_count_changed(int new_count, Folder.CountChangeReason reason) {
email_count_changed(new_count, reason);
}
public abstract Geary.FolderPath get_path();
@ -90,10 +90,36 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
}
}
public abstract async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier initial_id,
int count, Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null)
throws Error;
public virtual void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
Cancellable? cancellable = null) {
do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, flags, cb, cancellable);
}
private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
Cancellable? cancellable) {
try {
Gee.List<Geary.Email>? list = yield list_email_by_id_async(initial_id, count,
required_fields, flags, cancellable);
if (list != null && list.size > 0)
cb(list, null);
cb(null, null);
} catch (Error err) {
cb(null, err);
}
}
public abstract async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
public abstract async void remove_email_async(int position, Cancellable? cancellable = null)
public abstract async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null)
throws Error;
public virtual string to_string() {

View file

@ -27,6 +27,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
public EngineFolder owner;
public int position;
public int new_remote_count;
public EmailIdentifier? id;
public ReplayRemoval(EngineFolder owner, int position, int new_remote_count) {
base ("Removal");
@ -34,10 +35,20 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
this.owner = owner;
this.position = position;
this.new_remote_count = new_remote_count;
id = null;
}
public ReplayRemoval.with_id(EngineFolder owner, EmailIdentifier id) {
base ("Removal.with_id");
this.owner = owner;
position = -1;
new_remote_count = -1;
this.id = id;
}
public override async void replay() {
yield owner.do_replay_remove_message(position, new_remote_count);
yield owner.do_replay_remove_message(position, new_remote_count, id);
}
}
@ -128,6 +139,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// signals
folder.messages_appended.connect(on_remote_messages_appended);
folder.message_removed.connect(on_remote_message_removed);
folder.message_at_removed.connect(on_remote_message_at_removed);
// state
remote_count = yield folder.get_email_count_async(cancellable);
@ -260,38 +272,52 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
}
}
private void on_remote_message_removed(int position, int total) {
debug("on_remote_message_removed: position=%d total=%d", position, total);
private void on_remote_message_removed(Geary.EmailIdentifier id) {
debug("on_remote_message_removed: %s", id.to_string());
replay_queue.schedule(new ReplayRemoval.with_id(this, id));
}
private void on_remote_message_at_removed(int position, int total) {
debug("on_remote_message_at_removed: position=%d total=%d", position, total);
replay_queue.schedule(new ReplayRemoval(this, position, total));
}
// This MUST only be called from ReplayRemoval.
private async void do_replay_remove_message(int remote_position, int new_remote_count) {
try {
// calculate the local position of the message in the local store
int local_count = yield local_folder.get_email_count_async();
int local_low = ((remote_count - local_count) + 1).clamp(1, remote_count);
if (remote_position < local_low) {
debug("do_replay_remove_message: Not removing message at %d from local store, not present",
remote_position);
} else {
// Adjust remote position to local position
yield local_folder.remove_email_async((remote_position - local_low) + 1);
private async void do_replay_remove_message(int remote_position, int new_remote_count,
Geary.EmailIdentifier? id) {
if (remote_position < 1)
assert(id != null);
else
assert(new_remote_count >= 0);
if (id == null) {
try {
Gee.List<Geary.Email>? local = yield local_folder.list_email_async(remote_position, 1,
Geary.Email.Field.NONE, Geary.Folder.ListFlags.NONE, null);
if (local != null && local.size > 0)
id = local[0].id;
} catch (Error err) {
debug("Unable to determine ID of removed message #%d from %s: %s", remote_position,
to_string(), err.message);
}
// save new remote count
remote_count = new_remote_count;
notify_message_removed(remote_position, new_remote_count);
// only fire "positions-altered" if indeed positions have been altered
if (remote_position != new_remote_count)
notify_positions_reordered();
} catch (Error err) {
debug("Unable to remove message #%d from %s: %s", remote_position, to_string(),
err.message);
}
if (id != null) {
try {
// Reflect change in the local store and notify subscribers
yield local_folder.remove_email_async(id, null);
notify_message_removed(id);
} catch (Error err2) {
debug("Unable to remove message #%d from %s: %s", remote_position, to_string(),
err2.message);
}
}
// save new remote count and notify of change
remote_count = new_remote_count;
notify_email_count_changed(remote_count, CountChangeReason.REMOVED);
}
public override async int get_email_count_async(Cancellable? cancellable = null) throws Error {
@ -322,6 +348,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
return accumulator;
}
// TODO: Capture Error and report via EmailCallback.
public override void lazy_list_email(int low, int count, Geary.Email.Field required_fields,
Geary.Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) {
// schedule do_list_email_async(), using the callback to drive availability of email
@ -392,19 +419,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// fixup local email positions to match server's positions
if (local_list_size > 0 && remote_count > 0 && local_count < remote_count) {
int adjustment = remote_count - local_count;
foreach (Geary.Email email in local_list) {
email.update_location(new Geary.EmailLocation(this,
email.location.position + adjustment, email.location.ordering));
}
} else if (local_list_size > 0 && local_only) {
// if remote_count is -1, the remote folder hasn't been opened so the true count hasn't
// been determined; create local EmailLocations that update themselves when the
// folder is opened and the count is known (adjusted by the local_offset passed in)
foreach (Geary.Email local_email in local_list) {
local_email.update_location(new Geary.EmailLocation.local(this,
local_email.location.position, local_email.location.ordering,
(count + low - 1) - local_email.location.position));
}
foreach (Geary.Email email in local_list)
email.update_position(email.position + adjustment);
}
// report list
@ -434,7 +450,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
for (int position = low; position <= (low + (count - 1)); position++) {
bool found = false;
for (int ctr = 0; ctr < local_list_size; ctr++) {
if (local_list[ctr].location.position == position) {
if (local_list[ctr].position == position) {
found = true;
break;
@ -486,6 +502,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
return accumulator;
}
// TODO: Capture Error and report via EmailCallback.
public override void lazy_list_email_sparse(int[] by_position, Geary.Email.Field required_fields,
Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) {
// schedule listing in the background, using the callback to drive availability of email
@ -546,11 +563,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// reverse the process, fixing up all the returned messages to match the server's notions
if (local_list_size > 0 && local_offset > 0) {
foreach (Geary.Email email in local_list) {
int new_position = email.location.position + local_offset;
email.update_location(new Geary.EmailLocation(this, new_position,
email.location.ordering));
}
foreach (Geary.Email email in local_list)
email.update_position(email.position + local_offset);
}
if (local_list_size == by_position.length || local_only) {
@ -576,7 +590,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
bool found = false;
if (local_list != null) {
foreach (Geary.Email email2 in local_list) {
if (email2.location.position == position) {
if (email2.position == position) {
found = true;
break;
@ -624,6 +638,84 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
cb(null, null);
}
public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier initial_id,
int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
Cancellable? cancellable = null) throws Error {
Gee.List<Geary.Email> list = new Gee.ArrayList<Geary.Email>();
yield do_list_email_by_id_async(initial_id, count, required_fields, list, null, cancellable,
flags.is_all_set(Folder.ListFlags.FAST));
return (list.size > 0) ? list : null;
}
public override void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
Cancellable? cancellable = null) {
do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, cb, cancellable,
flags.is_all_set(Folder.ListFlags.FAST));
}
private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable, bool local_only) {
try {
yield do_list_email_by_id_async(initial_id, count, required_fields, null, cb, cancellable,
local_only);
} catch (Error err) {
cb(null, err);
}
}
// STRATEGY: Determine position number of message at initial_id then work via positions from
// that point on.
private async void do_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, Gee.List<Geary.Email>? accumulator, EmailCallback? cb,
Cancellable? cancellable, bool local_only) throws Error {
if (!opened)
throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
// listing by ID requires the remote to be open and fully synchronized, as there's no
// reliable way to determine certain counts and positions without it
//
// TODO: Need to deal with this in a sane manner when offline
if (!yield wait_for_remote_to_open())
throw new EngineError.SERVER_UNAVAILABLE("Must be synchronized with server for listing by ID");
assert(remote_count >= 0);
int local_count = yield local_folder.get_email_count_async(cancellable);
int initial_position = yield local_folder.get_id_position_async(initial_id, cancellable);
if (initial_position <= 0) {
throw new EngineError.NOT_FOUND("Email ID %s in %s not known to local store",
initial_id.to_string(), to_string);
}
// since count can also indicate "to earliest" or "to latest", normalize
// (count is exclusive of initial_id, hence adding/substracting one, meaning that a count
// of zero or one are accepted)
int low, high;
if (count < 0) {
low = (count != int.MIN) ? (initial_position + count + 1) : 1;
high = initial_position;
} else if (count > 0) {
low = initial_position;
high = (count != int.MAX) ? (initial_position + count - 1) : remote_count;
} else {
// count == 0
low = initial_position;
high = initial_position;
}
int actual_count = (high - low + 1);
debug("do_list_email_by_id_async: initial_id=%s initial_position=%d count=%d actual_count=%d low=%d high=%d local_count=%d remote_count=%d",
initial_id.to_string(), initial_position, count, actual_count, low, high, local_count,
remote_count);
yield do_list_email_async(low, actual_count, required_fields, accumulator, cb, cancellable,
local_only);
}
private async Gee.List<Geary.Email>? remote_list_email(int[] needed_by_position,
Geary.Email.Field required_fields, EmailCallback? cb, Cancellable? cancellable) throws Error {
// possible to call remote multiple times, wait for it to open once and go
@ -737,8 +829,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
return email;
}
public override async void remove_email_async(int position, Cancellable? cancellable = null)
throws Error {
public override async void remove_email_async(Geary.EmailIdentifier email_id,
Cancellable? cancellable = null) throws Error {
if (!opened)
throw new EngineError.OPEN_REQUIRED("Folder %s not opened", to_string());

View file

@ -83,10 +83,12 @@ private class Geary.GenericImapFolder : Geary.EngineFolder {
}
if (uid_start_value < remote_properties.uid_next.value) {
Imap.UID uid_start = new Imap.UID(uid_start_value);
Geary.Imap.EmailIdentifier uid_start = new Geary.Imap.EmailIdentifier(
new Geary.Imap.UID(uid_start_value));
Gee.List<Geary.Email>? newest = yield imap_remote_folder.list_email_uid_async(
uid_start, null, Geary.Email.Field.PROPERTIES, cancellable);
Gee.List<Geary.Email>? newest = yield imap_remote_folder.list_email_by_id_async(
uid_start, int.MAX, Geary.Email.Field.PROPERTIES, Geary.Folder.ListFlags.NONE,
cancellable);
if (newest != null && newest.size > 0) {
debug("saving %d newest emails in %s", newest.size, to_string());
@ -103,8 +105,8 @@ private class Geary.GenericImapFolder : Geary.EngineFolder {
// fetch email from earliest email to last to (a) remove any deletions and (b) update
// any flags that may have changed
Geary.Imap.UID last_uid = new Geary.Imap.UID(local_properties.uid_next.value - 1);
Geary.Imap.UID? earliest_uid = yield imap_local_folder.get_earliest_uid_async(cancellable);
int64 full_uid_count = local_properties.uid_next.value - 1 - earliest_uid.value;
// if no earliest UID, that means no messages in local store, so nothing to update
if (earliest_uid == null || !earliest_uid.is_valid()) {
@ -113,8 +115,21 @@ private class Geary.GenericImapFolder : Geary.EngineFolder {
return true;
}
Gee.List<Geary.Email>? old_local = yield imap_local_folder.list_email_uid_async(earliest_uid,
last_uid, Geary.Email.Field.PROPERTIES, cancellable);
// If no UID's, nothing to update
if (full_uid_count <= 0 || (full_uid_count > int.MAX)) {
debug("No valid UID range in local folder %s (count=%lld), nothing to update", to_string(),
full_uid_count);
return true;
}
Geary.Imap.EmailIdentifier earliest_id = new Geary.Imap.EmailIdentifier(earliest_uid);
int full_id_count = (int) full_uid_count;
// Get the local emails in the range
Gee.List<Geary.Email>? old_local = yield imap_local_folder.list_email_by_id_async(
earliest_id, full_id_count, Geary.Email.Field.PROPERTIES, Geary.Folder.ListFlags.NONE,
cancellable);
int local_length = (old_local != null) ? old_local.size : 0;
// as before, if empty folder, nothing to update
@ -124,8 +139,10 @@ private class Geary.GenericImapFolder : Geary.EngineFolder {
return true;
}
Gee.List<Geary.Email>? old_remote = yield imap_remote_folder.list_email_uid_async(earliest_uid,
last_uid, Geary.Email.Field.PROPERTIES, cancellable);
// Get the remote emails in the range
Gee.List<Geary.Email>? old_remote = yield imap_remote_folder.list_email_by_id_async(
earliest_id, full_id_count, Geary.Email.Field.PROPERTIES, Geary.Folder.ListFlags.NONE,
cancellable);
int remote_length = (old_remote != null) ? old_remote.size : 0;
int remote_ctr = 0;
@ -164,34 +181,42 @@ private class Geary.GenericImapFolder : Geary.EngineFolder {
// local's email on the server has been removed, remove locally
try {
yield local_folder.remove_email_async(old_local[local_ctr].location.position,
cancellable);
yield local_folder.remove_email_async(old_local[local_ctr].id, cancellable);
} catch (Error remove_err) {
debug("Unable to remove discarded email from %s: %s", to_string(),
remove_err.message);
}
notify_message_removed(old_local[local_ctr].id);
local_ctr++;
}
}
// add newly-discovered emails to local store
int appended = 0;
for (; remote_ctr < remote_length; remote_ctr++) {
try {
yield local_folder.create_email_async(old_remote[remote_ctr], cancellable);
appended++;
} catch (Error append_err) {
debug("Unable to append new email to %s: %s", to_string(), append_err.message);
}
}
// remove anything left over
if (appended > 0)
notify_messages_appended(appended);
// remove anything left over ... use local count rather than remote as we're still in a stage
// where only the local messages are available
for (; local_ctr < local_length; local_ctr++) {
try {
yield local_folder.remove_email_async(old_local[local_ctr].location.position,
cancellable);
yield local_folder.remove_email_async(old_local[local_ctr].id, cancellable);
} catch (Error discard_err) {
debug("Unable to discard email from %s: %s", to_string(), discard_err.message);
}
notify_message_removed(old_local[local_ctr].id);
}
return true;

View file

@ -27,6 +27,21 @@ private interface Geary.LocalFolder : Object, Geary.Folder {
public async abstract bool is_email_present_async(Geary.EmailIdentifier id,
out Geary.Email.Field available_fields, Cancellable? cancellable = null) throws Error;
/**
* Converts an EmailIdentifier into positional addressing in the Folder. This call relies on
* the fact that when a Folder is fully opened, the local stores' tail list of messages (the
* messages located at the top of the stack, i.e. the latest ones added) are synchronized with
* the server and is gap-free, even if all the fields for those messages is not entirely
* available.
*
* Returns a positive value locating the position of the email. Other values (zero, negative)
* indicate the EmailIdentifier is unknown, which could mean the message is not associated with
* the folder, or is buried so far down the list on the remote server that it's not known
* locally (yet).
*/
public async abstract int get_id_position_async(Geary.EmailIdentifier id, Cancellable? cancellable)
throws Error;
/**
* Geary allows for a single message to exist in multiple folders. This method checks if the
* email is associated with this folder. It may rely on a Message-ID being present, in which

View file

@ -16,5 +16,13 @@ private interface Geary.RemoteAccount : Object, Geary.Account {
}
private interface Geary.RemoteFolder : Object, Geary.Folder {
/**
* A remote folder may report *either* a message has been removed by its EmailIdentifier
* (in which case it should use "message-removed") or by its position (in which case it should
* use this signal, "message-at-removed"), but never both for the same removal.
*/
public signal void message_at_removed(int position, int total);
protected abstract void notify_message_at_removed(int position, int total);
}

View file

@ -7,8 +7,7 @@
// TODO: This class currently deals with generic email storage as well as IMAP-specific issues; in
// the future, to support other email services, will need to break this up.
private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Geary.Imap.FolderExtensions,
Geary.ReferenceSemantics {
private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Geary.ReferenceSemantics {
protected int manual_ref_count { get; protected set; }
private ImapDatabase db;
@ -79,6 +78,23 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
return yield location_table.fetch_count_for_folder_async(null, folder_row.id, cancellable);
}
public async int get_id_position_async(Geary.EmailIdentifier id, Cancellable? cancellable)
throws Error {
check_open();
Transaction transaction = yield db.begin_transaction_async("Folder.get_id_position_async",
cancellable);
int64 message_id;
if (!yield location_table.does_ordering_exist_async(transaction, folder_row.id,
id.ordering, out message_id, cancellable)) {
return -1;
}
return yield location_table.fetch_message_position_async(transaction, message_id, folder_row.id,
cancellable);
}
public override async void create_email_async(Geary.Email email, Cancellable? cancellable = null)
throws Error {
yield atomic_create_email_async(null, email, cancellable);
@ -88,8 +104,6 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
Cancellable? cancellable) throws Error {
check_open();
Geary.Imap.EmailIdentifier id = (Geary.Imap.EmailIdentifier) email.id;
Transaction transaction = supplied_transaction ?? yield db.begin_transaction_async(
"Folder.atomic_create_email_async", cancellable);
@ -97,9 +111,9 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
// not account-wide)
int64 message_id;
if (yield location_table.does_ordering_exist_async(transaction, folder_row.id,
email.location.ordering, out message_id, cancellable)) {
throw new EngineError.ALREADY_EXISTS("Email with UID %s already exists in %s",
id.uid.to_string(), to_string());
email.id.ordering, out message_id, cancellable)) {
throw new EngineError.ALREADY_EXISTS("Email with ID %s already exists in %s",
email.id.to_string(), to_string());
}
// TODO: Also check by Message-ID (and perhaps other EmailProperties) to link an existing
@ -108,10 +122,9 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
message_id = yield message_table.create_async(transaction,
new MessageRow.from_email(message_table, email), cancellable);
// create the message location in the location lookup table using its UID for the ordering
// (which fulfills the requirements for the ordering column)
// create the message location in the location lookup table
MessageLocationRow location_row = new MessageLocationRow(location_table, Row.INVALID_ID,
message_id, folder_row.id, email.location.ordering, email.location.position);
message_id, folder_row.id, email.id.ordering, email.position);
yield location_table.create_async(transaction, location_row, cancellable);
// only write out the IMAP email properties if they're supplied and there's something to
@ -166,16 +179,37 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
return yield list_email(transaction, list, required_fields, cancellable);
}
public async Gee.List<Geary.Email>? list_email_uid_async(Geary.Imap.UID? low, Geary.Imap.UID? high,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier initial_id,
int count, Geary.Email.Field required_fields, Geary.Folder.ListFlags flags,
Cancellable? cancellable = null) throws Error {
if (count == 0) {
Geary.Email email = yield fetch_email_async(initial_id, required_fields, cancellable);
Gee.List<Geary.Email> singleton = new Gee.ArrayList<Geary.Email>();
singleton.add(email);
return singleton;
}
check_open();
Transaction transaction = yield db.begin_transaction_async("Folder.list_email_uid_async",
Geary.Imap.UID uid = ((Geary.Imap.EmailIdentifier) initial_id).uid;
Transaction transaction = yield db.begin_transaction_async("Folder.list_email_by_id_async",
cancellable);
int64 low, high;
if (count < 0) {
high = uid.value;
low = (count != int.MIN) ? (high + count).clamp(1, uint32.MAX) : -1;
} else {
// count > 0
low = uid.value;
high = (count != int.MAX) ? (low + count).clamp(1, uint32.MAX) : -1;
}
Gee.List<MessageLocationRow>? list = yield location_table.list_ordering_async(transaction,
folder_row.id,(low != null) ? low.value : 1, (high != null) ? high.value : -1,
cancellable);
folder_row.id, low, high, cancellable);
return yield list_email(transaction, list, required_fields, cancellable);
}
@ -219,9 +253,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
continue;
}
Geary.Email email = message_row.to_email(
new Geary.Imap.EmailLocation(this, position, uid),
new Geary.Imap.EmailIdentifier(uid));
Geary.Email email = message_row.to_email(position, new Geary.Imap.EmailIdentifier(uid));
if (properties != null)
email.set_email_properties(properties.get_imap_email_properties());
@ -279,7 +311,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
id.to_string(), to_string());
}
Geary.Email email = message_row.to_email(new Geary.Imap.EmailLocation(this, position, uid), id);
Geary.Email email = message_row.to_email(position, id);
if (properties != null)
email.set_email_properties(properties.get_imap_email_properties());
@ -295,7 +327,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
return (ordering >= 1) ? new Geary.Imap.UID(ordering) : null;
}
public override async void remove_email_async(int position, Cancellable? cancellable = null)
public override async void remove_email_async(Geary.EmailIdentifier id, Cancellable? cancellable = null)
throws Error {
check_open();
@ -306,17 +338,15 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
// (since it may be located in multiple folders). This means at some point in the future
// a vacuum will be required to remove emails that are completely unassociated with the
// account
if (!yield location_table.remove_by_position_async(transaction, folder_row.id, position, cancellable)) {
throw new EngineError.NOT_FOUND("Message #%d in local store of %s not found", position,
if (!yield location_table.remove_by_ordering_async(transaction, folder_row.id, id.ordering,
cancellable)) {
throw new EngineError.NOT_FOUND("Message %s in local store of %s not found", id.to_string(),
to_string());
}
int count = yield location_table.fetch_count_for_folder_async(transaction, folder_row.id,
cancellable);
yield transaction.commit_async(cancellable);
notify_message_removed(position, count);
notify_message_removed(id);
}
public async bool is_email_present_async(Geary.EmailIdentifier id, out Geary.Email.Field available_fields,
@ -352,9 +382,6 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
Cancellable? cancellable = null) throws Error {
check_open();
Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location;
Geary.Imap.EmailIdentifier id = (Geary.Imap.EmailIdentifier) email.id;
Transaction transaction = yield db.begin_transaction_async("Folder.update_email_async",
cancellable);
@ -362,7 +389,7 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
// a message_id that can be used for a merge; note that this works without a Message-ID)
int64 message_id;
bool associated = yield location_table.does_ordering_exist_async(transaction, folder_row.id,
id.uid.value, out message_id, cancellable);
email.id.ordering, out message_id, cancellable);
// If working around the lack of a Message-ID and not associated with this folder, treat
// this operation as a create; otherwise, since a folder-association is determined, do
@ -413,15 +440,15 @@ private class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder, Gea
if (!associated) {
// see if an email exists at this position
MessageLocationRow? location_row = yield location_table.fetch_async(transaction,
folder_row.id, location.position, cancellable);
folder_row.id, email.position, cancellable);
if (location_row != null) {
throw new EngineError.ALREADY_EXISTS("Email already exists at position %d in %s",
email.location.position, to_string());
email.position, to_string());
}
// insert email at supplied position
location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id,
folder_row.id, id.uid.value, location.position);
folder_row.id, email.id.ordering, email.position);
yield location_table.create_async(transaction, location_row, cancellable);
}

View file

@ -226,6 +226,30 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table {
return -1;
}
public async int fetch_message_position_async(Transaction? transaction, int64 message_id,
int64 folder_id, Cancellable? cancellable) throws Error {
Transaction locked = yield obtain_lock_async(transaction,
"MessageLocationTable.fetch_message_position_async", cancellable);
SQLHeavy.Query query = locked.prepare(
"SELECT message_id FROM MessageLocationTable WHERE folder_id=? ORDER BY ordering");
query.bind_int64(0, folder_id);
SQLHeavy.QueryResult results = yield query.execute_async(cancellable);
int position = 1;
while (!results.finished) {
if (results.fetch_int64(0) == message_id)
return position;
yield results.next_async(cancellable);
position++;
}
// not found
return -1;
}
public async int fetch_count_for_folder_async(Transaction? transaction,
int64 folder_id, Cancellable? cancellable) throws Error {
Transaction locked = yield obtain_lock_async(transaction,
@ -278,33 +302,26 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table {
return (!result.finished) ? result.fetch_int64(0) : -1;
}
public async bool remove_by_position_async(Transaction? transaction, int64 folder_id,
int position, Cancellable? cancellable) throws Error {
assert(position >= 1);
public async bool remove_by_ordering_async(Transaction? transaction, int64 folder_id,
int64 ordering, Cancellable? cancellable) throws Error {
Transaction locked = yield obtain_lock_async(transaction,
"MessageLocationTable.remove_by_position_async", cancellable);
"MessageLocationTable.remove_by_ordering_async", cancellable);
SQLHeavy.Query query = locked.prepare(
"SELECT id FROM MessageLocationTable WHERE folder_id = ? ORDER BY ordering LIMIT 1 OFFSET ?");
"SELECT id FROM MessageLocationTable WHERE folder_id=? AND ordering=?");
query.bind_int64(0, folder_id);
query.bind_int(1, position - 1);
query.bind_int64(1, ordering);
SQLHeavy.QueryResult results = yield query.execute_async(cancellable);
if (results.finished)
return false;
query = locked.prepare(
"DELETE FROM MessageLocationTable WHERE id = ?");
query = locked.prepare("DELETE FROM MessageLocationTable WHERE id=?");
query.bind_int64(0, results.fetch_int(0));
yield query.execute_async(cancellable);
locked.set_commit_required();
// only commit if performing our own transaction
if (transaction == null)
yield locked.commit_async(cancellable);
yield release_lock_async(transaction, locked, cancellable);
return true;

View file

@ -82,8 +82,8 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
body = fetch_string_for(result, MessageTable.Column.BODY);
}
public Geary.Email to_email(Geary.EmailLocation location, Geary.EmailIdentifier id) throws Error {
Geary.Email email = new Geary.Email(location, id);
public Geary.Email to_email(int position, Geary.EmailIdentifier id) throws Error {
Geary.Email email = new Geary.Email(position, id);
if (((fields & Geary.Email.Field.DATE) != 0) && (date != null))
email.set_send_date(new RFC822.Date(date));

View file

@ -22,7 +22,6 @@ def build(bld):
'../engine/api/geary-conversations.vala',
'../engine/api/geary-credentials.vala',
'../engine/api/geary-email-identifier.vala',
'../engine/api/geary-email-location.vala',
'../engine/api/geary-email-properties.vala',
'../engine/api/geary-email.vala',
'../engine/api/geary-endpoint.vala',
@ -38,9 +37,7 @@ def build(bld):
'../engine/imap/api/imap-account.vala',
'../engine/imap/api/imap-email-identifier.vala',
'../engine/imap/api/imap-email-location.vala',
'../engine/imap/api/imap-email-properties.vala',
'../engine/imap/api/imap-folder-extensions.vala',
'../engine/imap/api/imap-folder-properties.vala',
'../engine/imap/api/imap-folder.vala',
'../engine/imap/command/imap-command-response.vala',