Commit 4ccabcbd authored by Jim Nelson's avatar Jim Nelson

Further work toward persisting messages locally (#3742).

This iteration now stores headers locally and fetches them first before going to the network.  Work done in the database to deal with IMAPisms.  More work on the GMime bindings (couple of mistakes in prior commit).
parent 2fbe93af
......@@ -15,7 +15,7 @@ ENGINE_SRC := \
src/engine/api/Account.vala \
src/engine/api/Email.vala \
src/engine/api/EmailProperties.vala \
src/engine/api/EmailOrdering.vala \
src/engine/api/EmailLocation.vala \
src/engine/api/Folder.vala \
src/engine/api/FolderProperties.vala \
src/engine/api/Credentials.vala \
......@@ -30,6 +30,8 @@ ENGINE_SRC := \
src/engine/sqlite/MessageTable.vala \
src/engine/sqlite/MessageLocationRow.vala \
src/engine/sqlite/MessageLocationTable.vala \
src/engine/sqlite/ImapMessageLocationPropertiesTable.vala \
src/engine/sqlite/ImapMessageLocationPropertiesRow.vala \
src/engine/sqlite/api/Account.vala \
src/engine/sqlite/api/Folder.vala \
src/engine/state/Machine.vala \
......@@ -70,10 +72,12 @@ ENGINE_SRC := \
src/engine/imap/decoders/SelectExamineResults.vala \
src/engine/imap/decoders/StatusResults.vala \
src/engine/imap/api/Account.vala \
src/engine/imap/api/EmailLocation.vala \
src/engine/imap/api/EmailProperties.vala \
src/engine/imap/api/Folder.vala \
src/engine/imap/api/FolderProperties.vala \
src/engine/rfc822/MailboxAddress.vala \
src/engine/rfc822/MailboxAddresses.vala \
src/engine/rfc822/MessageData.vala \
src/engine/util/Memory.vala \
src/engine/util/ReferenceSemantics.vala \
......
......@@ -50,7 +50,7 @@ CREATE TABLE MessageLocationTable (
id INTEGER PRIMARY KEY,
message_id INTEGER REFERENCES MessageTable ON DELETE CASCADE,
folder_id INTEGER REFERENCES FolderTable ON DELETE CASCADE,
ordering INTEGER
position INTEGER
);
CREATE INDEX MessageLocationTableMessageIDIndex ON MessageLocationTable(message_id);
......@@ -92,3 +92,15 @@ CREATE TABLE ImapMessagePropertiesTable (
CREATE INDEX ImapMessagePropertiesTableMessageIDIndex ON ImapMessagePropertiesTable(message_id);
--
-- ImapMessageLocationPropertiesTable
--
CREATE TABLE ImapMessageLocationPropertiesTable (
id INTEGER PRIMARY KEY,
location_id INTEGER UNIQUE REFERENCES MessageLocationTable ON DELETE CASCADE,
uid INTEGER
);
CREATE INDEX ImapMessageLocationPropertiesTableLocationIDIndex ON ImapMessageLocationPropertiesTable(location_id);
......@@ -68,14 +68,6 @@ public class MainWindow : Gtk.Window {
do_start.begin();
}
private void on_folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed) {
if (added != null) {
folder_list_store.add_folders(added);
debug("%d folders added", added.size);
}
}
private async void do_start() {
try {
// pull down the root-level folders
......@@ -196,10 +188,13 @@ public class MainWindow : Gtk.Window {
private async void do_select_folder(Geary.Folder folder) throws Error {
message_list_store.clear();
if (current_folder != null)
if (current_folder != null) {
current_folder.email_added_removed.disconnect(on_email_added_removed);
yield current_folder.close_async();
}
current_folder = folder;
current_folder.email_added_removed.connect(on_email_added_removed);
yield current_folder.open_async(true);
......@@ -207,7 +202,7 @@ public class MainWindow : Gtk.Window {
Geary.Email.Field.ENVELOPE);
if (email != null && email.size > 0) {
foreach (Geary.Email envelope in email)
message_list_store.append_header(envelope);
message_list_store.append_envelope(envelope);
}
}
......@@ -236,7 +231,8 @@ public class MainWindow : Gtk.Window {
return;
}
Geary.Email text = yield current_folder.fetch_email_async(email.msg_num, Geary.Email.Field.BODY);
Geary.Email text = yield current_folder.fetch_email_async(email.location.position,
Geary.Email.Field.BODY);
message_buffer.set_text(text.body.buffer.to_ascii_string());
}
......@@ -247,5 +243,20 @@ public class MainWindow : Gtk.Window {
debug("Unable to select message: %s", err.message);
}
}
private void on_folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed) {
if (added != null) {
folder_list_store.add_folders(added);
debug("%d folders added", added.size);
}
}
private void on_email_added_removed(Gee.List<Geary.Email>? added, Gee.List<Geary.Folder>? removed) {
if (added != null) {
foreach (Geary.Email email in added)
message_list_store.append_envelope(email);
}
}
}
......@@ -54,7 +54,8 @@ public class MessageListStore : Gtk.TreeStore {
set_column_types(Column.get_types());
}
public void append_header(Geary.Email envelope) {
// The Email should've been fetched with Geary.Email.Field.ENVELOPE, at least.
public void append_envelope(Geary.Email envelope) {
Gtk.TreeIter iter;
append(out iter, null);
......
......@@ -30,7 +30,7 @@ private class Geary.EngineFolder : Object, Geary.Folder {
return null;
}
public async void create_email_async(Geary.Email email, Geary.EmailOrdering ordering,
public async void create_email_async(Geary.Email email, Geary.Email.Field fields,
Cancellable? cancellable) throws Error {
throw new EngineError.READONLY("Engine currently read-only");
}
......@@ -57,9 +57,111 @@ private class Geary.EngineFolder : Object, Geary.Folder {
return 0;
}
public async Gee.List<Geary.Email> list_email_async(int low, int count, Geary.Email.Field fields,
public async Gee.List<Geary.Email>? list_email_async(int low, int count, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error {
return yield net_folder.list_email_async(low, count, fields, cancellable);
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);
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 (needed_by_position.length != 0)
background_update_email_list.begin(needed_by_position, fields, cancellable);
}
return local_list;
}
public async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position,
Geary.Email.Field 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);
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 (!found)
needed_by_position += position;
}
if (needed_by_position.length != 0)
background_update_email_list.begin(needed_by_position, fields, cancellable);
}
return local_list;
}
private async void background_update_email_list(int[] needed_by_position, Geary.Email.Field fields,
Cancellable? cancellable) {
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;
}
if (net_list != null && net_list.size == 0)
net_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;
}
}
}
}
public async Geary.Email fetch_email_async(int num, Geary.Email.Field fields,
......
......@@ -33,7 +33,7 @@ public class Geary.Email : Object {
}
}
public int msg_num { get; private set; }
public Geary.EmailLocation location { get; private set; }
// DATE
public Geary.RFC822.Date? date = null;
......@@ -64,8 +64,8 @@ public class Geary.Email : Object {
// PROPERTIES
public Geary.EmailProperties? properties = null;
public Email(int msg_num) {
this.msg_num = msg_num;
public Email(Geary.EmailLocation location) {
this.location = location;
}
public string to_string() {
......
......@@ -4,11 +4,11 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.EmailOrdering {
public int64 ordinal { get; private set; }
public class Geary.EmailLocation : Object {
public int position { get; private set; }
public EmailOrdering(int64 ordinal) {
this.ordinal = ordinal;
public EmailLocation(int position) {
this.position = position;
}
}
......@@ -15,6 +15,9 @@ public interface Geary.Folder : Object {
public signal void closed(CloseReason reason);
public signal void email_added_removed(Gee.List<Geary.Email>? added,
Gee.List<Geary.Email>? removed);
public signal void updated();
public virtual void notify_opened() {
......@@ -25,6 +28,11 @@ public interface Geary.Folder : Object {
closed(reason);
}
public virtual void notify_email_added_removed(Gee.List<Geary.Email>? added,
Gee.List<Geary.Email>? removed) {
email_added_removed(added, removed);
}
public virtual void notify_updated() {
updated();
}
......@@ -39,16 +47,25 @@ public interface Geary.Folder : Object {
public abstract int get_message_count() throws Error;
public abstract async void create_email_async(Geary.Email email, Geary.EmailOrdering ordering,
Cancellable? cancellable = null) throws Error;
public abstract async void create_email_async(Geary.Email email,
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error;
/**
* low is one-based.
*/
public abstract async Gee.List<Geary.Email> list_email_async(int low, int count,
public abstract async Gee.List<Geary.Email>? list_email_async(int low, int count,
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error;
public abstract async Geary.Email fetch_email_async(int msg_num, Geary.Email.Field fields,
/**
* 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;
/**
* position is one-based.
*/
public abstract async Geary.Email fetch_email_async(int position, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error;
}
......@@ -30,7 +30,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
uid_validity = context.uid_validity;
}
public async Gee.List<Geary.Email>? list_async(int low, int count, Geary.Email.Field fields,
public async Gee.List<Geary.Email>? list_set_async(MessageSet msg_set, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error {
if (context.is_closed())
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
......@@ -39,7 +39,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
throw new EngineError.BAD_PARAMETERS("No email fields specify for list");
CommandResponse resp = yield context.session.send_command_async(
new FetchCommand(context.session.generate_tag(), new MessageSet.range(low, count),
new FetchCommand(context.session.generate_tag(), msg_set,
fields_to_fetch_data_types(fields)), cancellable);
if (resp.status_response.status != Status.OK)
......@@ -49,12 +49,13 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
FetchResults[] results = FetchResults.decode(resp);
foreach (FetchResults res in results) {
Geary.Email email = new Geary.Email(res.msg_num);
// TODO: Add UID
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(res.msg_num, 0));
fetch_results_to_email(res, fields, email);
msgs.add(email);
}
return msgs;
return (msgs != null && msgs.size > 0) ? msgs : null;
}
public async Geary.Email fetch_async(int msg_num, Geary.Email.Field fields,
......@@ -78,7 +79,8 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
results[0].msg_num, msg_num);
}
Geary.Email email = new Geary.Email(msg_num);
// TODO: Add UID
Geary.Email email = new Geary.Email(new Geary.Imap.EmailLocation(results[0].msg_num, 0));
fetch_results_to_email(results[0], fields, email);
return email;
......
......@@ -28,12 +28,12 @@ public class Geary.Imap.MessageSet {
value = "%d:*".printf(low_msg_num);
}
public MessageSet.scattered(int[] msg_nums) {
value = build_scattered_range(msg_nums);
public MessageSet.sparse(int[] msg_nums) {
value = build_sparse_range(msg_nums);
}
public MessageSet.scattered_to_highest(int[] msg_nums) {
value = "%s:*".printf(build_scattered_range(msg_nums));
public MessageSet.sparse_to_highest(int[] msg_nums) {
value = "%s:*".printf(build_sparse_range(msg_nums));
}
public MessageSet.multirange(MessageSet[] msg_sets) {
......@@ -50,7 +50,7 @@ public class Geary.Imap.MessageSet {
value = builder.str;
}
public MessageSet.multiscattered(MessageSet[] msg_sets) {
public MessageSet.multisparse(MessageSet[] msg_sets) {
StringBuilder builder = new StringBuilder();
for (int ctr = 0; ctr < msg_sets.length; ctr++) {
unowned MessageSet msg_set = msg_sets[ctr];
......@@ -68,7 +68,9 @@ public class Geary.Imap.MessageSet {
value = custom;
}
private static string build_scattered_range(int[] msg_nums) {
// TODO: It would be more efficient to look for runs in the numbers and form the set specifier
// with them.
private static string build_sparse_range(int[] msg_nums) {
assert(msg_nums.length > 0);
StringBuilder builder = new StringBuilder();
......
/* 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 class Geary.Imap.EmailLocation : Geary.EmailLocation {
public int64 uid { get; private set; }
public EmailLocation(int position, int64 uid) {
base (position);
this.uid = uid;
}
}
......@@ -57,25 +57,33 @@ public class Geary.Imap.Folder : Object, Geary.Folder {
return mailbox.count;
}
public async void create_email_async(Geary.Email email, Geary.EmailOrdering ordring,
public async void create_email_async(Geary.Email email, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error {
throw new EngineError.READONLY("IMAP currently read-only");
}
public async Gee.List<Geary.Email> list_email_async(int low, int count, Geary.Email.Field fields,
public async Gee.List<Geary.Email>? list_email_async(int low, int count, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
return yield mailbox.list_async(low, count, fields, cancellable);
return yield mailbox.list_set_async(new MessageSet.range(low, count), fields, cancellable);
}
public async Geary.Email fetch_email_async(int msg_num, Geary.Email.Field fields,
public async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position,
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
return yield mailbox.list_set_async(new MessageSet.sparse(by_position), fields, cancellable);
}
public async Geary.Email fetch_email_async(int position, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
return yield mailbox.fetch_async(msg_num, fields, cancellable);
return yield mailbox.fetch_async(position, fields, cancellable);
}
public string to_string() {
......
......@@ -138,7 +138,7 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder {
StringParameter mailbox = fields.get_as_string(2);
StringParameter domain = fields.get_as_string(3);
Geary.RFC822.MailboxAddress addr = new Geary.RFC822.MailboxAddress(
Geary.RFC822.MailboxAddress addr = new Geary.RFC822.MailboxAddress.imap(
(name != null) ? name.nullable_value : null,
(source_route != null) ? source_route.nullable_value : null,
mailbox.value,
......
......@@ -11,7 +11,20 @@ public class Geary.RFC822.MailboxAddress {
public string domain { get; private set; }
public string address { get; private set; }
public MailboxAddress(string? name, string? source_route, string mailbox, string domain) {
public MailboxAddress(string? name, string address) {
this.name = name;
this.address = address;
source_route = null;
int atsign = address.index_of_char('@');
if (atsign > 0) {
mailbox = address.slice(0, atsign);
domain = address.slice(atsign + 1, address.length);
}
}
public MailboxAddress.imap(string? name, string? source_route, string mailbox, string domain) {
this.name = name;
this.source_route = source_route;
this.mailbox = mailbox;
......@@ -36,6 +49,10 @@ public class Geary.RFC822.MailboxAddress {
return name ?? mailbox;
}
public string to_rfc822_string() {
return get_full_address();
}
public string to_string() {
return get_full_address();
}
......
/* 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 class Geary.RFC822.MailboxAddresses : Geary.Common.MessageData, Geary.RFC822.MessageData {
public int size { get { return addrs.size; } }
private Gee.List<MailboxAddress> addrs = new Gee.ArrayList<MailboxAddress>();
public MailboxAddresses(Gee.Collection<MailboxAddress> addrs) {
this.addrs.add_all(addrs);
}
public MailboxAddresses.from_rfc822_string(string rfc822) {
InternetAddressList addrlist = InternetAddressList.parse_string(rfc822);
int length = addrlist.length();
for (int ctr = 0; ctr < length; ctr++) {
InternetAddress? addr = addrlist.get_address(ctr);
// TODO: Handle group lists
InternetAddressMailbox? mbox_addr = addr as InternetAddressMailbox;
if (mbox_addr == null)
continue;
addrs.add(new MailboxAddress(mbox_addr.get_name(), mbox_addr.get_addr()));
}
}
public MailboxAddress? get(int index) {
return addrs.get(index);
}
public Gee.Iterator<MailboxAddress> iterator() {
return addrs.iterator();
}
public Gee.List<MailboxAddress> get_all() {
return addrs.read_only_view;
}
public string to_rfc822_string() {
switch (addrs.size) {
case 0:
return "";
case 1:
return addrs[0].to_rfc822_string();
default:
StringBuilder builder = new StringBuilder();
foreach (MailboxAddress addr in addrs) {
if (!String.is_empty(builder.str))
builder.append(", ");
builder.append(addr.to_rfc822_string());
}
return builder.str;
}
}
public override string to_string() {
switch (addrs.size) {
case 0:
return "(no addresses)";
case 1:
return addrs[0].to_string();
default:
StringBuilder builder = new StringBuilder();
foreach (MailboxAddress addr in addrs) {
if (!String.is_empty(builder.str))
builder.append(", ");
builder.append(addr.to_string());
}
return builder.str;
}
}
}
......@@ -50,49 +50,6 @@ public class Geary.RFC822.Subject : Geary.Common.StringMessageData, Geary.RFC822
}
}
public class Geary.RFC822.MailboxAddresses : Geary.Common.MessageData, Geary.RFC822.MessageData {
public int size { get { return addrs.size; } }
private Gee.List<MailboxAddress> addrs = new Gee.ArrayList<MailboxAddress>();
public MailboxAddresses(Gee.Collection<MailboxAddress> addrs) {
this.addrs.add_all(addrs);
}
public MailboxAddress? get(int index) {
return addrs.get(index);
}
public Gee.Iterator<MailboxAddress> iterator() {
return addrs.iterator();
}
public Gee.List<MailboxAddress> get_all() {
return addrs.read_only_view;
}
public override string to_string() {
switch (addrs.size) {
case 0:
return "(no addresses)";
case 1:
return addrs[0].to_string();
default:
StringBuilder builder = new StringBuilder();
foreach (MailboxAddress addr in addrs) {
if (!String.is_empty(builder.str))
builder.append(", ");
builder.append(addr.to_string());
}
return builder.str;
}
}
}
public class Geary.RFC822.Header : Geary.Common.BlockMessageData, Geary.RFC822.MessageData {
public Header(Geary.Memory.AbstractBuffer buffer) {
base ("RFC822.Header", buffer);
......
/* 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 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 ImapMessageLocationPropertiesRow(ImapMessageLocationPropertiesTable table, int64 id,
int64 location_id, int64 uid) {
base (table);
this.id = id;
this.location_id = location_id;
this.uid = uid;
}
}
/* 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 class Geary.Sqlite.ImapMessageLocationPropertiesTable : Geary.Sqlite.Table {
// This *must* be in the same order as the schema.
public enum Column {
ID,
LOCATION_ID,
UID
}
public ImapMessageLocationPropertiesTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) {
base (gdb, table);
}
public async int64 create_async(ImapMessageLocationPropertiesRow row,
Cancellable? cancellable = null) throws Error {
SQLHeavy.Query query = db.prepare(
"INSERT INTO ImapMessageLocationPropertiesTable (location_id, uid) VALUES (?, ?)");