Persist messages locally: #3742
This completes the heavy lifting of persisting messages locally. The strategy is that the local database may be sparsely populated, both in the availability of messages in a folder and the fields of a message that is partially stored. As data is pulled from the remote server it's always stored in the database. Future requests will always go to the database first, preventing unnecessary network traffic. Also, this patch will detect when a message is stored in multiple folders on the server. The database uses soft links from the folder to the message, so the message is stored only once in the database. This technique relies heavily on the availability and validity of the Message-ID header, but we expect this to be reliable the vast majority of the time.
This commit is contained in:
parent
4ccabcbd3e
commit
d179cb9bdd
26 changed files with 1143 additions and 273 deletions
2
Makefile
2
Makefile
|
|
@ -20,6 +20,8 @@ ENGINE_SRC := \
|
|||
src/engine/api/FolderProperties.vala \
|
||||
src/engine/api/Credentials.vala \
|
||||
src/engine/api/EngineError.vala \
|
||||
src/engine/api/RemoteInterfaces.vala \
|
||||
src/engine/api/LocalInterfaces.vala \
|
||||
src/engine/sqlite/Database.vala \
|
||||
src/engine/sqlite/Table.vala \
|
||||
src/engine/sqlite/Row.vala \
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ CREATE INDEX FolderTableParentIndex ON FolderTable(parent_id);
|
|||
|
||||
CREATE TABLE MessageTable (
|
||||
id INTEGER PRIMARY KEY,
|
||||
fields INTEGER,
|
||||
|
||||
date_field TEXT,
|
||||
date_time_t INTEGER,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ public class MessageListStore : Gtk.TreeStore {
|
|||
}
|
||||
|
||||
// The Email should've been fetched with Geary.Email.Field.ENVELOPE, at least.
|
||||
//
|
||||
// TODO: Need to insert email's in their proper position, not merely append.
|
||||
public void append_envelope(Geary.Email envelope) {
|
||||
Gtk.TreeIter iter;
|
||||
append(out iter, null);
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
|
||||
private class Geary.EngineFolder : Object, Geary.Folder {
|
||||
private NetworkAccount net;
|
||||
private RemoteAccount remote;
|
||||
private LocalAccount local;
|
||||
private Geary.Folder local_folder;
|
||||
private Geary.Folder net_folder;
|
||||
private RemoteFolder remote_folder;
|
||||
private LocalFolder local_folder;
|
||||
|
||||
public EngineFolder(NetworkAccount net, LocalAccount local, Geary.Folder local_folder) {
|
||||
this.net = net;
|
||||
public EngineFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) {
|
||||
this.remote = remote;
|
||||
this.local = local;
|
||||
this.local_folder = local_folder;
|
||||
|
||||
|
|
@ -30,152 +30,222 @@ private class Geary.EngineFolder : Object, Geary.Folder {
|
|||
return null;
|
||||
}
|
||||
|
||||
public async void create_email_async(Geary.Email email, Geary.Email.Field fields,
|
||||
Cancellable? cancellable) throws Error {
|
||||
public async void create_email_async(Geary.Email email, Cancellable? cancellable) throws Error {
|
||||
throw new EngineError.READONLY("Engine currently read-only");
|
||||
}
|
||||
|
||||
public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error {
|
||||
if (net_folder == null) {
|
||||
net_folder = yield net.fetch_folder_async(null, local_folder.get_name(), cancellable);
|
||||
net_folder.updated.connect(on_net_updated);
|
||||
yield local_folder.open_async(readonly, cancellable);
|
||||
|
||||
if (remote_folder == null) {
|
||||
remote_folder = (RemoteFolder) yield remote.fetch_folder_async(null, local_folder.get_name(),
|
||||
cancellable);
|
||||
remote_folder.updated.connect(on_remote_updated);
|
||||
}
|
||||
|
||||
yield net_folder.open_async(readonly, cancellable);
|
||||
yield remote_folder.open_async(readonly, cancellable);
|
||||
|
||||
notify_opened();
|
||||
}
|
||||
|
||||
public async void close_async(Cancellable? cancellable = null) throws Error {
|
||||
if (net_folder != null) {
|
||||
net_folder.updated.disconnect(on_net_updated);
|
||||
yield net_folder.close_async(cancellable);
|
||||
}
|
||||
yield local_folder.close_async(cancellable);
|
||||
|
||||
net_folder = null;
|
||||
if (remote_folder != null) {
|
||||
remote_folder.updated.disconnect(on_remote_updated);
|
||||
yield remote_folder.close_async(cancellable);
|
||||
remote_folder = null;
|
||||
|
||||
notify_closed(CloseReason.FOLDER_CLOSED);
|
||||
}
|
||||
}
|
||||
|
||||
public int get_message_count() throws Error {
|
||||
public async int get_email_count(Cancellable? cancellable = null) throws Error {
|
||||
// TODO
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Gee.List<Geary.Email>? list_email_async(int low, int count, Geary.Email.Field fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
public async Gee.List<Geary.Email>? list_email_async(int low, int count,
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
|
||||
assert(low >= 1);
|
||||
assert(count >= 0);
|
||||
|
||||
if (count == 0)
|
||||
return null;
|
||||
|
||||
Gee.List<Geary.Email>? local_list = yield local_folder.list_email_async(low, count, fields,
|
||||
cancellable);
|
||||
Gee.List<Geary.Email>? local_list = yield local_folder.list_email_async(low, count,
|
||||
required_fields, cancellable);
|
||||
int local_list_size = (local_list != null) ? local_list.size : 0;
|
||||
debug("local list found %d", local_list_size);
|
||||
|
||||
if (net_folder != null && local_list_size != count) {
|
||||
// go through the positions from (low) to (low + count) and see if they're not already
|
||||
// present in local_list; whatever isn't present needs to be fetched
|
||||
int[] needed_by_position = new int[0];
|
||||
int position = low;
|
||||
for (int index = 0; (index < count) && (position <= (low + count - 1)); position++) {
|
||||
while ((index < local_list_size) && (local_list[index].location.position < position))
|
||||
index++;
|
||||
|
||||
if (index >= local_list_size || local_list[index].location.position != position)
|
||||
needed_by_position += position;
|
||||
}
|
||||
if (remote_folder == null || local_list_size == count)
|
||||
return local_list;
|
||||
|
||||
// go through the positions from (low) to (low + count) and see if they're not already
|
||||
// present in local_list; whatever isn't present needs to be fetched
|
||||
int[] needed_by_position = new int[0];
|
||||
int position = low;
|
||||
for (int index = 0; (index < count) && (position <= (low + count - 1)); position++) {
|
||||
while ((index < local_list_size) && (local_list[index].location.position < position))
|
||||
index++;
|
||||
|
||||
if (needed_by_position.length != 0)
|
||||
background_update_email_list.begin(needed_by_position, fields, cancellable);
|
||||
if (index >= local_list_size || local_list[index].location.position != position)
|
||||
needed_by_position += position;
|
||||
}
|
||||
|
||||
return local_list;
|
||||
if (needed_by_position.length == 0)
|
||||
return local_list;
|
||||
|
||||
Gee.List<Geary.Email>? remote_list = yield remote_list_email(needed_by_position,
|
||||
required_fields, cancellable);
|
||||
|
||||
return combine_lists(local_list, remote_list);
|
||||
}
|
||||
|
||||
public async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position,
|
||||
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error {
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
|
||||
if (by_position.length == 0)
|
||||
return null;
|
||||
|
||||
Gee.List<Geary.Email>? local_list = yield local_folder.list_email_sparse_async(by_position,
|
||||
fields, cancellable);
|
||||
required_fields, cancellable);
|
||||
int local_list_size = (local_list != null) ? local_list.size : 0;
|
||||
|
||||
if (net_folder != null && local_list_size != by_position.length) {
|
||||
// go through the list looking for anything not already in the sparse by_position list
|
||||
// to fetch from the server; since by_position is not guaranteed to be sorted, the local
|
||||
// list needs to be searched each iteration.
|
||||
//
|
||||
// TODO: Optimize this, especially if large lists/sparse sets are supplied
|
||||
int[] needed_by_position = new int[0];
|
||||
foreach (int position in by_position) {
|
||||
bool found = false;
|
||||
if (local_list != null) {
|
||||
foreach (Geary.Email email in local_list) {
|
||||
if (email.location.position == position) {
|
||||
found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
if (remote_folder == null || local_list_size == by_position.length)
|
||||
return local_list;
|
||||
|
||||
// go through the list looking for anything not already in the sparse by_position list
|
||||
// to fetch from the server; since by_position is not guaranteed to be sorted, the local
|
||||
// list needs to be searched each iteration.
|
||||
//
|
||||
// TODO: Optimize this, especially if large lists/sparse sets are supplied
|
||||
int[] needed_by_position = new int[0];
|
||||
foreach (int position in by_position) {
|
||||
bool found = false;
|
||||
if (local_list != null) {
|
||||
foreach (Geary.Email email in local_list) {
|
||||
if (email.location.position == position) {
|
||||
found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
needed_by_position += position;
|
||||
}
|
||||
|
||||
if (needed_by_position.length != 0)
|
||||
background_update_email_list.begin(needed_by_position, fields, cancellable);
|
||||
if (!found)
|
||||
needed_by_position += position;
|
||||
}
|
||||
|
||||
return local_list;
|
||||
if (needed_by_position.length == 0)
|
||||
return local_list;
|
||||
|
||||
Gee.List<Geary.Email>? remote_list = yield remote_list_email(needed_by_position,
|
||||
required_fields, cancellable);
|
||||
|
||||
return combine_lists(local_list, remote_list);
|
||||
}
|
||||
|
||||
private async void background_update_email_list(int[] needed_by_position, Geary.Email.Field fields,
|
||||
Cancellable? cancellable) {
|
||||
private async Gee.List<Geary.Email>? remote_list_email(int[] needed_by_position,
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable) throws Error {
|
||||
debug("Background fetching %d emails for %s", needed_by_position.length, get_name());
|
||||
|
||||
Gee.List<Geary.Email>? net_list = null;
|
||||
try {
|
||||
net_list = yield net_folder.list_email_sparse_async(needed_by_position, fields,
|
||||
cancellable);
|
||||
} catch (Error net_err) {
|
||||
message("Unable to fetch emails from server: %s", net_err.message);
|
||||
|
||||
if (net_err is IOError.CANCELLED)
|
||||
return;
|
||||
}
|
||||
Gee.List<Geary.Email>? remote_list = yield remote_folder.list_email_sparse_async(
|
||||
needed_by_position, required_fields, cancellable);
|
||||
|
||||
if (net_list != null && net_list.size == 0)
|
||||
net_list = null;
|
||||
if (remote_list != null && remote_list.size == 0)
|
||||
remote_list = null;
|
||||
|
||||
if (net_list != null)
|
||||
notify_email_added_removed(net_list, null);
|
||||
|
||||
if (net_list != null) {
|
||||
foreach (Geary.Email email in net_list) {
|
||||
try {
|
||||
yield local_folder.create_email_async(email, fields, cancellable);
|
||||
} catch (Error local_err) {
|
||||
message("Unable to create email in local store: %s", local_err.message);
|
||||
|
||||
if (local_err is IOError.CANCELLED)
|
||||
return;
|
||||
// if any were fetched, store locally
|
||||
// TODO: Bulk writing
|
||||
if (remote_list != null) {
|
||||
foreach (Geary.Email email in remote_list) {
|
||||
bool exists_in_system = false;
|
||||
if (email.message_id != null) {
|
||||
int count;
|
||||
exists_in_system = yield local.has_message_id_async(email.message_id, out count,
|
||||
cancellable);
|
||||
}
|
||||
|
||||
bool exists_in_folder = yield local_folder.is_email_associated_async(email,
|
||||
cancellable);
|
||||
|
||||
// NOTE: Although this looks redundant, this is a complex decision case and laying
|
||||
// it out like this helps explain the logic. Also, this code relies on the fact
|
||||
// that update_email_async() is a powerful call which might be broken down in the
|
||||
// future (requiring a duplicate email be manually associated with the folder,
|
||||
// for example), and so would like to keep this around to facilitate that.
|
||||
if (!exists_in_system && !exists_in_folder) {
|
||||
// This case indicates the email is new to the local store OR has no
|
||||
// Message-ID and so a new copy must be stored.
|
||||
yield local_folder.create_email_async(email, cancellable);
|
||||
} else if (exists_in_system && !exists_in_folder) {
|
||||
// This case indicates the email has been (partially) stored previously but
|
||||
// was not associated with this folder; update it (which implies association)
|
||||
yield local_folder.update_email_async(email, false, cancellable);
|
||||
} else if (!exists_in_system && exists_in_folder) {
|
||||
// This case indicates the message doesn't have a Message-ID and can only be
|
||||
// identified by a folder-specific ID, so it can be updated in the folder
|
||||
// (This may result in multiple copies of the message stored locally.)
|
||||
yield local_folder.update_email_async(email, true, cancellable);
|
||||
} else if (exists_in_system && exists_in_folder) {
|
||||
// This indicates the message is in the local store and was previously
|
||||
// associated with this folder, so merely update the local store
|
||||
yield local_folder.update_email_async(email, false, cancellable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return remote_list;
|
||||
}
|
||||
|
||||
public async Geary.Email fetch_email_async(int num, Geary.Email.Field fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
if (net_folder == null)
|
||||
if (remote_folder == null)
|
||||
throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name());
|
||||
|
||||
return yield net_folder.fetch_email_async(num, fields, cancellable);
|
||||
try {
|
||||
return yield local_folder.fetch_email_async(num, fields, cancellable);
|
||||
} catch (Error err) {
|
||||
// TODO: Better parsing of error; currently merely falling through and trying network
|
||||
// for copy
|
||||
debug("Unable to fetch email from local store: %s", err.message);
|
||||
}
|
||||
|
||||
// To reach here indicates either the local version does not have all the requested fields
|
||||
// or it's simply not present. If it's not present, want to ensure that the Message-ID
|
||||
// is requested, as that's a good way to manage duplicate messages in the system
|
||||
Geary.Email.Field available_fields;
|
||||
bool is_present = yield local_folder.is_email_present_at(num, out available_fields, cancellable);
|
||||
if (!is_present)
|
||||
fields = fields.set(Geary.Email.Field.REFERENCES);
|
||||
|
||||
// fetch from network
|
||||
Geary.Email email = yield remote_folder.fetch_email_async(num, fields, cancellable);
|
||||
|
||||
// save to local store
|
||||
yield local_folder.update_email_async(email, false, cancellable);
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
private void on_local_updated() {
|
||||
}
|
||||
|
||||
private void on_net_updated() {
|
||||
private void on_remote_updated() {
|
||||
}
|
||||
|
||||
private Gee.List<Geary.Email>? combine_lists(Gee.List<Geary.Email>? a, Gee.List<Geary.Email>? b) {
|
||||
if (a == null)
|
||||
return b;
|
||||
|
||||
if (b == null)
|
||||
return a;
|
||||
|
||||
Gee.List<Geary.Email> combined = new Gee.ArrayList<Geary.Email>();
|
||||
combined.add_all(a);
|
||||
combined.add_all(b);
|
||||
|
||||
return combined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,20 @@
|
|||
*/
|
||||
|
||||
private class Geary.ImapEngine : Object, Geary.Account {
|
||||
private NetworkAccount net;
|
||||
private RemoteAccount remote;
|
||||
private LocalAccount local;
|
||||
|
||||
public ImapEngine(NetworkAccount net, LocalAccount local) {
|
||||
this.net = net;
|
||||
public ImapEngine(RemoteAccount remote, LocalAccount local) {
|
||||
this.remote = remote;
|
||||
this.local = local;
|
||||
}
|
||||
|
||||
public Geary.Email.Field get_required_fields_for_writing() {
|
||||
// Return the more restrictive of the two, which is the NetworkAccount's.
|
||||
// TODO: This could be determined at runtime rather than fixed in stone here.
|
||||
return Geary.Email.Field.HEADER | Geary.Email.Field.BODY;
|
||||
}
|
||||
|
||||
public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
}
|
||||
|
|
@ -27,7 +33,7 @@ private class Geary.ImapEngine : Object, Geary.Account {
|
|||
|
||||
Gee.Collection<Geary.Folder> engine_list = new Gee.ArrayList<Geary.Folder>();
|
||||
foreach (Geary.Folder local_folder in local_list)
|
||||
engine_list.add(new EngineFolder(net, local, local_folder));
|
||||
engine_list.add(new EngineFolder(remote, local, (LocalFolder) local_folder));
|
||||
|
||||
background_update_folders.begin(parent, engine_list);
|
||||
|
||||
|
|
@ -38,8 +44,9 @@ private class Geary.ImapEngine : Object, Geary.Account {
|
|||
|
||||
public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
Geary.Folder local_folder = yield local.fetch_folder_async(parent, folder_name, cancellable);
|
||||
Geary.Folder engine_folder = new EngineFolder(net, local, local_folder);
|
||||
LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(parent, folder_name,
|
||||
cancellable);
|
||||
Geary.Folder engine_folder = new EngineFolder(remote, local, local_folder);
|
||||
|
||||
return engine_folder;
|
||||
}
|
||||
|
|
@ -65,20 +72,20 @@ private class Geary.ImapEngine : Object, Geary.Account {
|
|||
|
||||
private async void background_update_folders(Geary.Folder? parent,
|
||||
Gee.Collection<Geary.Folder> engine_folders) {
|
||||
Gee.Collection<Geary.Folder> net_folders;
|
||||
Gee.Collection<Geary.Folder> remote_folders;
|
||||
try {
|
||||
net_folders = yield net.list_folders_async(parent);
|
||||
} catch (Error neterror) {
|
||||
error("Unable to retrieve folder list from server: %s", neterror.message);
|
||||
remote_folders = yield remote.list_folders_async(parent);
|
||||
} catch (Error remote_error) {
|
||||
error("Unable to retrieve folder list from server: %s", remote_error.message);
|
||||
}
|
||||
|
||||
Gee.Set<string> local_names = get_folder_names(engine_folders);
|
||||
Gee.Set<string> net_names = get_folder_names(net_folders);
|
||||
Gee.Set<string> remote_names = get_folder_names(remote_folders);
|
||||
|
||||
debug("%d local names, %d net names", local_names.size, net_names.size);
|
||||
debug("%d local names, %d remote names", local_names.size, remote_names.size);
|
||||
|
||||
Gee.List<Geary.Folder>? to_add = get_excluded_folders(net_folders, local_names);
|
||||
Gee.List<Geary.Folder>? to_remove = get_excluded_folders(engine_folders, net_names);
|
||||
Gee.List<Geary.Folder>? to_add = get_excluded_folders(remote_folders, local_names);
|
||||
Gee.List<Geary.Folder>? to_remove = get_excluded_folders(engine_folders, remote_names);
|
||||
|
||||
debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size);
|
||||
|
||||
|
|
@ -98,10 +105,10 @@ private class Geary.ImapEngine : Object, Geary.Account {
|
|||
Gee.Collection<Geary.Folder> engine_added = null;
|
||||
if (to_add != null) {
|
||||
engine_added = new Gee.ArrayList<Geary.Folder>();
|
||||
foreach (Geary.Folder net_folder in to_add) {
|
||||
foreach (Geary.Folder remote_folder in to_add) {
|
||||
try {
|
||||
engine_added.add(new EngineFolder(net, local,
|
||||
yield local.fetch_folder_async(parent, net_folder.get_name())));
|
||||
engine_added.add(new EngineFolder(remote, local,
|
||||
(LocalFolder) yield local.fetch_folder_async(parent, remote_folder.get_name())));
|
||||
} catch (Error convert_err) {
|
||||
error("Unable to fetch local folder: %s", convert_err.message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,18 @@ public interface Geary.Account : Object {
|
|||
folders_added_removed(added, removed);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns which Geary.Email.Field fields must be available in a Geary.Email to
|
||||
* write (or save or store) the message to the backing medium. Different implementations will
|
||||
* have different requirements, which must be reconciled.
|
||||
*
|
||||
* In this case, Geary.Email.Field.NONE means "any".
|
||||
*
|
||||
* If a write operation is attempted on an email that does not have all these fields fulfilled,
|
||||
* an EngineError.INCOMPLETE_MESSAGE will be thrown.
|
||||
*/
|
||||
public abstract Geary.Email.Field get_required_fields_for_writing();
|
||||
|
||||
public abstract async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
|
||||
Cancellable? cancellable = null) throws Error;
|
||||
|
||||
|
|
@ -32,9 +44,3 @@ public interface Geary.Account : Object {
|
|||
Cancellable? cancellable = null) throws Error;
|
||||
}
|
||||
|
||||
public interface Geary.NetworkAccount : Object, Geary.Account {
|
||||
}
|
||||
|
||||
public interface Geary.LocalAccount : Object, Geary.Account {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,19 +5,19 @@
|
|||
*/
|
||||
|
||||
public class Geary.Email : Object {
|
||||
[Flags]
|
||||
// THESE VALUES ARE PERSISTED. Change them only if you know what you're doing.
|
||||
public enum Field {
|
||||
NONE = 0,
|
||||
DATE,
|
||||
ORIGINATORS,
|
||||
RECEIVERS,
|
||||
REFERENCES,
|
||||
SUBJECT,
|
||||
HEADER,
|
||||
BODY,
|
||||
PROPERTIES,
|
||||
ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT,
|
||||
ALL = 0xFFFFFFFF;
|
||||
NONE = 0,
|
||||
DATE = 1 << 0,
|
||||
ORIGINATORS = 1 << 1,
|
||||
RECEIVERS = 1 << 2,
|
||||
REFERENCES = 1 << 3,
|
||||
SUBJECT = 1 << 4,
|
||||
HEADER = 1 << 5,
|
||||
BODY = 1 << 6,
|
||||
PROPERTIES = 1 << 7,
|
||||
ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT,
|
||||
ALL = 0xFFFFFFFF;
|
||||
|
||||
public static Field[] all() {
|
||||
return {
|
||||
|
|
@ -31,43 +31,112 @@ public class Geary.Email : Object {
|
|||
PROPERTIES
|
||||
};
|
||||
}
|
||||
|
||||
public inline bool is_set(Field required_fields) {
|
||||
return (this & required_fields) == required_fields;
|
||||
}
|
||||
|
||||
public inline Field set(Field field) {
|
||||
return (this | field);
|
||||
}
|
||||
|
||||
public inline Field clear(Field field) {
|
||||
return (this & ~(field));
|
||||
}
|
||||
}
|
||||
|
||||
public Geary.EmailLocation location { get; private set; }
|
||||
|
||||
// DATE
|
||||
public Geary.RFC822.Date? date = null;
|
||||
public Geary.RFC822.Date? date { get; private set; default = null; }
|
||||
|
||||
// ORIGINATORS
|
||||
public Geary.RFC822.MailboxAddresses? from = null;
|
||||
public Geary.RFC822.MailboxAddresses? sender = null;
|
||||
public Geary.RFC822.MailboxAddresses? reply_to = null;
|
||||
public Geary.RFC822.MailboxAddresses? from { get; private set; default = null; }
|
||||
public Geary.RFC822.MailboxAddresses? sender { get; private set; default = null; }
|
||||
public Geary.RFC822.MailboxAddresses? reply_to { get; private set; default = null; }
|
||||
|
||||
// RECEIVERS
|
||||
public Geary.RFC822.MailboxAddresses? to = null;
|
||||
public Geary.RFC822.MailboxAddresses? cc = null;
|
||||
public Geary.RFC822.MailboxAddresses? bcc = null;
|
||||
public Geary.RFC822.MailboxAddresses? to { get; private set; default = null; }
|
||||
public Geary.RFC822.MailboxAddresses? cc { get; private set; default = null; }
|
||||
public Geary.RFC822.MailboxAddresses? bcc { get; private set; default = null; }
|
||||
|
||||
// REFERENCES
|
||||
public Geary.RFC822.MessageID? message_id = null;
|
||||
public Geary.RFC822.MessageID? in_reply_to = null;
|
||||
public Geary.RFC822.MessageID? message_id { get; private set; default = null; }
|
||||
public Geary.RFC822.MessageID? in_reply_to { get; private set; default = null; }
|
||||
|
||||
// SUBJECT
|
||||
public Geary.RFC822.Subject? subject = null;
|
||||
public Geary.RFC822.Subject? subject { get; private set; default = null; }
|
||||
|
||||
// HEADER
|
||||
public RFC822.Header? header = null;
|
||||
public RFC822.Header? header { get; private set; default = null; }
|
||||
|
||||
// BODY
|
||||
public RFC822.Text? body = null;
|
||||
public RFC822.Text? body { get; private set; default = null; }
|
||||
|
||||
// PROPERTIES
|
||||
public Geary.EmailProperties? properties = null;
|
||||
public Geary.EmailProperties? properties { get; private set; default = null; }
|
||||
|
||||
public Geary.Email.Field fields { get; private set; default = Field.NONE; }
|
||||
|
||||
public Email(Geary.EmailLocation location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public void set_send_date(Geary.RFC822.Date date) {
|
||||
this.date = date;
|
||||
|
||||
fields |= Field.DATE;
|
||||
}
|
||||
|
||||
public void set_originators(Geary.RFC822.MailboxAddresses? from,
|
||||
Geary.RFC822.MailboxAddresses? sender, Geary.RFC822.MailboxAddresses? reply_to) {
|
||||
this.from = from;
|
||||
this.sender = sender;
|
||||
this.reply_to = reply_to;
|
||||
|
||||
fields |= Field.ORIGINATORS;
|
||||
}
|
||||
|
||||
public void set_receivers(Geary.RFC822.MailboxAddresses? to,
|
||||
Geary.RFC822.MailboxAddresses? cc, Geary.RFC822.MailboxAddresses? bcc) {
|
||||
this.to = to;
|
||||
this.cc = cc;
|
||||
this.bcc = bcc;
|
||||
|
||||
fields |= Field.RECEIVERS;
|
||||
}
|
||||
|
||||
public void set_references(Geary.RFC822.MessageID? message_id, Geary.RFC822.MessageID? in_reply_to) {
|
||||
this.message_id = message_id;
|
||||
this.in_reply_to = in_reply_to;
|
||||
|
||||
fields |= Field.REFERENCES;
|
||||
}
|
||||
|
||||
public void set_message_subject(Geary.RFC822.Subject subject) {
|
||||
this.subject = subject;
|
||||
|
||||
fields |= Field.SUBJECT;
|
||||
}
|
||||
|
||||
public void set_message_header(Geary.RFC822.Header header) {
|
||||
this.header = header;
|
||||
|
||||
fields |= Field.HEADER;
|
||||
}
|
||||
|
||||
public void set_message_body(Geary.RFC822.Text body) {
|
||||
this.body = body;
|
||||
|
||||
fields |= Field.BODY;
|
||||
}
|
||||
|
||||
public void set_email_properties(Geary.EmailProperties properties) {
|
||||
this.properties = properties;
|
||||
|
||||
fields |= Field.PROPERTIES;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
public errordomain Geary.EngineError {
|
||||
OPEN_REQUIRED,
|
||||
ALREADY_OPEN,
|
||||
ALREADY_EXISTS,
|
||||
NOT_FOUND,
|
||||
READONLY,
|
||||
BAD_PARAMETERS
|
||||
BAD_PARAMETERS,
|
||||
INCOMPLETE_MESSAGE
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,28 +11,71 @@ public interface Geary.Folder : Object {
|
|||
FOLDER_CLOSED
|
||||
}
|
||||
|
||||
/**
|
||||
* This is fired when the Folder is successfully opened by a caller. It will only fire once
|
||||
* until the Folder is closed.
|
||||
*/
|
||||
public signal void opened();
|
||||
|
||||
/**
|
||||
* This is fired when the Folder is successfully closed by a caller. It will only fire once
|
||||
* until the Folder is re-opened.
|
||||
*
|
||||
* The CloseReason enum can be used to inspect why the folder was closed: the connection was
|
||||
* broken locally or remotely, or the Folder was simply closed (and the underlying connection
|
||||
* is still available).
|
||||
*/
|
||||
public signal void closed(CloseReason reason);
|
||||
|
||||
/**
|
||||
* "email-added-removed" is fired when new email has been detected due to background monitoring
|
||||
* operations or if an unrelated operation causes or reveals the existence or removal of
|
||||
* messages.
|
||||
*
|
||||
* There are no guarantees of what Geary.Email.Field fields will be available when these are
|
||||
* reported. If more information is required, use the fetch or list operations.
|
||||
*/
|
||||
public signal void email_added_removed(Gee.List<Geary.Email>? added,
|
||||
Gee.List<Geary.Email>? removed);
|
||||
|
||||
/**
|
||||
* TBD.
|
||||
*/
|
||||
public signal void updated();
|
||||
|
||||
public virtual void notify_opened() {
|
||||
/**
|
||||
* 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 virtual void notify_opened() {
|
||||
opened();
|
||||
}
|
||||
|
||||
public virtual void notify_closed(CloseReason reason) {
|
||||
/**
|
||||
* 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 virtual void notify_closed(CloseReason reason) {
|
||||
closed(reason);
|
||||
}
|
||||
|
||||
public virtual void notify_email_added_removed(Gee.List<Geary.Email>? added,
|
||||
/**
|
||||
* 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 virtual void notify_email_added_removed(Gee.List<Geary.Email>? added,
|
||||
Gee.List<Geary.Email>? removed) {
|
||||
email_added_removed(added, removed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public virtual void notify_updated() {
|
||||
updated();
|
||||
}
|
||||
|
|
@ -41,31 +84,120 @@ public interface Geary.Folder : Object {
|
|||
|
||||
public abstract Geary.FolderProperties? get_properties();
|
||||
|
||||
/**
|
||||
* The Folder must be opened before most operations may be performed on it. Depending on the
|
||||
* implementation this might entail opening a network connection or setting the connection to
|
||||
* a particular state, opening a file or database, and so on.
|
||||
*
|
||||
* If the Folder has been opened previously, EngineError.ALREADY_OPEN is thrown. There are no
|
||||
* other side-effects.
|
||||
*/
|
||||
public abstract async void open_async(bool readonly, Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* The Folder should be closed when operations on it are concluded. Depending on the
|
||||
* implementation this might entail closing a network connection or reverting it to another
|
||||
* state, or closing file handles or database connections.
|
||||
*
|
||||
* If the Folder is already closed, the method silently returns.
|
||||
*/
|
||||
public abstract async void close_async(Cancellable? cancellable = null) throws Error;
|
||||
|
||||
public abstract int get_message_count() throws Error;
|
||||
|
||||
public abstract async void create_email_async(Geary.Email email,
|
||||
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error;
|
||||
/*
|
||||
* Returns the number of messages in the Folder. They can be addressed by their position,
|
||||
* from 1 to n.
|
||||
*
|
||||
* Note that this only returns the number of messages available to the backing medium. In the
|
||||
* case of the local store, this might be less than the number on the network server. Folders
|
||||
* created by Engine are aggregating objects and will return the true count.
|
||||
*
|
||||
* Also note that local folders may be sparsely populated. get_count() returns the last position
|
||||
* available, but not all emails from 1 to n may be available.
|
||||
*
|
||||
* The Folder must be opened prior to attempting this operation.
|
||||
*/
|
||||
public abstract async int get_email_count(Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* If the Folder object detects that the supplied Email does not have sufficient fields for
|
||||
* writing it, it should throw an EngineError.INCOMPLETE_MESSAGE. Use
|
||||
* get_required_fields_for_writing() to determine which fields must be present to create the
|
||||
* email.
|
||||
*
|
||||
* This method will throw EngineError.ALREADY_EXISTS if the email already exists in the folder
|
||||
* *and* the backing medium allows for checking prior to creation (which is not necessarily
|
||||
* the case with network folders). Use LocalFolder.update_email_async() to update fields on
|
||||
* an existing message in the local store. Saving an email on the server will be available
|
||||
* later.
|
||||
*
|
||||
* The Folder must be opened prior to attempting this operation.
|
||||
*/
|
||||
public abstract async void create_email_async(Geary.Email email, Cancellable? cancellable = null)
|
||||
throws Error;
|
||||
|
||||
/**
|
||||
* Returns a list of messages that fulfill the required_fields flags starting at the low
|
||||
* position and moving up to (low + count). The list is not guaranteed to be in any
|
||||
* particular order.
|
||||
*
|
||||
* If any position in low to (low + count) are out of range, only the email within range are
|
||||
* reported. No error is thrown. This allows callers to blindly request the first n emails
|
||||
* in a folder without determining the count first.
|
||||
*
|
||||
* Note that this only returns the emails with the required fields that are available to the
|
||||
* Folder's backing medium. The local store may have fewer or incomplete messages, meaning that
|
||||
* this will return an incomplete list. It is up to the caller to determine what's missing
|
||||
* and take the appropriate steps.
|
||||
*
|
||||
* In the case of a Folder returned by Engine, it will use what's available in the local store
|
||||
* and fetch from the network only what it needs, so that the caller gets a full list.
|
||||
* Note that this means the call may take some time to complete.
|
||||
*
|
||||
* TODO: Delayed listing methods (where what's available are reported via a callback after the
|
||||
* async method has completed) will be implemented in the future for more responsive behavior.
|
||||
* These may be operations only available from Folders returned by Engine.
|
||||
*
|
||||
* The Folder must be opened prior to attempting this operation.
|
||||
*
|
||||
* low is one-based.
|
||||
*/
|
||||
public abstract async Gee.List<Geary.Email>? list_email_async(int low, int count,
|
||||
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error;
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* Like list_email_async(), but the caller passes a sparse list of email by it's ordered
|
||||
* position in the folder. If any of the positions in the sparse list are out of range,
|
||||
* only the emails within range are reported. The list is not guaranteed to be in any
|
||||
* particular order.
|
||||
*
|
||||
* See the notes in list_email_async() regarding issues about local versus remote stores and
|
||||
* possible future additions to the API.
|
||||
*
|
||||
* The Folder must be opened prior to attempting this operation.
|
||||
*
|
||||
* All positions are one-based.
|
||||
*/
|
||||
public abstract async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position,
|
||||
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error;
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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(int position, Geary.Email.Field fields,
|
||||
public abstract async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields,
|
||||
Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* Used for debugging. Should not be used for user-visible labels.
|
||||
*/
|
||||
public virtual string to_string() {
|
||||
return get_name();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
71
src/engine/api/LocalInterfaces.vala
Normal file
71
src/engine/api/LocalInterfaces.vala
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
public interface Geary.LocalAccount : Object, Geary.Account {
|
||||
/**
|
||||
* Returns true if the email (identified by its Message-ID) already exists in the account's
|
||||
* local store, no matter the folder.
|
||||
*
|
||||
* Note that there are no guarantees of the uniqueness of a Message-ID, or even that a message
|
||||
* will have one. Because of this situation the method can return the number of messages
|
||||
* found with that ID.
|
||||
*/
|
||||
public async abstract bool has_message_id_async(Geary.RFC822.MessageID message_id,
|
||||
out int count, Cancellable? cancellable = null) throws Error;
|
||||
}
|
||||
|
||||
public interface Geary.LocalFolder : Object, Geary.Folder {
|
||||
/**
|
||||
* Unlike the remote store, the local store can be sparsely populated, both by fields within
|
||||
* an email and position (ordering) within the list. This checks if the email at position
|
||||
* is available. If it returns true, the available_fields indicate what is stored locally.
|
||||
*/
|
||||
public async abstract bool is_email_present_at(int position, out Geary.Email.Field available_fields,
|
||||
Cancellable? cancellable = null) 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
|
||||
* case if it's not the method will throw an EngineError.INCOMPLETE_MESSAGE.
|
||||
*
|
||||
* If the email is not in the local store, this method returns false.
|
||||
*/
|
||||
public async abstract bool is_email_associated_async(Geary.Email email, Cancellable? cancellable = null)
|
||||
throws Error;
|
||||
|
||||
/**
|
||||
* Geary allows for a single message to exist in multiple folders. It also allows for partial
|
||||
* email information to be stored and updated, building the local store as more information is
|
||||
* downloaded from the server.
|
||||
*
|
||||
* update_email_async() updates the email's information in the local store, adding any new
|
||||
* fields not already present. If the email has fields already stored, the local version *will*
|
||||
* be overwritten with this new information. However, if the email has fewer fields than the
|
||||
* local version, the old information will not be lost. In this sense this is a merge
|
||||
* operation.
|
||||
*
|
||||
* update_email_async() will also attempt to associate an email existing in the system with this
|
||||
* folder. If the message has folder-specific properties that identify it, those will be used;
|
||||
* if not, update_email_async() will attempt to use the Message-ID. If the Message-ID is not
|
||||
* available in the email, it will throw EngineError.INCOMPLETE_MESSAGE unless
|
||||
* duplicate_okay is true, which confirms that it's okay to not attempt the linkage (which
|
||||
* should be done if the message simply lacks a Message-ID).
|
||||
* TODO: Examine other fields in the email and attempt to match it with existing messages.
|
||||
*
|
||||
* The EmailLocation field is used to position the email in the folder's ordering.
|
||||
* If another email exists at the same EmailLocation.position, EngineError.ALREADY_EXISTS
|
||||
* will be thrown.
|
||||
*
|
||||
* If the email does not exist in the local store OR the email has no Message-ID and
|
||||
* no_incomplete_error is true OR multiple messages are found in the system with the same
|
||||
* Message-ID, update_email-async() will see if there's any indication of the email being
|
||||
* associated with the folder. If so, it will merge in the new information. If not, this
|
||||
* method will fall-through to create_email_async().
|
||||
*/
|
||||
public async abstract void update_email_async(Geary.Email email, bool duplicate_okay,
|
||||
Cancellable? cancellable = null) throws Error;
|
||||
}
|
||||
|
||||
12
src/engine/api/RemoteInterfaces.vala
Normal file
12
src/engine/api/RemoteInterfaces.vala
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
public interface Geary.RemoteAccount : Object, Geary.Account {
|
||||
}
|
||||
|
||||
public interface Geary.RemoteFolder : Object, Geary.Folder {
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +54,18 @@ public abstract class Geary.Common.LongMessageData : Geary.Common.MessageData {
|
|||
}
|
||||
}
|
||||
|
||||
public abstract class Geary.Common.Int64MessageData : Geary.Common.MessageData {
|
||||
public int64 value { get; private set; }
|
||||
|
||||
public Int64MessageData(int64 value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return value.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Geary.Common.BlockMessageData : Geary.Common.MessageData {
|
||||
public string data_name { get; private set; }
|
||||
public Geary.Memory.AbstractBuffer buffer { get; private set; }
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
|
|||
|
||||
FetchResults[] results = FetchResults.decode(resp);
|
||||
foreach (FetchResults res in results) {
|
||||
// TODO: Add UID
|
||||
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(res.msg_num, 0));
|
||||
UID? uid = res.get_data(FetchDataType.UID) as UID;
|
||||
assert(uid != null);
|
||||
|
||||
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(res.msg_num, uid));
|
||||
fetch_results_to_email(res, fields, email);
|
||||
msgs.add(email);
|
||||
}
|
||||
|
|
@ -79,8 +81,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
|
|||
results[0].msg_num, msg_num);
|
||||
}
|
||||
|
||||
// TODO: Add UID
|
||||
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(results[0].msg_num, 0));
|
||||
UID? uid = results[0].get_data(FetchDataType.UID) as UID;
|
||||
assert(uid != null);
|
||||
|
||||
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(results[0].msg_num, uid));
|
||||
fetch_results_to_email(results[0], fields, email);
|
||||
|
||||
return email;
|
||||
|
|
@ -132,11 +136,14 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
|
|||
}
|
||||
|
||||
assert(data_type_set.size > 0);
|
||||
FetchDataType[] data_types = new FetchDataType[data_type_set.size];
|
||||
FetchDataType[] data_types = new FetchDataType[data_type_set.size + 1];
|
||||
int ctr = 0;
|
||||
foreach (FetchDataType data_type in data_type_set)
|
||||
data_types[ctr++] = data_type;
|
||||
|
||||
// UID is always fetched, no matter what the caller requests
|
||||
data_types[ctr] = FetchDataType.UID;
|
||||
|
||||
return data_types;
|
||||
}
|
||||
|
||||
|
|
@ -152,39 +159,31 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
|
|||
Envelope envelope = (Envelope) data;
|
||||
|
||||
if ((fields & Geary.Email.Field.DATE) != 0)
|
||||
email.date = envelope.sent;
|
||||
email.set_send_date(envelope.sent);
|
||||
|
||||
if ((fields & Geary.Email.Field.SUBJECT) != 0)
|
||||
email.subject = envelope.subject;
|
||||
email.set_message_subject(envelope.subject);
|
||||
|
||||
if ((fields & Geary.Email.Field.ORIGINATORS) != 0) {
|
||||
email.from = envelope.from;
|
||||
email.sender = envelope.sender;
|
||||
email.reply_to = envelope.reply_to;
|
||||
}
|
||||
if ((fields & Geary.Email.Field.ORIGINATORS) != 0)
|
||||
email.set_originators(envelope.from, envelope.sender, envelope.reply_to);
|
||||
|
||||
if ((fields & Geary.Email.Field.RECEIVERS) != 0) {
|
||||
email.to = envelope.to;
|
||||
email.cc = envelope.cc;
|
||||
email.bcc = envelope.bcc;
|
||||
}
|
||||
if ((fields & Geary.Email.Field.RECEIVERS) != 0)
|
||||
email.set_receivers(envelope.to, envelope.cc, envelope.bcc);
|
||||
|
||||
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
|
||||
email.in_reply_to = envelope.in_reply_to;
|
||||
email.message_id = envelope.message_id;
|
||||
}
|
||||
if ((fields & Geary.Email.Field.REFERENCES) != 0)
|
||||
email.set_references(envelope.message_id, envelope.in_reply_to);
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_HEADER:
|
||||
email.header = (RFC822.Header) data;
|
||||
email.set_message_header((RFC822.Header) data);
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_TEXT:
|
||||
email.body = (RFC822.Text) data;
|
||||
email.set_message_body((RFC822.Text) data);
|
||||
break;
|
||||
|
||||
case FetchDataType.FLAGS:
|
||||
email.properties = new Imap.EmailProperties((MessageFlags) data);
|
||||
email.set_email_properties(new Imap.EmailProperties((MessageFlags) data));
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
public interface Geary.Imap.MessageData : Geary.Common.MessageData {
|
||||
}
|
||||
|
||||
public class Geary.Imap.UID : Geary.Common.IntMessageData, Geary.Imap.MessageData {
|
||||
public UID(int value) {
|
||||
public class Geary.Imap.UID : Geary.Common.Int64MessageData, Geary.Imap.MessageData {
|
||||
public UID(int64 value) {
|
||||
base (value);
|
||||
}
|
||||
}
|
||||
|
|
@ -109,13 +109,13 @@ public class Geary.Imap.Envelope : Geary.Common.MessageData, Geary.Imap.MessageD
|
|||
public Geary.RFC822.MailboxAddresses? cc { get; private set; }
|
||||
public Geary.RFC822.MailboxAddresses? bcc { get; private set; }
|
||||
public Geary.RFC822.MessageID? in_reply_to { get; private set; }
|
||||
public Geary.RFC822.MessageID message_id { get; private set; }
|
||||
public Geary.RFC822.MessageID? message_id { get; private set; }
|
||||
|
||||
public Envelope(Geary.RFC822.Date sent, Geary.RFC822.Subject subject,
|
||||
Geary.RFC822.MailboxAddresses from, Geary.RFC822.MailboxAddresses sender,
|
||||
Geary.RFC822.MailboxAddresses? reply_to, Geary.RFC822.MailboxAddresses? to,
|
||||
Geary.RFC822.MailboxAddresses? cc, Geary.RFC822.MailboxAddresses? bcc,
|
||||
Geary.RFC822.MessageID? in_reply_to, Geary.RFC822.MessageID message_id) {
|
||||
Geary.RFC822.MessageID? in_reply_to, Geary.RFC822.MessageID? message_id) {
|
||||
this.sent = sent;
|
||||
this.subject = subject;
|
||||
this.from = from;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.Account : Object, Geary.Account, Geary.NetworkAccount {
|
||||
public class Geary.Imap.Account : Object, Geary.Account, Geary.RemoteAccount {
|
||||
private ClientSessionManager session_mgr;
|
||||
|
||||
public Account(Credentials cred, uint default_port) {
|
||||
session_mgr = new ClientSessionManager(cred, default_port);
|
||||
}
|
||||
|
||||
public Geary.Email.Field get_required_fields_for_writing() {
|
||||
return Geary.Email.Field.HEADER | Geary.Email.Field.BODY;
|
||||
}
|
||||
|
||||
public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
throw new EngineError.READONLY("IMAP readonly");
|
||||
|
|
|
|||
|
|
@ -4,10 +4,15 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
//
|
||||
// The EmailLocation for any message originating from an ImapAccount is guaranteed to have its
|
||||
// UID attached to it, whether or not it was requested in the FETCH operation.
|
||||
//
|
||||
|
||||
private class Geary.Imap.EmailLocation : Geary.EmailLocation {
|
||||
public int64 uid { get; private set; }
|
||||
public Geary.Imap.UID uid { get; private set; }
|
||||
|
||||
public EmailLocation(int position, int64 uid) {
|
||||
public EmailLocation(int position, Geary.Imap.UID uid) {
|
||||
base (position);
|
||||
|
||||
this.uid = uid;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.Folder : Object, Geary.Folder {
|
||||
public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder {
|
||||
private ClientSessionManager session_mgr;
|
||||
private MailboxInformation info;
|
||||
private string name;
|
||||
|
|
@ -42,23 +42,32 @@ public class Geary.Imap.Folder : Object, Geary.Folder {
|
|||
|
||||
this.readonly = Trillian.from_boolean(readonly);
|
||||
properties.uid_validity = mailbox.uid_validity;
|
||||
|
||||
notify_opened();
|
||||
}
|
||||
|
||||
public async void close_async(Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
return;
|
||||
|
||||
mailbox = null;
|
||||
readonly = Trillian.UNKNOWN;
|
||||
properties.uid_validity = null;
|
||||
|
||||
notify_closed(CloseReason.FOLDER_CLOSED);
|
||||
}
|
||||
|
||||
public int get_message_count() throws Error {
|
||||
public async int get_email_count(Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
|
||||
return mailbox.count;
|
||||
}
|
||||
|
||||
public async void create_email_async(Geary.Email email, Geary.Email.Field fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
public async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
|
||||
throw new EngineError.READONLY("IMAP currently read-only");
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +92,9 @@ public class Geary.Imap.Folder : Object, Geary.Folder {
|
|||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
|
||||
// TODO: If position out of range, throw EngineError.NOT_FOUND
|
||||
|
||||
return yield mailbox.fetch_async(position, fields, cancellable);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,12 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder {
|
|||
ListParameter? cc = listp.get_as_nullable_list(6);
|
||||
ListParameter? bcc = listp.get_as_nullable_list(7);
|
||||
StringParameter? in_reply_to = listp.get_as_nullable_string(8);
|
||||
StringParameter message_id = listp.get_as_string(9);
|
||||
StringParameter? message_id = listp.get_as_string(9);
|
||||
|
||||
// Although Message-ID is required to be returned by IMAP, it may be blank if the email
|
||||
// does not supply it (optional according to RFC822); deal with this cognitive dissonance
|
||||
if (String.is_empty(message_id.value))
|
||||
message_id = null;
|
||||
|
||||
return new Envelope(new Geary.RFC822.Date(sent.value),
|
||||
new Geary.RFC822.Subject(subject.value),
|
||||
|
|
@ -124,7 +129,7 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder {
|
|||
(cc != null) ? parse_addresses(cc) : null,
|
||||
(bcc != null) ? parse_addresses(bcc) : null,
|
||||
(in_reply_to != null) ? new Geary.RFC822.MessageID(in_reply_to.value) : null,
|
||||
new Geary.RFC822.MessageID(message_id.value));
|
||||
(message_id != null) ? new Geary.RFC822.MessageID(message_id.value) : null);
|
||||
}
|
||||
|
||||
// TODO: This doesn't handle group lists (see Johnson, p.268) -- this will throw an
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
public class Geary.Sqlite.ImapMessageLocationPropertiesRow : Geary.Sqlite.Row {
|
||||
public int64 id { get; private set; }
|
||||
public int64 location_id { get; private set; }
|
||||
public int64 uid { get; private set; }
|
||||
public Geary.Imap.UID uid { get; private set; }
|
||||
|
||||
public ImapMessageLocationPropertiesRow(ImapMessageLocationPropertiesTable table, int64 id,
|
||||
int64 location_id, int64 uid) {
|
||||
int64 location_id, Geary.Imap.UID uid) {
|
||||
base (table);
|
||||
|
||||
this.id = id;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class Geary.Sqlite.ImapMessageLocationPropertiesTable : Geary.Sqlite.Tabl
|
|||
SQLHeavy.Query query = db.prepare(
|
||||
"INSERT INTO ImapMessageLocationPropertiesTable (location_id, uid) VALUES (?, ?)");
|
||||
query.bind_int64(0, row.location_id);
|
||||
query.bind_int64(1, row.uid);
|
||||
query.bind_int64(1, row.uid.value);
|
||||
|
||||
return yield query.execute_insert_async(cancellable);
|
||||
}
|
||||
|
|
@ -37,7 +37,29 @@ public class Geary.Sqlite.ImapMessageLocationPropertiesTable : Geary.Sqlite.Tabl
|
|||
return null;
|
||||
|
||||
return new ImapMessageLocationPropertiesRow(this, result.fetch_int64(0), location_id,
|
||||
result.fetch_int64(1));
|
||||
new Geary.Imap.UID(result.fetch_int64(1)));
|
||||
}
|
||||
|
||||
public async bool search_uid_in_folder(Geary.Imap.UID uid, int64 folder_id,
|
||||
out int64 message_id, Cancellable? cancellable = null) throws Error {
|
||||
message_id = Row.INVALID_ID;
|
||||
|
||||
SQLHeavy.Query query = db.prepare(
|
||||
"SELECT MessageLocationTable.message_id "
|
||||
+ "FROM ImapMessageLocationPropertiesTable "
|
||||
+ "INNER JOIN MessageLocationTable "
|
||||
+ "WHERE MessageLocationTable.folder_id=? "
|
||||
+ "AND ImapMessageLocationPropertiesTable.location_id=MessageLocationTable.id "
|
||||
+ "AND ImapMessageLocationPropertiesTable.uid=?");
|
||||
query.bind_int64(0, folder_id);
|
||||
query.bind_int64(1, uid.value);
|
||||
|
||||
SQLHeavy.QueryResult result = yield query.execute_async(cancellable);
|
||||
|
||||
if (!result.finished)
|
||||
message_id = result.fetch_int64(0);
|
||||
|
||||
return !result.finished;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
|
||||
public int64 id { get; set; default = INVALID_ID; }
|
||||
public Geary.Email.Field fields { get; set; default = Geary.Email.Field.NONE; }
|
||||
|
||||
public string? date { get; set; }
|
||||
public time_t date_time_t { get; set; default = -1; }
|
||||
|
|
@ -34,33 +35,19 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
|
|||
public MessageRow.from_email(MessageTable table, Geary.Email email) {
|
||||
base (table);
|
||||
|
||||
date = (email.date != null) ? email.date.original : null;
|
||||
date_time_t = (email.date != null) ? email.date.as_time_t : -1;
|
||||
|
||||
from = flatten_addresses(email.from);
|
||||
sender = flatten_addresses(email.sender);
|
||||
reply_to = flatten_addresses(email.reply_to);
|
||||
|
||||
to = flatten_addresses(email.to);
|
||||
cc = flatten_addresses(email.cc);
|
||||
bcc = flatten_addresses(email.bcc);
|
||||
|
||||
message_id = (email.message_id != null) ? email.message_id.value : null;
|
||||
in_reply_to = (email.in_reply_to != null) ? email.in_reply_to.value : null;
|
||||
|
||||
subject = (email.subject != null) ? email.subject.value : null;
|
||||
|
||||
header = (email.header != null) ? email.header.buffer.to_ascii_string() : null;
|
||||
|
||||
body = (email.body != null) ? email.body.buffer.to_ascii_string() : null;
|
||||
set_from_email(email.fields, email);
|
||||
}
|
||||
|
||||
public MessageRow.from_query_result(Table table, Geary.Email.Field fields, SQLHeavy.QueryResult result)
|
||||
throws Error {
|
||||
public MessageRow.from_query_result(Table table, Geary.Email.Field requested_fields,
|
||||
SQLHeavy.QueryResult result) throws Error {
|
||||
base (table);
|
||||
|
||||
id = fetch_int64_for(result, MessageTable.Column.ID);
|
||||
|
||||
// the available fields are an intersection of what's available in the database and
|
||||
// what was requested
|
||||
fields = requested_fields & fetch_int_for(result, MessageTable.Column.FIELDS);
|
||||
|
||||
if ((fields & Geary.Email.Field.DATE) != 0) {
|
||||
date = fetch_string_for(result, MessageTable.Column.DATE_FIELD);
|
||||
date_time_t = (time_t) fetch_int64_for(result, MessageTable.Column.DATE_INT64);
|
||||
|
|
@ -96,31 +83,46 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
|
|||
public Geary.Email to_email(Geary.EmailLocation location) throws Error {
|
||||
Geary.Email email = new Geary.Email(location);
|
||||
|
||||
email.date = (date != null) ? new RFC822.Date(date) : null;
|
||||
if (((fields & Geary.Email.Field.DATE) != 0) && (date != null))
|
||||
email.set_send_date(new RFC822.Date(date));
|
||||
|
||||
email.from = unflatten_addresses(from);
|
||||
email.sender = unflatten_addresses(sender);
|
||||
email.reply_to = unflatten_addresses(reply_to);
|
||||
if ((fields & Geary.Email.Field.ORIGINATORS) != 0) {
|
||||
email.set_originators(unflatten_addresses(from), unflatten_addresses(sender),
|
||||
unflatten_addresses(reply_to));
|
||||
}
|
||||
|
||||
email.to = unflatten_addresses(to);
|
||||
email.cc = unflatten_addresses(cc);
|
||||
email.bcc = unflatten_addresses(bcc);
|
||||
if ((fields & Geary.Email.Field.RECEIVERS) != 0) {
|
||||
email.set_receivers(unflatten_addresses(to), unflatten_addresses(cc),
|
||||
unflatten_addresses(bcc));
|
||||
}
|
||||
|
||||
email.message_id = (message_id != null) ? new RFC822.MessageID(message_id) : null;
|
||||
email.in_reply_to = (in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null;
|
||||
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
|
||||
email.set_references((message_id != null) ? new RFC822.MessageID(message_id) : null,
|
||||
(in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null);
|
||||
}
|
||||
|
||||
email.subject = (subject != null) ? new RFC822.Subject(subject) : null;
|
||||
if (((fields & Geary.Email.Field.SUBJECT) != 0) && (subject != null))
|
||||
email.set_message_subject(new RFC822.Subject(subject));
|
||||
|
||||
email.header = (header != null) ? new RFC822.Header(new Geary.Memory.StringBuffer(header))
|
||||
: null;
|
||||
if (((fields & Geary.Email.Field.HEADER) != 0) && (header != null))
|
||||
email.set_message_header(new RFC822.Header(new Geary.Memory.StringBuffer(header)));
|
||||
|
||||
email.body = (body != null) ? new RFC822.Text(new Geary.Memory.StringBuffer(body))
|
||||
: null;
|
||||
if (((fields & Geary.Email.Field.BODY) != 0) && (body != null))
|
||||
email.set_message_body(new RFC822.Text(new Geary.Memory.StringBuffer(body)));
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
public string? flatten_addresses(RFC822.MailboxAddresses? addrs) {
|
||||
public void merge_from_network(Geary.Email email) {
|
||||
foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
|
||||
if ((email.fields & field) != 0)
|
||||
set_from_email(field, email);
|
||||
else
|
||||
unset_fields(field);
|
||||
}
|
||||
}
|
||||
|
||||
private string? flatten_addresses(RFC822.MailboxAddresses? addrs) {
|
||||
if (addrs == null)
|
||||
return null;
|
||||
|
||||
|
|
@ -144,8 +146,111 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
|
|||
}
|
||||
}
|
||||
|
||||
public RFC822.MailboxAddresses? unflatten_addresses(string? str) {
|
||||
private RFC822.MailboxAddresses? unflatten_addresses(string? str) {
|
||||
return String.is_empty(str) ? null : new RFC822.MailboxAddresses.from_rfc822_string(str);
|
||||
}
|
||||
|
||||
private void set_from_email(Geary.Email.Field fields, Geary.Email email) {
|
||||
// Although the fields bitmask might indicate various fields are set, they may still be
|
||||
// null if empty
|
||||
|
||||
if ((fields & Geary.Email.Field.DATE) != 0) {
|
||||
date = (email.date != null) ? email.date.original : null;
|
||||
date_time_t = (email.date != null) ? email.date.as_time_t : -1;
|
||||
|
||||
this.fields = this.fields.set(Geary.Email.Field.DATE);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.ORIGINATORS) != 0) {
|
||||
from = flatten_addresses(email.from);
|
||||
sender = flatten_addresses(email.sender);
|
||||
reply_to = flatten_addresses(email.reply_to);
|
||||
|
||||
this.fields = this.fields.set(Geary.Email.Field.ORIGINATORS);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.RECEIVERS) != 0) {
|
||||
to = flatten_addresses(email.to);
|
||||
cc = flatten_addresses(email.cc);
|
||||
bcc = flatten_addresses(email.bcc);
|
||||
|
||||
this.fields = this.fields.set(Geary.Email.Field.RECEIVERS);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
|
||||
message_id = (email.message_id != null) ? email.message_id.value : null;
|
||||
in_reply_to = (email.in_reply_to != null) ? email.in_reply_to.value : null;
|
||||
|
||||
this.fields = this.fields.set(Geary.Email.Field.REFERENCES);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.SUBJECT) != 0) {
|
||||
subject = (email.subject != null) ? email.subject.value : null;
|
||||
|
||||
this.fields = this.fields.set(Geary.Email.Field.SUBJECT);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.HEADER) != 0) {
|
||||
header = (email.header != null) ? email.header.buffer.to_ascii_string() : null;
|
||||
|
||||
this.fields = this.fields.set(Geary.Email.Field.HEADER);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.BODY) != 0) {
|
||||
body = (email.body != null) ? email.body.buffer.to_ascii_string() : null;
|
||||
|
||||
this.fields = this.fields.set(Geary.Email.Field.BODY);
|
||||
}
|
||||
}
|
||||
|
||||
private void unset_fields(Geary.Email.Field fields) {
|
||||
if ((fields & Geary.Email.Field.DATE) != 0) {
|
||||
date = null;
|
||||
date_time_t = -1;
|
||||
|
||||
this.fields = this.fields.clear(Geary.Email.Field.DATE);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.ORIGINATORS) != 0) {
|
||||
from = null;
|
||||
sender = null;
|
||||
reply_to = null;
|
||||
|
||||
this.fields = this.fields.clear(Geary.Email.Field.ORIGINATORS);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.RECEIVERS) != 0) {
|
||||
to = null;
|
||||
cc = null;
|
||||
bcc = null;
|
||||
|
||||
this.fields = this.fields.clear(Geary.Email.Field.RECEIVERS);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
|
||||
message_id = null;
|
||||
in_reply_to = null;
|
||||
|
||||
this.fields = this.fields.clear(Geary.Email.Field.REFERENCES);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.SUBJECT) != 0) {
|
||||
subject = null;
|
||||
|
||||
this.fields = this.fields.clear(Geary.Email.Field.SUBJECT);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.HEADER) != 0) {
|
||||
header = null;
|
||||
|
||||
this.fields = this.fields.clear(Geary.Email.Field.HEADER);
|
||||
}
|
||||
|
||||
if ((fields & Geary.Email.Field.BODY) != 0) {
|
||||
body = null;
|
||||
|
||||
this.fields = this.fields.clear(Geary.Email.Field.BODY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
|
|||
// This *must* match the column order in the database
|
||||
public enum Column {
|
||||
ID,
|
||||
FIELDS,
|
||||
|
||||
DATE_FIELD,
|
||||
DATE_INT64,
|
||||
|
|
@ -37,31 +38,116 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
|
|||
public async int64 create_async(MessageRow row, Cancellable? cancellable) throws Error {
|
||||
SQLHeavy.Query query = db.prepare(
|
||||
"INSERT INTO MessageTable "
|
||||
+ "(date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, message_id, in_reply_to, subject, header, body) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
query.bind_string(0, row.date);
|
||||
query.bind_int64(1, row.date_time_t);
|
||||
query.bind_string(2, row.from);
|
||||
query.bind_string(3, row.sender);
|
||||
query.bind_string(4, row.reply_to);
|
||||
query.bind_string(5, row.to);
|
||||
query.bind_string(6, row.cc);
|
||||
query.bind_string(7, row.bcc);
|
||||
query.bind_string(8, row.message_id);
|
||||
query.bind_string(9, row.in_reply_to);
|
||||
query.bind_string(10, row.subject);
|
||||
query.bind_string(11, row.header);
|
||||
query.bind_string(12, row.body);
|
||||
+ "(fields, date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, "
|
||||
+ "message_id, in_reply_to, subject, header, body) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
query.bind_int(0, row.fields);
|
||||
query.bind_string(1, row.date);
|
||||
query.bind_int64(2, row.date_time_t);
|
||||
query.bind_string(3, row.from);
|
||||
query.bind_string(4, row.sender);
|
||||
query.bind_string(5, row.reply_to);
|
||||
query.bind_string(6, row.to);
|
||||
query.bind_string(7, row.cc);
|
||||
query.bind_string(8, row.bcc);
|
||||
query.bind_string(9, row.message_id);
|
||||
query.bind_string(10, row.in_reply_to);
|
||||
query.bind_string(11, row.subject);
|
||||
query.bind_string(12, row.header);
|
||||
query.bind_string(13, row.body);
|
||||
|
||||
return yield query.execute_insert_async(cancellable);
|
||||
}
|
||||
|
||||
public async void merge_async(MessageRow row, Cancellable? cancellable = null) throws Error {
|
||||
SQLHeavy.Transaction transaction = db.begin_transaction();
|
||||
|
||||
// merge the valid fields in the row
|
||||
SQLHeavy.Query query = transaction.prepare(
|
||||
"UPDATE MessageTable SET fields = fields | ? WHERE id=?");
|
||||
query.bind_int(0, row.fields);
|
||||
query.bind_int64(1, row.id);
|
||||
|
||||
query.execute();
|
||||
|
||||
if (row.fields.is_set(Geary.Email.Field.DATE)) {
|
||||
query = transaction.prepare(
|
||||
"UPDATE MessageTable SET date_field=?, date_time_t=? WHERE id=?");
|
||||
query.bind_string(0, row.date);
|
||||
query.bind_int64(1, row.date_time_t);
|
||||
query.bind_int64(2, row.id);
|
||||
|
||||
query.execute();
|
||||
}
|
||||
|
||||
if (row.fields.is_set(Geary.Email.Field.ORIGINATORS)) {
|
||||
query = transaction.prepare(
|
||||
"UPDATE MessageTable SET from_field=?, sender=?, reply_to=? WHERE id=?");
|
||||
query.bind_string(0, row.from);
|
||||
query.bind_string(1, row.sender);
|
||||
query.bind_string(2, row.reply_to);
|
||||
query.bind_int64(3, row.id);
|
||||
|
||||
query.execute();
|
||||
}
|
||||
|
||||
if (row.fields.is_set(Geary.Email.Field.RECEIVERS)) {
|
||||
query = transaction.prepare(
|
||||
"UPDATE MessageTable SET to_field=?, cc=?, bcc=? WHERE id=?");
|
||||
query.bind_string(0, row.to);
|
||||
query.bind_string(1, row.cc);
|
||||
query.bind_string(2, row.bcc);
|
||||
query.bind_int64(3, row.id);
|
||||
|
||||
query.execute();
|
||||
}
|
||||
|
||||
if (row.fields.is_set(Geary.Email.Field.REFERENCES)) {
|
||||
query = transaction.prepare(
|
||||
"UPDATE MessageTable SET message_id=?, in_reply_to=? WHERE id=?");
|
||||
query.bind_string(0, row.message_id);
|
||||
query.bind_string(1, row.in_reply_to);
|
||||
query.bind_int64(2, row.id);
|
||||
|
||||
query.execute();
|
||||
}
|
||||
|
||||
if (row.fields.is_set(Geary.Email.Field.SUBJECT)) {
|
||||
query = transaction.prepare(
|
||||
"UPDATE MessageTable SET subject=? WHERE id=?");
|
||||
query.bind_string(0, row.subject);
|
||||
query.bind_int64(1, row.id);
|
||||
|
||||
query.execute();
|
||||
}
|
||||
|
||||
if (row.fields.is_set(Geary.Email.Field.HEADER)) {
|
||||
query = transaction.prepare(
|
||||
"UPDATE MessageTable SET header=? WHERE id=?");
|
||||
query.bind_string(0, row.header);
|
||||
query.bind_int64(1, row.id);
|
||||
|
||||
query.execute();
|
||||
}
|
||||
|
||||
if (row.fields.is_set(Geary.Email.Field.BODY)) {
|
||||
query = transaction.prepare(
|
||||
"UPDATE MessageTable SET body=? WHERE id=?");
|
||||
query.bind_string(0, row.body);
|
||||
query.bind_int64(1, row.id);
|
||||
|
||||
query.execute();
|
||||
}
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
public async Gee.List<MessageRow>? list_by_message_id_async(Geary.RFC822.MessageID message_id,
|
||||
Geary.Email.Field fields, Cancellable? cancellable) throws Error {
|
||||
assert(fields != Geary.Email.Field.NONE);
|
||||
|
||||
SQLHeavy.Query query = db.prepare(
|
||||
"SELECT %s FROM MessageTable WHERE message_id = ?".printf(fields_to_columns(fields)));
|
||||
"SELECT %s FROM MessageTable WHERE message_id=?".printf(fields_to_columns(fields)));
|
||||
query.bind_string(0, message_id.value);
|
||||
|
||||
SQLHeavy.QueryResult results = yield query.execute_async(cancellable);
|
||||
|
|
@ -77,25 +163,42 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
|
|||
return (list.size > 0) ? list : null;
|
||||
}
|
||||
|
||||
public async MessageRow? fetch_async(int64 id, Geary.Email.Field fields,
|
||||
public async MessageRow? fetch_async(int64 id, Geary.Email.Field requested_fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
assert(fields != Geary.Email.Field.NONE);
|
||||
assert(requested_fields != Geary.Email.Field.NONE);
|
||||
|
||||
SQLHeavy.Query query = db.prepare(
|
||||
"SELECT id, %s FROM MessageTable WHERE id = ?".printf(fields_to_columns(fields)));
|
||||
"SELECT %s FROM MessageTable WHERE id=?".printf(fields_to_columns(requested_fields)));
|
||||
query.bind_int64(0, id);
|
||||
|
||||
SQLHeavy.QueryResult results = yield query.execute_async(cancellable);
|
||||
if (results.finished)
|
||||
return null;
|
||||
|
||||
MessageRow row = new MessageRow.from_query_result(this, fields, results);
|
||||
MessageRow row = new MessageRow.from_query_result(this, requested_fields, results);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
public async bool fetch_fields_async(int64 id, out Geary.Email.Field available_fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
available_fields = Geary.Email.Field.NONE;
|
||||
|
||||
SQLHeavy.Query query = db.prepare(
|
||||
"SELECT fields FROM MessageTable WHERE id=?");
|
||||
query.bind_int64(0, id);
|
||||
|
||||
SQLHeavy.QueryResult result = yield query.execute_async(cancellable);
|
||||
if (result.finished)
|
||||
return false;
|
||||
|
||||
available_fields = (Geary.Email.Field) result.fetch_int(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string fields_to_columns(Geary.Email.Field fields) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
StringBuilder builder = new StringBuilder("id, fields");
|
||||
foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
|
||||
string? append = null;
|
||||
if ((fields & field) != 0) {
|
||||
|
|
@ -140,5 +243,35 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
|
|||
|
||||
return builder.str;
|
||||
}
|
||||
|
||||
public async int search_message_id_count_async(Geary.RFC822.MessageID message_id,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
SQLHeavy.Query query = db.prepare(
|
||||
"SELECT COUNT(*) FROM MessageTable WHERE message_id=?");
|
||||
query.bind_string(0, message_id.value);
|
||||
|
||||
SQLHeavy.QueryResult result = yield query.execute_async(cancellable);
|
||||
|
||||
return (result.finished) ? 0 : result.fetch_int(0);
|
||||
}
|
||||
|
||||
public async Gee.List<int64?>? search_message_id_async(Geary.RFC822.MessageID message_id,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
SQLHeavy.Query query = db.prepare(
|
||||
"SELECT id FROM MessageTable WHERE message_id=?");
|
||||
query.bind_string(0, message_id.value);
|
||||
|
||||
SQLHeavy.QueryResult result = yield query.execute_async(cancellable);
|
||||
if (result.finished)
|
||||
return null;
|
||||
|
||||
Gee.List<int64?> list = new Gee.ArrayList<int64?>();
|
||||
do {
|
||||
list.add(result.fetch_int64(0));
|
||||
yield result.next_async(cancellable);
|
||||
} while (!result.finished);
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,15 +14,25 @@ public abstract class Geary.Sqlite.Row {
|
|||
}
|
||||
|
||||
public int fetch_int_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error {
|
||||
return result.fetch_int(result.field_index(table.get_field_name(col)));
|
||||
return result.fetch_int(field_index(result, col));
|
||||
}
|
||||
|
||||
public int64 fetch_int64_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error {
|
||||
return result.fetch_int64(result.field_index(table.get_field_name(col)));
|
||||
return result.fetch_int64(field_index(result, col));
|
||||
}
|
||||
|
||||
public string fetch_string_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error {
|
||||
return result.fetch_string(result.field_index(table.get_field_name(col)));
|
||||
return result.fetch_string(field_index(result, col));
|
||||
}
|
||||
|
||||
private int field_index(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error {
|
||||
try {
|
||||
return result.field_index(table.get_field_name(col));
|
||||
} catch (SQLHeavy.Error err) {
|
||||
debug("Bad column #%d in %s: %s", col, table.to_string(), err.message);
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,5 +22,9 @@ public abstract class Geary.Sqlite.Table {
|
|||
public string get_field_name(int col) throws SQLHeavy.Error {
|
||||
return table.field_name(col);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return table.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount {
|
||||
private MailDatabase db;
|
||||
private FolderTable folder_table;
|
||||
private MessageTable message_table;
|
||||
|
||||
public Account(Geary.Credentials cred) {
|
||||
try {
|
||||
|
|
@ -16,6 +17,11 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount {
|
|||
}
|
||||
|
||||
folder_table = db.get_folder_table();
|
||||
message_table = db.get_message_table();
|
||||
}
|
||||
|
||||
public Geary.Email.Field get_required_fields_for_writing() {
|
||||
return Geary.Email.Field.NONE;
|
||||
}
|
||||
|
||||
public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
|
||||
|
|
@ -62,5 +68,12 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount {
|
|||
Cancellable? cancellable = null) throws Error {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public async bool has_message_id_async(Geary.RFC822.MessageID message_id, out int count,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
count = yield message_table.search_message_id_count_async(message_id);
|
||||
|
||||
return (count > 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class Geary.Sqlite.Folder : Object, Geary.Folder {
|
||||
// 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.
|
||||
|
||||
public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder {
|
||||
private MailDatabase db;
|
||||
private FolderRow folder_row;
|
||||
private MessageTable message_table;
|
||||
private MessageLocationTable location_table;
|
||||
private ImapMessageLocationPropertiesTable imap_location_table;
|
||||
private string name;
|
||||
private bool opened = false;
|
||||
|
||||
internal Folder(MailDatabase db, FolderRow folder_row) throws Error {
|
||||
this.db = db;
|
||||
|
|
@ -23,6 +27,11 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder {
|
|||
imap_location_table = db.get_imap_message_location_table();
|
||||
}
|
||||
|
||||
private void check_open() throws Error {
|
||||
if (!opened)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not open", to_string());
|
||||
}
|
||||
|
||||
public string get_name() {
|
||||
return name;
|
||||
}
|
||||
|
|
@ -32,23 +41,46 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder {
|
|||
}
|
||||
|
||||
public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error {
|
||||
if (opened)
|
||||
throw new EngineError.ALREADY_OPEN("%s already open", to_string());
|
||||
|
||||
opened = true;
|
||||
notify_opened();
|
||||
}
|
||||
|
||||
public async void close_async(Cancellable? cancellable = null) throws Error {
|
||||
if (!opened)
|
||||
return;
|
||||
|
||||
opened = false;
|
||||
notify_closed(CloseReason.FOLDER_CLOSED);
|
||||
}
|
||||
|
||||
public int get_message_count() throws Error {
|
||||
public async int get_email_count(Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
// TODO
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async void create_email_async(Geary.Email email, Geary.Email.Field fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
int64 message_id = yield message_table.create_async(
|
||||
new MessageRow.from_email(message_table, email),
|
||||
cancellable);
|
||||
public async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location;
|
||||
|
||||
// See if it already exists; first by UID (which is only guaranteed to be unique in a folder,
|
||||
// not account-wide)
|
||||
int64 message_id;
|
||||
if (yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id, out message_id,
|
||||
cancellable)) {
|
||||
throw new EngineError.ALREADY_EXISTS("Email with UID %s already exists in %s",
|
||||
location.uid.to_string(), get_name());
|
||||
}
|
||||
|
||||
message_id = yield message_table.create_async(
|
||||
new MessageRow.from_email(message_table, email),
|
||||
cancellable);
|
||||
|
||||
MessageLocationRow location_row = new MessageLocationRow(location_table, Row.INVALID_ID,
|
||||
message_id, folder_row.id, location.position);
|
||||
int64 location_id = yield location_table.create_async(location_row, cancellable);
|
||||
|
|
@ -58,40 +90,54 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder {
|
|||
yield imap_location_table.create_async(imap_location_row, cancellable);
|
||||
}
|
||||
|
||||
public async Gee.List<Geary.Email>? list_email_async(int low, int count, Geary.Email.Field fields,
|
||||
Cancellable? cancellable) throws Error {
|
||||
public async Gee.List<Geary.Email>? list_email_async(int low, int count,
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable) throws Error {
|
||||
assert(low >= 1);
|
||||
assert(count >= 1);
|
||||
|
||||
// low is zero-based in the database.
|
||||
check_open();
|
||||
|
||||
Gee.List<MessageLocationRow>? list = yield location_table.list_async(folder_row.id, low,
|
||||
count, cancellable);
|
||||
|
||||
return yield list_email(list, fields, cancellable);
|
||||
return yield list_email(list, required_fields, cancellable);
|
||||
}
|
||||
|
||||
public async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position,
|
||||
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error {
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.List<MessageLocationRow>? list = yield location_table.list_sparse_async(folder_row.id,
|
||||
by_position, cancellable);
|
||||
|
||||
return yield list_email(list, fields, cancellable);
|
||||
return yield list_email(list, required_fields, cancellable);
|
||||
}
|
||||
|
||||
private async Gee.List<Geary.Email>? list_email(Gee.List<MessageLocationRow>? list,
|
||||
Geary.Email.Field fields, Cancellable? cancellable) throws Error {
|
||||
Geary.Email.Field required_fields, Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
if (list == null || list.size == 0)
|
||||
return null;
|
||||
|
||||
// TODO: As this loop involves multiple database operations to form an email, might make
|
||||
// sense in the future to launch each async method separately, putting the final results
|
||||
// together when all the information is fetched
|
||||
Gee.List<Geary.Email> emails = new Gee.ArrayList<Geary.Email>();
|
||||
foreach (MessageLocationRow location_row in list) {
|
||||
// fetch the IMAP message location properties that are associated with the generic
|
||||
// message location
|
||||
ImapMessageLocationPropertiesRow? imap_location_row = yield imap_location_table.fetch_async(
|
||||
location_row.id, cancellable);
|
||||
assert(imap_location_row != null);
|
||||
|
||||
// fetch the message itself
|
||||
MessageRow? message_row = yield message_table.fetch_async(location_row.message_id,
|
||||
fields, cancellable);
|
||||
required_fields, cancellable);
|
||||
assert(message_row != null);
|
||||
// only add to the list if the email contains all the required fields
|
||||
if (!message_row.fields.is_set(required_fields))
|
||||
continue;
|
||||
|
||||
emails.add(message_row.to_email(new Geary.Imap.EmailLocation(location_row.position,
|
||||
imap_location_row.uid)));
|
||||
|
|
@ -100,11 +146,12 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder {
|
|||
return (emails.size > 0) ? emails : null;
|
||||
}
|
||||
|
||||
public async Geary.Email fetch_email_async(int position, Geary.Email.Field fields,
|
||||
public async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
assert(position >= 1);
|
||||
|
||||
// num is zero-based in the database.
|
||||
check_open();
|
||||
|
||||
MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position,
|
||||
cancellable);
|
||||
if (location_row == null)
|
||||
|
|
@ -121,13 +168,143 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder {
|
|||
|
||||
assert(imap_location_row.location_id == location_row.id);
|
||||
|
||||
MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, fields,
|
||||
cancellable);
|
||||
MessageRow? message_row = yield message_table.fetch_async(location_row.message_id,
|
||||
required_fields, cancellable);
|
||||
if (message_row == null)
|
||||
throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name);
|
||||
|
||||
if (!message_row.fields.is_set(required_fields)) {
|
||||
throw new EngineError.INCOMPLETE_MESSAGE(
|
||||
"Message at position %d in folder %s only fulfills %Xh fields", position, to_string(),
|
||||
message_row.fields);
|
||||
}
|
||||
|
||||
return message_row.to_email(new Geary.Imap.EmailLocation(location_row.position,
|
||||
imap_location_row.uid));
|
||||
}
|
||||
|
||||
public async bool is_email_present_at(int position, out Geary.Email.Field available_fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
available_fields = Geary.Email.Field.NONE;
|
||||
|
||||
MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position,
|
||||
cancellable);
|
||||
if (location_row == null)
|
||||
return false;
|
||||
|
||||
return yield message_table.fetch_fields_async(location_row.message_id, out available_fields,
|
||||
cancellable);
|
||||
}
|
||||
|
||||
public async bool is_email_associated_async(Geary.Email email, Cancellable? cancellable = null)
|
||||
throws Error {
|
||||
check_open();
|
||||
|
||||
int64 message_id;
|
||||
return yield imap_location_table.search_uid_in_folder(
|
||||
((Geary.Imap.EmailLocation) email.location).uid, folder_row.id, out message_id,
|
||||
cancellable);
|
||||
}
|
||||
|
||||
public async void update_email_async(Geary.Email email, bool duplicate_okay,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location;
|
||||
|
||||
// See if the message can be identified in the folder (which both reveals association and
|
||||
// a message_id that can be used for a merge; note that this works without a Message-ID)
|
||||
int64 message_id;
|
||||
bool associated = yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id,
|
||||
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
|
||||
// a merge
|
||||
if (email.message_id == null) {
|
||||
if (!associated) {
|
||||
if (!duplicate_okay)
|
||||
throw new EngineError.INCOMPLETE_MESSAGE("No Message-ID");
|
||||
|
||||
yield create_email_async(email, cancellable);
|
||||
} else {
|
||||
yield merge_email_async(message_id, email, cancellable);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If not associated, find message with matching Message-ID
|
||||
if (!associated) {
|
||||
Gee.List<int64?>? list = yield message_table.search_message_id_async(email.message_id,
|
||||
cancellable);
|
||||
|
||||
// If none found, this operation is a create
|
||||
if (list == null || list.size == 0) {
|
||||
yield create_email_async(email, cancellable);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Too many found turns this operation into a create
|
||||
if (list.size != 1) {
|
||||
yield create_email_async(email, cancellable);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
message_id = list[0];
|
||||
}
|
||||
|
||||
// Found a message. If not associated with this folder, associate now.
|
||||
// TODO: Need to lock the database during this operation, as these steps should be atomic.
|
||||
if (!associated) {
|
||||
// see if an email exists at this position
|
||||
MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id,
|
||||
location.position);
|
||||
if (location_row != null) {
|
||||
throw new EngineError.ALREADY_EXISTS("Email already exists at position %d in %s",
|
||||
email.location.position, to_string());
|
||||
}
|
||||
|
||||
// insert email at supplied position
|
||||
location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id,
|
||||
folder_row.id, location.position);
|
||||
int64 location_id = yield location_table.create_async(location_row, cancellable);
|
||||
|
||||
// update position propeties
|
||||
ImapMessageLocationPropertiesRow imap_location_row = new ImapMessageLocationPropertiesRow(
|
||||
imap_location_table, Row.INVALID_ID, location_id, location.uid);
|
||||
yield imap_location_table.create_async(imap_location_row, cancellable);
|
||||
}
|
||||
|
||||
// Merge any new information with the existing message in the local store
|
||||
yield merge_email_async(message_id, email, cancellable);
|
||||
|
||||
// Done.
|
||||
}
|
||||
|
||||
// TODO: The database should be locked around this method, as it should be atomic.
|
||||
// TODO: Merge email properties
|
||||
private async void merge_email_async(int64 message_id, Geary.Email email,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
assert(message_id != Row.INVALID_ID);
|
||||
|
||||
// if nothing to merge, nothing to do
|
||||
if (email.fields == Geary.Email.Field.NONE)
|
||||
return;
|
||||
|
||||
MessageRow? message_row = yield message_table.fetch_async(message_id, email.fields,
|
||||
cancellable);
|
||||
assert(message_row != null);
|
||||
|
||||
message_row.merge_from_network(email);
|
||||
|
||||
// possible nothing has changed or been added
|
||||
if (message_row.fields != Geary.Email.Field.NONE)
|
||||
yield message_table.merge_async(message_row, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue