Fetches only a small portion of the message for previews: Closes #4254, Closes #3799

Before we were fetching the entire message body (including attachments) to get the
preview text.  This patch now offers the ability to fetch a small (128 byte) preview
of the email.

Also, since this ticket is about speeding up performance, I've introduced NonblockingBatch,
which allows for multiple async operations to be executed in parallel easily.  I've added
its use in a few places to speed up operations, including one that was causing the lag
in #3799, which is why this commit closes that ticket.
This commit is contained in:
Jim Nelson 2011-11-16 18:14:17 -08:00
parent 782e6fa5f5
commit 18716ae6ce
22 changed files with 672 additions and 118 deletions

View file

@ -39,7 +39,9 @@ CREATE TABLE MessageTable (
header TEXT, header TEXT,
body TEXT body TEXT,
preview TEXT
); );
CREATE INDEX MessageTableMessageIDIndex ON MessageTable(message_id); CREATE INDEX MessageTableMessageIDIndex ON MessageTable(message_id);

View file

@ -8,6 +8,58 @@ public class MainWindow : Gtk.Window {
private const int MESSAGE_LIST_WIDTH = 250; private const int MESSAGE_LIST_WIDTH = 250;
private const int FETCH_EMAIL_CHUNK_COUNT = 50; private const int FETCH_EMAIL_CHUNK_COUNT = 50;
private class FetchPreviewOperation : Geary.NonblockingBatchOperation {
public MainWindow owner;
public Geary.Folder folder;
public Geary.EmailIdentifier email_id;
public int index;
public FetchPreviewOperation(MainWindow owner, Geary.Folder folder,
Geary.EmailIdentifier email_id, int index) {
this.owner = owner;
this.folder = folder;
this.email_id = email_id;
this.index = index;
}
public override async Object? execute(Cancellable? cancellable) throws Error {
Geary.Email? preview = yield folder.fetch_email_async(email_id,
MessageListStore.WITH_PREVIEW_FIELDS, cancellable);
owner.message_list_store.set_preview_at_index(index, preview);
return null;
}
}
private class ListFoldersOperation : Geary.NonblockingBatchOperation {
public Geary.Account account;
public Geary.FolderPath path;
public ListFoldersOperation(Geary.Account account, Geary.FolderPath path) {
this.account = account;
this.path = path;
}
public override async Object? execute(Cancellable? cancellable) throws Error {
return yield account.list_folders_async(path, cancellable);
}
}
private class FetchSpecialFolderOperation : Geary.NonblockingBatchOperation {
public Geary.Account account;
public Geary.SpecialFolder special_folder;
public FetchSpecialFolderOperation(Geary.Account account, Geary.SpecialFolder special_folder) {
this.account = account;
this.special_folder = special_folder;
}
public override async Object? execute(Cancellable? cancellable) throws Error {
return yield account.fetch_folder_async(special_folder.path);
}
}
private MainToolbar main_toolbar; private MainToolbar main_toolbar;
private MessageListStore message_list_store = new MessageListStore(); private MessageListStore message_list_store = new MessageListStore();
private MessageListView message_list_view; private MessageListView message_list_view;
@ -60,9 +112,24 @@ public class MainWindow : Gtk.Window {
// add all the special folders, which are assumed to always exist // add all the special folders, which are assumed to always exist
Geary.SpecialFolderMap? special_folders = account.get_special_folder_map(); Geary.SpecialFolderMap? special_folders = account.get_special_folder_map();
if (special_folders != null) { if (special_folders != null) {
foreach (Geary.SpecialFolder special_folder in special_folders.get_all()) { Geary.NonblockingBatch batch = new Geary.NonblockingBatch();
Geary.Folder folder = yield account.fetch_folder_async(special_folder.path); foreach (Geary.SpecialFolder special_folder in special_folders.get_all())
folder_list_store.add_special_folder(special_folder, folder); batch.add(new FetchSpecialFolderOperation(account, special_folder));
debug("Listing special folders");
yield batch.execute_all();
debug("Completed list of special folders");
foreach (int id in batch.get_ids()) {
FetchSpecialFolderOperation op = (FetchSpecialFolderOperation) batch.get_operation(id);
try {
Geary.Folder folder = (Geary.Folder) batch.get_result(id);
folder_list_store.add_special_folder(op.special_folder, folder);
} catch (Error inner_error) {
message("Unable to fetch special folder %s: %s", op.special_folder.path.to_string(),
inner_error.message);
}
} }
// If inbox is specified, select that // If inbox is specified, select that
@ -78,7 +145,7 @@ public class MainWindow : Gtk.Window {
else else
debug("no folders"); debug("no folders");
} catch (Error err) { } catch (Error err) {
warning("%s", err.message); message("%s", err.message);
} }
} }
@ -268,18 +335,21 @@ public class MainWindow : Gtk.Window {
} }
private async void do_fetch_previews(Cancellable? cancellable) throws Error { private async void do_fetch_previews(Cancellable? cancellable) throws Error {
Geary.NonblockingBatch batch = new Geary.NonblockingBatch();
int count = message_list_store.get_count(); int count = message_list_store.get_count();
for (int ctr = 0; ctr < count; ctr++) { for (int ctr = 0; ctr < count; ctr++) {
Geary.Email? email = message_list_store.get_newest_message_at_index(ctr); Geary.Email? email = message_list_store.get_newest_message_at_index(ctr);
if (email == null) if (email != null)
continue; batch.add(new FetchPreviewOperation(this, current_folder, email.id, ctr));
Geary.Email? body = yield current_folder.fetch_email_async(email.id,
Geary.Email.Field.HEADER | Geary.Email.Field.BODY | Geary.Email.Field.ENVELOPE |
Geary.Email.Field.PROPERTIES, cancellable);
message_list_store.set_preview_at_index(ctr, body);
} }
debug("Fetching %d previews", count);
yield batch.execute_all(cancellable);
debug("Completed fetching %d previews", count);
batch.throw_first_exception();
// with all the previews fetched, now go back and do a full list (if required) // with all the previews fetched, now go back and do a full list (if required)
if (second_list_pass_required) { if (second_list_pass_required) {
second_list_pass_required = false; second_list_pass_required = false;
@ -358,14 +428,28 @@ public class MainWindow : Gtk.Window {
} }
private async void search_folders_for_children(Gee.Collection<Geary.Folder> folders) { private async void search_folders_for_children(Gee.Collection<Geary.Folder> folders) {
Geary.NonblockingBatch batch = new Geary.NonblockingBatch();
foreach (Geary.Folder folder in folders)
batch.add(new ListFoldersOperation(account, folder.get_path()));
debug("Listing folder children");
try {
yield batch.execute_all();
} catch (Error err) {
debug("Unable to execute batch: %s", err.message);
return;
}
debug("Completed listing folder children");
Gee.ArrayList<Geary.Folder> accumulator = new Gee.ArrayList<Geary.Folder>(); Gee.ArrayList<Geary.Folder> accumulator = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.Folder folder in folders) { foreach (int id in batch.get_ids()) {
ListFoldersOperation op = (ListFoldersOperation) batch.get_operation(id);
try { try {
Gee.Collection<Geary.Folder> children = yield account.list_folders_async( Gee.Collection<Geary.Folder> children = (Gee.Collection<Geary.Folder>) batch.get_result(id);
folder.get_path(), null);
accumulator.add_all(children); accumulator.add_all(children);
} catch (Error err) { } catch (Error err2) {
debug("Unable to list children of %s: %s", folder.to_string(), err.message); debug("Unable to list children of %s: %s", op.path.to_string(), err2.message);
} }
} }

View file

@ -42,21 +42,14 @@ public class FormattedMessageData : Object {
public FormattedMessageData.from_email(Geary.Email email, int num_emails) { public FormattedMessageData.from_email(Geary.Email email, int num_emails) {
assert(email.fields.fulfills(MessageListStore.REQUIRED_FIELDS)); assert(email.fields.fulfills(MessageListStore.REQUIRED_FIELDS));
StringBuilder builder = new StringBuilder(); string preview = "";
if (email.fields.fulfills(Geary.Email.Field.BODY)) { if (email.fields.fulfills(Geary.Email.Field.PREVIEW) && email.preview != null)
try { preview = email.preview.buffer.to_utf8();
Geary.Memory.AbstractBuffer buffer = email.get_message().
get_first_mime_part_of_content_type("text/plain");
builder.append(buffer.to_utf8());
} catch (Error e) {
debug("Error displaying message body: %s".printf(e.message));
}
}
string from = (email.from != null && email.from.size > 0) ? email.from[0].get_short_address() : ""; string from = (email.from != null && email.from.size > 0) ? email.from[0].get_short_address() : "";
this(email.properties.is_unread(), Date.pretty_print(email.date.value), this(email.properties.is_unread(), Date.pretty_print(email.date.value),
from, email.subject.value, Geary.String.reduce_whitespace(builder.str), num_emails); from, email.subject.value, Geary.String.reduce_whitespace(preview), num_emails);
this.email = email; this.email = email;
} }

View file

@ -5,10 +5,12 @@
*/ */
public class MessageListStore : Gtk.TreeStore { public class MessageListStore : Gtk.TreeStore {
public const Geary.Email.Field REQUIRED_FIELDS = public const Geary.Email.Field REQUIRED_FIELDS =
Geary.Email.Field.ENVELOPE | Geary.Email.Field.PROPERTIES; Geary.Email.Field.ENVELOPE | Geary.Email.Field.PROPERTIES;
public const Geary.Email.Field WITH_PREVIEW_FIELDS =
Geary.Email.Field.ENVELOPE | Geary.Email.Field.PROPERTIES | Geary.Email.Field.PREVIEW;
public enum Column { public enum Column {
MESSAGE_DATA, MESSAGE_DATA,
MESSAGE_OBJECT, MESSAGE_OBJECT,

View file

@ -94,6 +94,7 @@ class ImapConsole : Gtk.Window {
"gmail", "gmail",
"keepalive", "keepalive",
"status", "status",
"preview",
"close" "close"
}; };
@ -196,6 +197,10 @@ class ImapConsole : Gtk.Window {
folder_status(cmd, args); folder_status(cmd, args);
break; break;
case "preview":
preview(cmd, args);
break;
default: default:
status("Unknown command \"%s\"".printf(cmd)); status("Unknown command \"%s\"".printf(cmd));
break; break;
@ -407,7 +412,7 @@ class ImapConsole : Gtk.Window {
status("Fetching fields %s".printf(args[0])); status("Fetching fields %s".printf(args[0]));
Geary.Imap.FetchBodyDataType fields = new Geary.Imap.FetchBodyDataType( Geary.Imap.FetchBodyDataType fields = new Geary.Imap.FetchBodyDataType(
Geary.Imap.FetchBodyDataType.SectionPart.HEADER_FIELDS, args[1:args.length]); Geary.Imap.FetchBodyDataType.SectionPart.HEADER_FIELDS, null, -1, -1, args[1:args.length]);
Gee.List<Geary.Imap.FetchBodyDataType> list = new Gee.ArrayList<Geary.Imap.FetchBodyDataType>(); Gee.List<Geary.Imap.FetchBodyDataType> list = new Gee.ArrayList<Geary.Imap.FetchBodyDataType>();
list.add(fields); list.add(fields);
@ -463,6 +468,31 @@ class ImapConsole : Gtk.Window {
} }
} }
private void preview(string cmd, string[] args) throws Error {
check_min_connected(cmd, args, 1, "<message-span>");
status("Preview %s".printf(args[0]));
Geary.Imap.FetchBodyDataType preview_data_type = new Geary.Imap.FetchBodyDataType.peek(
Geary.Imap.FetchBodyDataType.SectionPart.NONE, { 1 }, 0, Geary.Email.MAX_PREVIEW_BYTES,
null);
Gee.ArrayList<Geary.Imap.FetchBodyDataType> list = new Gee.ArrayList<Geary.Imap.FetchBodyDataType>();
list.add(preview_data_type);
cx.send_async.begin(new Geary.Imap.FetchCommand(
new Geary.Imap.MessageSet.custom(args[0]), null, list), null, on_preview_completed);
}
private void on_preview_completed(Object? source, AsyncResult result) {
try {
cx.send_async.end(result);
status("Preview fetched");
} catch (Error err) {
exception(err);
}
}
private void quit(string cmd, string[] args) throws Error { private void quit(string cmd, string[] args) throws Error {
Gtk.main_quit(); Gtk.main_quit();
} }

View file

@ -5,6 +5,10 @@
*/ */
public class Geary.Email : Object { public class Geary.Email : Object {
// This value is not persisted, but it does represent the expected max size of the preview
// when returned.
public const int MAX_PREVIEW_BYTES = 128;
// THESE VALUES ARE PERSISTED. Change them only if you know what you're doing. // THESE VALUES ARE PERSISTED. Change them only if you know what you're doing.
public enum Field { public enum Field {
NONE = 0, NONE = 0,
@ -16,6 +20,8 @@ public class Geary.Email : Object {
HEADER = 1 << 5, HEADER = 1 << 5,
BODY = 1 << 6, BODY = 1 << 6,
PROPERTIES = 1 << 7, PROPERTIES = 1 << 7,
PREVIEW = 1 << 8,
ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT, ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT,
ALL = 0xFFFFFFFF; ALL = 0xFFFFFFFF;
@ -28,7 +34,8 @@ public class Geary.Email : Object {
SUBJECT, SUBJECT,
HEADER, HEADER,
BODY, BODY,
PROPERTIES PROPERTIES,
PREVIEW
}; };
} }
@ -108,6 +115,9 @@ public class Geary.Email : Object {
// PROPERTIES // PROPERTIES
public Geary.EmailProperties? properties { get; private set; default = null; } public Geary.EmailProperties? properties { get; private set; default = null; }
// PREVIEW
public RFC822.Text? preview { get; private set; default = null; }
public Geary.Email.Field fields { get; private set; default = Field.NONE; } public Geary.Email.Field fields { get; private set; default = Field.NONE; }
private Geary.RFC822.Message? message = null; private Geary.RFC822.Message? message = null;
@ -188,6 +198,12 @@ public class Geary.Email : Object {
fields |= Field.PROPERTIES; fields |= Field.PROPERTIES;
} }
public void set_message_preview(Geary.RFC822.Text preview) {
this.preview = preview;
fields |= Field.PREVIEW;
}
/** /**
* This method requires Geary.Email.Field.HEADER and Geary.Email.Field.BODY be present. * This method requires Geary.Email.Field.HEADER and Geary.Email.Field.BODY be present.
* If not, EngineError.INCOMPLETE_MESSAGE is thrown. * If not, EngineError.INCOMPLETE_MESSAGE is thrown.

View file

@ -11,6 +11,7 @@ public errordomain Geary.EngineError {
NOT_FOUND, NOT_FOUND,
READONLY, READONLY,
BAD_PARAMETERS, BAD_PARAMETERS,
BAD_RESPONSE,
INCOMPLETE_MESSAGE, INCOMPLETE_MESSAGE,
SERVER_UNAVAILABLE, SERVER_UNAVAILABLE,
CLOSED CLOSED

View file

@ -10,6 +10,23 @@ private class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
public const string INBOX_NAME = "INBOX"; public const string INBOX_NAME = "INBOX";
public const string ASSUMED_SEPARATOR = "/"; public const string ASSUMED_SEPARATOR = "/";
private class StatusOperation : Geary.NonblockingBatchOperation {
public ClientSessionManager session_mgr;
public MailboxInformation mbox;
public Geary.FolderPath path;
public StatusOperation(ClientSessionManager session_mgr, MailboxInformation mbox,
Geary.FolderPath path) {
this.session_mgr = session_mgr;
this.mbox = mbox;
this.path = path;
}
public override async Object? execute(Cancellable? cancellable) throws Error {
return yield session_mgr.status_async(path.get_fullpath(), StatusDataType.all(), cancellable);
}
}
private Geary.Credentials cred; private Geary.Credentials cred;
private ClientSessionManager session_mgr; private ClientSessionManager session_mgr;
private Geary.Smtp.ClientSession smtp; private Geary.Smtp.ClientSession smtp;
@ -47,6 +64,8 @@ private class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
} }
Gee.Collection<Geary.Folder> folders = new Gee.ArrayList<Geary.Folder>(); Gee.Collection<Geary.Folder> folders = new Gee.ArrayList<Geary.Folder>();
Geary.NonblockingBatch batch = new Geary.NonblockingBatch();
foreach (MailboxInformation mbox in mboxes) { foreach (MailboxInformation mbox in mboxes) {
Geary.FolderPath path = process_path(processed, mbox.name, mbox.delim); Geary.FolderPath path = process_path(processed, mbox.name, mbox.delim);
@ -55,17 +74,22 @@ private class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
if (processed == null) if (processed == null)
delims.set(path.get_root().basename, mbox.delim); delims.set(path.get_root().basename, mbox.delim);
StatusResults? status = null; if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT))
if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT)) { batch.add(new StatusOperation(session_mgr, mbox, path));
try { else
status = yield session_mgr.status_async(path.get_fullpath(), folders.add(new Geary.Imap.Folder(session_mgr, path, null, mbox));
StatusDataType.all(), cancellable); }
} catch (Error status_err) {
message("Unable to fetch status for %s: %s", path.to_string(), status_err.message); yield batch.execute_all(cancellable);
}
foreach (int id in batch.get_ids()) {
StatusOperation op = (StatusOperation) batch.get_operation(id);
try {
folders.add(new Geary.Imap.Folder(session_mgr, op.path,
(StatusResults?) batch.get_result(id), op.mbox));
} catch (Error status_err) {
message("Unable to fetch status for %s: %s", op.path.to_string(), status_err.message);
} }
folders.add(new Geary.Imap.Folder(session_mgr, path, status, mbox));
} }
return folders; return folders;

View file

@ -120,7 +120,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
normalize_span_specifiers(ref low, ref count, mailbox.exists); normalize_span_specifiers(ref low, ref count, mailbox.exists);
return yield mailbox.list_set_async(this, new MessageSet.range(low, count), fields, cancellable); return yield mailbox.list_set_async(new MessageSet.range(low, count), fields, cancellable);
} }
public override async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position, public override async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position,
@ -129,7 +129,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
if (mailbox == null) if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
return yield mailbox.list_set_async(this, new MessageSet.sparse(by_position), fields, cancellable); return yield mailbox.list_set_async(new MessageSet.sparse(by_position), fields, cancellable);
} }
public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier email_id, public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier email_id,
@ -160,7 +160,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
msg_set = new MessageSet.uid(uid); msg_set = new MessageSet.uid(uid);
} }
return yield mailbox.list_set_async(this, msg_set, fields, cancellable); return yield mailbox.list_set_async(msg_set, fields, cancellable);
} }
public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id, public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
@ -168,9 +168,20 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
if (mailbox == null) if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
// TODO: If position out of range, throw EngineError.NOT_FOUND Gee.List<Geary.Email>? list = yield mailbox.list_set_async(
new MessageSet.uid(((Imap.EmailIdentifier) id).uid), fields, cancellable);
return yield mailbox.fetch_async(this, ((Imap.EmailIdentifier) id).uid, fields, cancellable); if (list == null || list.size == 0) {
throw new EngineError.NOT_FOUND("Unable to fetch email %s from %s", id.to_string(),
to_string());
}
if (list.size != 1) {
throw new EngineError.BAD_RESPONSE("Too many responses (%d) from %s when fetching %s",
list.size, to_string(), id.to_string());
}
return list[0];
} }
public override async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null) public override async void remove_email_async(Geary.EmailIdentifier email_id, Cancellable? cancellable = null)

View file

@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution. * (version 2.1 or later). See the COPYING file in this distribution.
*/ */
public class Geary.Imap.CommandResponse { public class Geary.Imap.CommandResponse : Object {
public Gee.List<ServerData> server_data { get; private set; } public Gee.List<ServerData> server_data { get; private set; }
public StatusResponse? status_response { get; private set; } public StatusResponse? status_response { get; private set; }

View file

@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution. * (version 2.1 or later). See the COPYING file in this distribution.
*/ */
public abstract class Geary.Imap.CommandResults { public abstract class Geary.Imap.CommandResults : Object {
public StatusResponse status_response { get; private set; } public StatusResponse status_response { get; private set; }
public CommandResults(StatusResponse status_response) { public CommandResults(StatusResponse status_response) {

View file

@ -85,11 +85,11 @@ public class Geary.Imap.FetchResults : Geary.Imap.CommandResults {
return map.keys; return map.keys;
} }
private void set_data(FetchDataType data_item, MessageData primitive) { private new void set_data(FetchDataType data_item, MessageData primitive) {
map.set(data_item, primitive); map.set(data_item, primitive);
} }
public MessageData? get_data(FetchDataType data_item) { public new MessageData? get_data(FetchDataType data_item) {
return map.get(data_item); return map.get(data_item);
} }

View file

@ -44,25 +44,47 @@ public class Geary.Imap.FetchBodyDataType {
} }
private SectionPart section_part; private SectionPart section_part;
private int[]? part_number;
private int partial_start;
private int partial_count;
private string[]? field_names; private string[]? field_names;
private bool is_peek; private bool is_peek;
/** /**
* See RFC-3501 6.4.5 for some light beach reading on how the FETCH body data specifier is formed.
*
* A fully-qualified specifier looks something like this:
*
* BODY[part_number.section_part]<subset_start.subset_count>
*
* or, when headers are specified:
*
* BODY[part_number.section_part (header_fields)]<subset_start.subset_count>
*
* Note that Gmail apparently doesn't like BODY[1.TEXT] and instead must be specified with
* BODY[1].
*
* Set part_number to null to ignore. Set partial_start less than zero to ignore.
* partial_count must be greater than zero if partial_start is greater than zero.
*
* field_names are required for SectionPart.HEADER_FIELDS and SectionPart.HEADER_FIELDS_NOT * field_names are required for SectionPart.HEADER_FIELDS and SectionPart.HEADER_FIELDS_NOT
* and must be null for all other SectionParts. * and must be null for all other SectionParts.
*/ */
public FetchBodyDataType(SectionPart section_part, string[]? field_names) { public FetchBodyDataType(SectionPart section_part, int[]? part_number, int partial_start,
init(section_part, field_names, false); int partial_count, string[]? field_names) {
init(section_part, part_number, partial_start, partial_count, field_names, false);
} }
/** /**
* Like FetchBodyDataType, but the /seen flag will not be set when used on a message. * Like FetchBodyDataType, but the /seen flag will not be set when used on a message.
*/ */
public FetchBodyDataType.peek(SectionPart section_part, string[]? field_names) { public FetchBodyDataType.peek(SectionPart section_part, int[]? part_number, int partial_start,
init(section_part, field_names, true); int partial_count, string[]? field_names) {
init(section_part, part_number, partial_start, partial_count, field_names, true);
} }
private void init(SectionPart section_part, string[]? field_names, bool is_peek) { private void init(SectionPart section_part, int[]? part_number, int partial_start, int partial_count,
string[]? field_names, bool is_peek) {
switch (section_part) { switch (section_part) {
case SectionPart.HEADER_FIELDS: case SectionPart.HEADER_FIELDS:
case SectionPart.HEADER_FIELDS_NOT: case SectionPart.HEADER_FIELDS_NOT:
@ -74,7 +96,13 @@ public class Geary.Imap.FetchBodyDataType {
break; break;
} }
if (partial_start >= 0)
assert(partial_count > 0);
this.section_part = section_part; this.section_part = section_part;
this.part_number = part_number;
this.partial_start = partial_start;
this.partial_count = partial_count;
this.field_names = field_names; this.field_names = field_names;
this.is_peek = is_peek; this.is_peek = is_peek;
} }
@ -89,6 +117,25 @@ public class Geary.Imap.FetchBodyDataType {
return new UnquotedStringParameter(serialize()); return new UnquotedStringParameter(serialize());
} }
private string serialize_part_number() {
if (part_number == null || part_number.length == 0)
return "";
StringBuilder builder = new StringBuilder();
foreach (int part in part_number) {
if (builder.len > 0)
builder.append_c('.');
builder.append_printf("%d", part);
}
// if there's a SectionPart that follows, append a period as a separator
if (section_part != SectionPart.NONE)
builder.append_c('.');
return builder.str;
}
private string serialize_field_names() { private string serialize_field_names() {
if (field_names == null || field_names.length == 0) if (field_names == null || field_names.length == 0)
return ""; return "";
@ -105,6 +152,10 @@ public class Geary.Imap.FetchBodyDataType {
return builder.str; return builder.str;
} }
private string serialize_partial() {
return (partial_start < 0) ? "" : "<%d.%d>".printf(partial_start, partial_count);
}
public static bool is_fetch_body(StringParameter items) { public static bool is_fetch_body(StringParameter items) {
string strd = items.value.down(); string strd = items.value.down();
@ -112,8 +163,11 @@ public class Geary.Imap.FetchBodyDataType {
} }
public string to_string() { public string to_string() {
return (!is_peek ? "body[%s%s]" : "body.peek[%s%s]").printf(section_part.serialize(), return (!is_peek ? "body[%s%s%s]%s" : "body.peek[%s%s%s]%s").printf(
serialize_field_names()); serialize_part_number(),
section_part.serialize(),
serialize_field_names(),
serialize_partial());
} }
} }

View file

@ -4,7 +4,6 @@
* (version 2.1 or later). See the COPYING file in this distribution. * (version 2.1 or later). See the COPYING file in this distribution.
*/ */
// TODO: Support body[section]<partial> and body.peek[section]<partial> forms
public enum Geary.Imap.FetchDataType { public enum Geary.Imap.FetchDataType {
UID, UID,
FLAGS, FLAGS,

View file

@ -73,6 +73,11 @@ public class Geary.Imap.StringParameter : Geary.Imap.Parameter {
return long.parse(value).clamp(clamp_min, clamp_max); return long.parse(value).clamp(clamp_min, clamp_max);
} }
// TODO: This does not check that the value is a properly-formed int64.
public int64 as_int64(int64 clamp_min = int64.MIN, int64 clamp_max = int64.MAX) throws ImapError {
return int64.parse(value).clamp(clamp_min, clamp_max);
}
public override string to_string() { public override string to_string() {
return value; return value;
} }

View file

@ -5,8 +5,8 @@
*/ */
public class Geary.Imap.ClientSessionManager { public class Geary.Imap.ClientSessionManager {
public const int MIN_POOL_SIZE = 2; private const int MIN_POOL_SIZE = 2;
public const int SELECTED_KEEPALIVE_SEC = 5; private const int SELECTED_KEEPALIVE_SEC = 60;
private Endpoint endpoint; private Endpoint endpoint;
private Credentials credentials; private Credentials credentials;

View file

@ -5,6 +5,20 @@
*/ */
public class Geary.Imap.Mailbox : Geary.SmartReference { public class Geary.Imap.Mailbox : Geary.SmartReference {
private class MailboxOperation : NonblockingBatchOperation {
public SelectedContext context;
public Command cmd;
public MailboxOperation(SelectedContext context, Command cmd) {
this.context = context;
this.cmd = cmd;
}
public override async Object? execute(Cancellable? cancellable) throws Error {
return yield context.session.send_command_async(cmd, cancellable);
}
}
public string name { get { return context.name; } } public string name { get { return context.name; } }
public int exists { get { return context.exists; } } public int exists { get { return context.exists; } }
public int recent { get { return context.recent; } } public int recent { get { return context.recent; } }
@ -49,70 +63,92 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
context.recent_altered.disconnect(on_recent_altered); context.recent_altered.disconnect(on_recent_altered);
} }
public async Gee.List<Geary.Email>? list_set_async(Geary.Folder folder, MessageSet msg_set, public async Gee.List<Geary.Email>? list_set_async(MessageSet msg_set, Geary.Email.Field fields,
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { Cancellable? cancellable = null) throws Error {
if (context.is_closed()) if (context.is_closed())
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name); throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
if (fields == Geary.Email.Field.NONE) if (fields == Geary.Email.Field.NONE)
throw new EngineError.BAD_PARAMETERS("No email fields specified"); throw new EngineError.BAD_PARAMETERS("No email fields specified");
NonblockingBatch batch = new NonblockingBatch();
Gee.List<Geary.Email> msgs = new Gee.ArrayList<Geary.Email>();
Gee.HashMap<int, Geary.Email> map = new Gee.HashMap<int, Geary.Email>();
Gee.List<FetchDataType> data_type_list = new Gee.ArrayList<FetchDataType>(); Gee.List<FetchDataType> data_type_list = new Gee.ArrayList<FetchDataType>();
Gee.List<FetchBodyDataType> body_data_type_list = new Gee.ArrayList<FetchBodyDataType>(); Gee.List<FetchBodyDataType> body_data_type_list = new Gee.ArrayList<FetchBodyDataType>();
fields_to_fetch_data_types(fields, data_type_list, body_data_type_list, false); fields_to_fetch_data_types(fields, data_type_list, body_data_type_list);
// if nothing else, should always fetch the UID, which is gotten via data_type_list
// (necessary to create the EmailIdentifier, also provides mappings of position -> UID)
assert(data_type_list.size > 0);
FetchCommand fetch_cmd = new FetchCommand.from_collection(msg_set, data_type_list, FetchCommand fetch_cmd = new FetchCommand.from_collection(msg_set, data_type_list,
body_data_type_list); body_data_type_list);
CommandResponse resp = yield context.session.send_command_async(fetch_cmd, cancellable); int plain_id = batch.add(new MailboxOperation(context, fetch_cmd));
if (resp.status_response.status != Status.OK) {
throw new ImapError.SERVER_ERROR("Server error for %s: %s", fetch_cmd.to_string(), int preview_id = NonblockingBatch.INVALID_ID;
resp.to_string()); if (fields.require(Geary.Email.Field.PREVIEW)) {
FetchBodyDataType fetch_preview = new FetchBodyDataType.peek(FetchBodyDataType.SectionPart.NONE,
{ 1 }, 0, Geary.Email.MAX_PREVIEW_BYTES, null);
Gee.List<FetchBodyDataType> list = new Gee.ArrayList<FetchBodyDataType>();
list.add(fetch_preview);
FetchCommand preview_cmd = new FetchCommand(msg_set, null, list);
preview_id = batch.add(new MailboxOperation(context, preview_cmd));
} }
Gee.List<Geary.Email> msgs = new Gee.ArrayList<Geary.Email>(); yield batch.execute_all(cancellable);
FetchResults[] results = FetchResults.decode(resp); // process "plain" FETCH results ... these are fetched every time for, if nothing else,
foreach (FetchResults res in results) { // the UID which provides a position -> UID mapping that is kept in the map.
UID? uid = res.get_data(FetchDataType.UID) as UID;
MailboxOperation plain_op = (MailboxOperation) batch.get_operation(plain_id);
CommandResponse plain_resp = (CommandResponse) batch.get_result(plain_id);
if (plain_resp.status_response.status != Status.OK) {
throw new ImapError.SERVER_ERROR("Server error for %s: %s", plain_op.cmd.to_string(),
plain_resp.to_string());
}
FetchResults[] plain_results = FetchResults.decode(plain_resp);
foreach (FetchResults plain_res in plain_results) {
UID? uid = plain_res.get_data(FetchDataType.UID) as UID;
// see fields_to_fetch_data_types() for why this is guaranteed // see fields_to_fetch_data_types() for why this is guaranteed
assert(uid != null); assert(uid != null);
Geary.Email email = new Geary.Email(res.msg_num, new Geary.Imap.EmailIdentifier(uid)); Geary.Email email = new Geary.Email(plain_res.msg_num, new Geary.Imap.EmailIdentifier(uid));
fetch_results_to_email(res, fields, email); fetch_results_to_email(plain_res, fields, email);
msgs.add(email); msgs.add(email);
map.set(plain_res.msg_num, email);
assert(map.get(plain_res.msg_num) != null);
} }
return (msgs != null && msgs.size > 0) ? msgs : null; // process preview FETCH results
}
public async Geary.Email fetch_async(Geary.Folder folder, Geary.Imap.UID uid, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error {
if (context.is_closed())
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
Gee.List<FetchDataType> data_type_list = new Gee.ArrayList<FetchDataType>(); if (preview_id != NonblockingBatch.INVALID_ID) {
Gee.List<FetchBodyDataType> body_data_type_list = new Gee.ArrayList<FetchBodyDataType>(); MailboxOperation preview_op = (MailboxOperation) batch.get_operation(preview_id);
fields_to_fetch_data_types(fields, data_type_list, body_data_type_list, true); CommandResponse preview_resp = (CommandResponse) batch.get_result(preview_id);
FetchCommand fetch_cmd = new FetchCommand.from_collection(new MessageSet.uid(uid), if (preview_resp.status_response.status != Status.OK) {
data_type_list, body_data_type_list); throw new ImapError.SERVER_ERROR("Server error for %s: %s", preview_op.cmd.to_string(),
preview_resp.to_string());
CommandResponse resp = yield context.session.send_command_async(fetch_cmd, cancellable); }
if (resp.status_response.status != Status.OK) {
throw new ImapError.SERVER_ERROR("Server error for %s: %s", fetch_cmd.to_string(), FetchResults[] preview_results = FetchResults.decode(preview_resp);
resp.to_string()); foreach (FetchResults preview_res in preview_results) {
Geary.Email? preview_email = map.get(preview_res.msg_num);
assert(preview_email != null);
preview_email.set_message_preview(new RFC822.Text(preview_res.get_body_data()[0]));
}
} }
FetchResults[] results = FetchResults.decode(resp); return (msgs.size > 0) ? msgs : null;
if (results.length != 1)
throw new ImapError.SERVER_ERROR("Too many responses from server: %d", results.length);
Geary.Email email = new Geary.Email(results[0].msg_num, new Geary.Imap.EmailIdentifier(uid));
fetch_results_to_email(results[0], fields, email);
return email;
} }
private void on_closed() { private void on_closed() {
@ -139,14 +175,10 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
flags_altered(flags); flags_altered(flags);
} }
// store FetchDataTypes in a set because the same data type may be requested multiple times
// by different fields (i.e. ENVELOPE)
private void fields_to_fetch_data_types(Geary.Email.Field fields, Gee.List<FetchDataType> data_types_list, private void fields_to_fetch_data_types(Geary.Email.Field fields, Gee.List<FetchDataType> data_types_list,
Gee.List<FetchBodyDataType> body_data_types_list, bool is_specific_uid) { Gee.List<FetchBodyDataType> body_data_types_list) {
// always fetch UID because it's needed for EmailIdentifier (unless single message is being // always fetch UID because it's needed for EmailIdentifier
// fetched by UID, in which case, obviously not necessary) data_types_list.add(FetchDataType.UID);
if (!is_specific_uid)
data_types_list.add(FetchDataType.UID);
// pack all the needed headers into a single FetchBodyDataType // pack all the needed headers into a single FetchBodyDataType
string[] field_names = new string[0]; string[] field_names = new string[0];
@ -207,7 +239,8 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
break; break;
case Geary.Email.Field.NONE: case Geary.Email.Field.NONE:
// not set case Geary.Email.Field.PREVIEW:
// not set (or, for previews, fetched separately)
break; break;
default: default:
@ -217,7 +250,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
if (field_names.length > 0) { if (field_names.length > 0) {
body_data_types_list.add(new FetchBodyDataType( body_data_types_list.add(new FetchBodyDataType(
FetchBodyDataType.SectionPart.HEADER_FIELDS, field_names)); FetchBodyDataType.SectionPart.HEADER_FIELDS, null, -1, -1, field_names));
} }
} }

View file

@ -5,7 +5,7 @@
*/ */
private class Geary.EngineFolder : Geary.AbstractFolder { private class Geary.EngineFolder : Geary.AbstractFolder {
private const int REMOTE_FETCH_CHUNK_COUNT = 10; private const int REMOTE_FETCH_CHUNK_COUNT = 5;
private class ReplayAppend : ReplayOperation { private class ReplayAppend : ReplayOperation {
public EngineFolder owner; public EngineFolder owner;
@ -52,6 +52,22 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
} }
} }
private class CommitOperation : NonblockingBatchOperation {
public Folder folder;
public Geary.Email email;
public CommitOperation(Folder folder, Geary.Email email) {
this.folder = folder;
this.email = email;
}
public override async Object? execute(Cancellable? cancellable) throws Error {
yield folder.create_email_async(email, cancellable);
return null;
}
}
private RemoteAccount remote; private RemoteAccount remote;
private LocalAccount local; private LocalAccount local;
private LocalFolder local_folder; private LocalFolder local_folder;
@ -762,9 +778,14 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
break; break;
// if any were fetched, store locally // if any were fetched, store locally
// TODO: Bulk writing NonblockingBatch batch = new NonblockingBatch();
foreach (Geary.Email email in remote_list) foreach (Geary.Email email in remote_list)
yield local_folder.create_email_async(email, cancellable); batch.add(new CommitOperation(local_folder, email));
yield batch.execute_all(cancellable);
batch.throw_first_exception();
if (cb != null) if (cb != null)
cb(remote_list, null); cb(remote_list, null);

View file

@ -0,0 +1,244 @@
/* 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.
*/
/**
* A NonblockingBatchOperation is an abstract base class used by NonblockingBatch. It represents
* a single task of asynchronous work. NonblockingBatch will execute it one time only.
*/
public abstract class Geary.NonblockingBatchOperation : Object {
public abstract async Object? execute(Cancellable? cancellable) throws Error;
}
/**
* NonblockingBatch allows for multiple asynchronous tasks to be executed in parallel and for their
* results to be examined after all have completed. It's designed specifically with Vala's async
* keyword in mind.
*
* Although the yield keyword allows for async tasks to execute, it only allows them to performed
* in serial. In a loop, for example, the next task in the loop won't execute until the current
* one has completed. The thread of execution won't block waiting for it, but this can be
* suboptiminal and certain cases.
*
* NonblockingBatch allows for multiple async tasks to be gathered (via the add() method) into a
* single batch. Each task must subclass from NonblockingBatchOperation. It's expected that the
* subclass will maintain state particular to the operation, although NonblockingBatch does gather
* two types of a results the task may generate: a result object (which descends from Object) or
* a thrown exception. Other results should be stored by the subclass.
*
* To use, create a NonblockingBatch and populate it via the add() method. When all
* NonblockingBatchOperations have been added, call execute_all(). NonblockingBatch will fire off
* all at once and only complete execute_all() when all of them have finished. As mentioned
* earlier, it's also gather their returned objects and thrown exceptions while they run. See
* get_result() and throw_first_exception() for more information.
*
* The caller will want to call *either* get_result() or throw_first_exception() to ensure that
* errors are propagated. It's not necessary to call both.
*
* After execute_all() has completed, the results may be examined. The NonblockingBatch object
* can *not* be reused.
*
* Currently NonblockingBatch will fire off all operations at once and let them complete. It does
* not attempt to stop the others if one throws exception. Also, there's no algorithm to submit the
* operations in smaller chunks (to avoid flooding the thread's MainLoop). These may be added in
* the future.
*/
public class Geary.NonblockingBatch : Object {
public const int INVALID_ID = -1;
private const int START_ID = 1;
private class BatchContext {
public weak NonblockingBatch owner;
public int id;
public NonblockingBatchOperation op;
public bool completed = false;
public Object? returned = null;
public Error? threw = null;
public BatchContext(NonblockingBatch owner, int id, NonblockingBatchOperation op) {
this.owner = owner;
this.id = id;
this.op = op;
}
public void execute(Cancellable? cancellable) {
op.execute.begin(cancellable, on_op_completed);
}
private void on_op_completed(Object? source, AsyncResult result) {
completed = true;
try {
returned = op.execute.end(result);
} catch (Error err) {
threw = err;
}
owner.on_context_completed(this);
}
}
/**
* Returns the number of NonblockingBatchOperations added.
*/
public int count {
get { return contexts.size; }
}
private Gee.HashMap<int, BatchContext> contexts = new Gee.HashMap<int, BatchContext>();
private NonblockingSemaphore sem = new NonblockingSemaphore();
private int next_result_id = START_ID;
private bool locked = false;
private int completed_ops = 0;
private Error? first_exception = null;
public signal void added(NonblockingBatchOperation op, int id);
public signal void started(int count);
public signal void operation_completed(NonblockingBatchOperation op, Object? returned,
Error? threw);
public signal void completed(int count, Error? first_error);
public NonblockingBatch() {
}
/**
* Adds a NonblockingBatchOperation to the batch. INVALID_ID is returned if the batch is
* executing or has already executed. Otherwise, returns an ID that can be used to fetch
* results of this particular NonblockingBatchOperation after execute_all() completes.
*
* The returned ID is only good for this NonblockingBatch. Since each instance uses the
* same algorithm, different instances will likely return the same ID, so they must be
* associated with the NonblockingBatch they originated from.
*/
public int add(NonblockingBatchOperation op) {
if (locked) {
warning("NonblockingBatch already executed or executing");
return INVALID_ID;
}
int id = next_result_id++;
contexts.set(id, new BatchContext(this, id, op));
added(op, id);
return id;
}
/**
* Executes all the NonblockingBatchOperations added to the batch. The supplied Cancellable
* will be passed to each operation.
*
* If the batch is executing or already executed, IOError.PENDING will be thrown. If the
* Cancellable is already cancelled, IOError.CANCELLED is thrown. Other errors may be thrown
* as well; see NonblockingAbstractSemaphore.wait_async().
*
* If there are no operations added to the batch, the method quietly exits.
*/
public async void execute_all(Cancellable? cancellable = null) throws Error {
if (locked)
throw new IOError.PENDING("NonblockingBatch already executed or executing");
locked = true;
// if empty, quietly exit
if (contexts.size == 0)
return;
// if already cancelled, not-so-quietly exit
if (cancellable != null && cancellable.is_cancelled())
throw new IOError.CANCELLED("NonblockingBatch cancelled before executing");
started(contexts.size);
// although they should technically be able to execute in any order, fire them off in the
// order they were submitted; this may hide bugs, but it also makes other bugs reproducible
int count = 0;
for (int id = START_ID; id < next_result_id; id++) {
BatchContext? context = contexts.get(id);
assert(context != null);
context.execute(cancellable);
count++;
}
assert(count == contexts.size);
yield sem.wait_async(cancellable);
}
/**
* Returns a Set of IDs for all added NonblockingBatchOperations.
*/
public Gee.Set<int> get_ids() {
return contexts.keys;
}
/**
* Returns the NonblockingBatchOperation for the supplied ID. Returns null if the ID is invalid
* or unknown.
*/
public NonblockingBatchOperation? get_operation(int id) {
BatchContext? context = contexts.get(id);
return (context != null) ? context.op : null;
}
/**
* Returns the resulting Object from the operation for the supplied ID. If the ID is invalid
* or unknown, or the operation returned null, null is returned.
*
* If the operation threw an exception, it will be thrown here. If all the operations' results
* are examined with this method, there is no need to call throw_first_exception().
*
* If the operation has not completed, IOError.BUSY will be thrown. It *is* legal to query
* the result of a completed operation while others are executing.
*/
public Object? get_result(int id) throws Error {
BatchContext? context = contexts.get(id);
if (context == null)
return null;
if (!context.completed)
throw new IOError.BUSY("NonblockingBatchOperation %d not completed", id);
if (context.threw != null)
throw context.threw;
return context.returned;
}
/**
* If no results are examined via get_result(), this method can be used to manually throw the
* first seen Error from the operations.
*/
public void throw_first_exception() throws Error {
if (first_exception != null)
throw first_exception;
}
private void on_context_completed(BatchContext context) {
if (first_exception == null && context.threw != null)
first_exception = context.threw;
operation_completed(context.op, context.returned, context.threw);
assert(completed_ops < contexts.size);
if (++completed_ops == contexts.size) {
try {
sem.notify();
} catch (Error err) {
debug("Unable to notify NonblockingBatch semaphore: %s", err.message);
}
completed(completed_ops, first_exception);
}
}
}

View file

@ -29,6 +29,8 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
public string? body { get; set; } public string? body { get; set; }
public string? preview { get; set; }
public MessageRow(Table table) { public MessageRow(Table table) {
base (table); base (table);
} }
@ -80,6 +82,9 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
if ((fields & Geary.Email.Field.BODY) != 0) if ((fields & Geary.Email.Field.BODY) != 0)
body = fetch_string_for(result, MessageTable.Column.BODY); body = fetch_string_for(result, MessageTable.Column.BODY);
if ((fields & Geary.Email.Field.PREVIEW) != 0)
preview = fetch_string_for(result, MessageTable.Column.PREVIEW);
} }
public Geary.Email to_email(int position, Geary.EmailIdentifier id) throws Error { public Geary.Email to_email(int position, Geary.EmailIdentifier id) throws Error {
@ -114,6 +119,9 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
if (((fields & Geary.Email.Field.BODY) != 0) && (body != null)) if (((fields & Geary.Email.Field.BODY) != 0) && (body != null))
email.set_message_body(new RFC822.Text(new Geary.Memory.StringBuffer(body))); email.set_message_body(new RFC822.Text(new Geary.Memory.StringBuffer(body)));
if (((fields & Geary.Email.Field.PREVIEW) != 0) && (preview != null))
email.set_message_preview(new RFC822.Text(new Geary.Memory.StringBuffer(preview)));
return email; return email;
} }
@ -206,6 +214,12 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
this.fields = this.fields.set(Geary.Email.Field.BODY); this.fields = this.fields.set(Geary.Email.Field.BODY);
} }
if ((fields & Geary.Email.Field.PREVIEW) != 0) {
preview = (email.preview != null) ? email.preview.buffer.to_utf8() : null;
this.fields = this.fields.set(Geary.Email.Field.PREVIEW);
}
} }
private void unset_fields(Geary.Email.Field fields) { private void unset_fields(Geary.Email.Field fields) {
@ -257,6 +271,12 @@ public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row {
this.fields = this.fields.clear(Geary.Email.Field.BODY); this.fields = this.fields.clear(Geary.Email.Field.BODY);
} }
if ((fields & Geary.Email.Field.PREVIEW) != 0) {
preview = null;
this.fields = this.fields.clear(Geary.Email.Field.PREVIEW);
}
} }
} }

View file

@ -29,7 +29,9 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
HEADER, HEADER,
BODY; BODY,
PREVIEW;
} }
internal MessageTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { internal MessageTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) {
@ -44,8 +46,8 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
SQLHeavy.Query query = locked.prepare( SQLHeavy.Query query = locked.prepare(
"INSERT INTO MessageTable " "INSERT INTO MessageTable "
+ "(fields, date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, " + "(fields, date_field, date_time_t, from_field, sender, reply_to, to_field, cc, bcc, "
+ "message_id, in_reply_to, reference_ids, subject, header, body) " + "message_id, in_reply_to, reference_ids, subject, header, body, preview) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
query.bind_int(0, row.fields); query.bind_int(0, row.fields);
query.bind_string(1, row.date); query.bind_string(1, row.date);
query.bind_int64(2, row.date_time_t); query.bind_int64(2, row.date_time_t);
@ -61,6 +63,7 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
query.bind_string(12, row.subject); query.bind_string(12, row.subject);
query.bind_string(13, row.header); query.bind_string(13, row.header);
query.bind_string(14, row.body); query.bind_string(14, row.body);
query.bind_string(15, row.preview);
int64 id = yield query.execute_insert_async(cancellable); int64 id = yield query.execute_insert_async(cancellable);
locked.set_commit_required(); locked.set_commit_required();
@ -156,6 +159,15 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
yield query.execute_async(cancellable); yield query.execute_async(cancellable);
} }
if (row.fields.is_any_set(Geary.Email.Field.PREVIEW)) {
query = locked.prepare(
"UPDATE MessageTable SET preview=? WHERE id=?");
query.bind_string(0, row.preview);
query.bind_int64(1, row.id);
yield query.execute_async(cancellable);
}
// only commit if internally atomic // only commit if internally atomic
if (transaction == null) if (transaction == null)
yield locked.commit_async(cancellable); yield locked.commit_async(cancellable);
@ -263,13 +275,15 @@ public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table {
case Geary.Email.Field.BODY: case Geary.Email.Field.BODY:
append = "body"; append = "body";
break; break;
case Geary.Email.Field.PREVIEW:
append = "preview";
break;
} }
} }
if (append != null) { if (append != null) {
if (!String.is_empty(builder.str)) builder.append(", ");
builder.append(", ");
builder.append(append); builder.append(append);
} }
} }

View file

@ -87,9 +87,10 @@ def build(bld):
'../engine/impl/geary-remote-interfaces.vala', '../engine/impl/geary-remote-interfaces.vala',
'../engine/impl/geary-replay-queue.vala', '../engine/impl/geary-replay-queue.vala',
'../engine/nonblocking/nonblocking-abstract-semaphore.vala',
'../engine/nonblocking/nonblocking-batch.vala',
'../engine/nonblocking/nonblocking-mailbox.vala', '../engine/nonblocking/nonblocking-mailbox.vala',
'../engine/nonblocking/nonblocking-mutex.vala', '../engine/nonblocking/nonblocking-mutex.vala',
'../engine/nonblocking/nonblocking-abstract-semaphore.vala',
'../engine/nonblocking/nonblocking-variants.vala', '../engine/nonblocking/nonblocking-variants.vala',
'../engine/rfc822/rfc822-error.vala', '../engine/rfc822/rfc822-error.vala',