Commit 18716ae6 authored by Jim Nelson's avatar Jim Nelson

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.
parent 782e6fa5
......@@ -39,7 +39,9 @@ CREATE TABLE MessageTable (
header TEXT,
body TEXT
body TEXT,
preview TEXT
);
CREATE INDEX MessageTableMessageIDIndex ON MessageTable(message_id);
......
......@@ -8,6 +8,58 @@ public class MainWindow : Gtk.Window {
private const int MESSAGE_LIST_WIDTH = 250;
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 MessageListStore message_list_store = new MessageListStore();
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
Geary.SpecialFolderMap? special_folders = account.get_special_folder_map();
if (special_folders != null) {
foreach (Geary.SpecialFolder special_folder in special_folders.get_all()) {
Geary.Folder folder = yield account.fetch_folder_async(special_folder.path);
folder_list_store.add_special_folder(special_folder, folder);
Geary.NonblockingBatch batch = new Geary.NonblockingBatch();
foreach (Geary.SpecialFolder special_folder in special_folders.get_all())
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
......@@ -78,7 +145,7 @@ public class MainWindow : Gtk.Window {
else
debug("no folders");
} 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 {
Geary.NonblockingBatch batch = new Geary.NonblockingBatch();
int count = message_list_store.get_count();
for (int ctr = 0; ctr < count; ctr++) {
Geary.Email? email = message_list_store.get_newest_message_at_index(ctr);
if (email == null)
continue;
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);
if (email != null)
batch.add(new FetchPreviewOperation(this, current_folder, email.id, ctr));
}
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)
if (second_list_pass_required) {
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) {
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>();
foreach (Geary.Folder folder in folders) {
foreach (int id in batch.get_ids()) {
ListFoldersOperation op = (ListFoldersOperation) batch.get_operation(id);
try {
Gee.Collection<Geary.Folder> children = yield account.list_folders_async(
folder.get_path(), null);
Gee.Collection<Geary.Folder> children = (Gee.Collection<Geary.Folder>) batch.get_result(id);
accumulator.add_all(children);
} catch (Error err) {
debug("Unable to list children of %s: %s", folder.to_string(), err.message);
} catch (Error err2) {
debug("Unable to list children of %s: %s", op.path.to_string(), err2.message);
}
}
......
......@@ -42,21 +42,14 @@ public class FormattedMessageData : Object {
public FormattedMessageData.from_email(Geary.Email email, int num_emails) {
assert(email.fields.fulfills(MessageListStore.REQUIRED_FIELDS));
StringBuilder builder = new StringBuilder();
if (email.fields.fulfills(Geary.Email.Field.BODY)) {
try {
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 preview = "";
if (email.fields.fulfills(Geary.Email.Field.PREVIEW) && email.preview != null)
preview = email.preview.buffer.to_utf8();
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),
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;
}
......
......@@ -5,10 +5,12 @@
*/
public class MessageListStore : Gtk.TreeStore {
public const Geary.Email.Field REQUIRED_FIELDS =
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 {
MESSAGE_DATA,
MESSAGE_OBJECT,
......
......@@ -94,6 +94,7 @@ class ImapConsole : Gtk.Window {
"gmail",
"keepalive",
"status",
"preview",
"close"
};
......@@ -196,6 +197,10 @@ class ImapConsole : Gtk.Window {
folder_status(cmd, args);
break;
case "preview":
preview(cmd, args);
break;
default:
status("Unknown command \"%s\"".printf(cmd));
break;
......@@ -407,7 +412,7 @@ class ImapConsole : Gtk.Window {
status("Fetching fields %s".printf(args[0]));
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>();
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 {
Gtk.main_quit();
}
......
......@@ -5,6 +5,10 @@
*/
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.
public enum Field {
NONE = 0,
......@@ -16,6 +20,8 @@ public class Geary.Email : Object {
HEADER = 1 << 5,
BODY = 1 << 6,
PROPERTIES = 1 << 7,
PREVIEW = 1 << 8,
ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT,
ALL = 0xFFFFFFFF;
......@@ -28,7 +34,8 @@ public class Geary.Email : Object {
SUBJECT,
HEADER,
BODY,
PROPERTIES
PROPERTIES,
PREVIEW
};
}
......@@ -108,6 +115,9 @@ public class Geary.Email : Object {
// PROPERTIES
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; }
private Geary.RFC822.Message? message = null;
......@@ -188,6 +198,12 @@ public class Geary.Email : Object {
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.
* If not, EngineError.INCOMPLETE_MESSAGE is thrown.
......
......@@ -11,6 +11,7 @@ public errordomain Geary.EngineError {
NOT_FOUND,
READONLY,
BAD_PARAMETERS,
BAD_RESPONSE,
INCOMPLETE_MESSAGE,
SERVER_UNAVAILABLE,
CLOSED
......
......@@ -10,6 +10,23 @@ private class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
public const string INBOX_NAME = "INBOX";
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 ClientSessionManager session_mgr;
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>();
Geary.NonblockingBatch batch = new Geary.NonblockingBatch();
foreach (MailboxInformation mbox in mboxes) {
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)
delims.set(path.get_root().basename, mbox.delim);
StatusResults? status = null;
if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT)) {
try {
status = yield session_mgr.status_async(path.get_fullpath(),
StatusDataType.all(), cancellable);
} catch (Error status_err) {
message("Unable to fetch status for %s: %s", path.to_string(), status_err.message);
}
if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT))
batch.add(new StatusOperation(session_mgr, mbox, path));
else
folders.add(new Geary.Imap.Folder(session_mgr, path, null, mbox));
}
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;
......
......@@ -120,7 +120,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
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,
......@@ -129,7 +129,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
if (mailbox == null)
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,
......@@ -160,7 +160,7 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
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,
......@@ -168,9 +168,20 @@ private class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
if (mailbox == null)
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)
......
......@@ -4,7 +4,7 @@
* (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 StatusResponse? status_response { get; private set; }
......
......@@ -4,7 +4,7 @@
* (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 CommandResults(StatusResponse status_response) {
......
......@@ -85,11 +85,11 @@ public class Geary.Imap.FetchResults : Geary.Imap.CommandResults {
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);
}
public MessageData? get_data(FetchDataType data_item) {
public new MessageData? get_data(FetchDataType data_item) {
return map.get(data_item);
}
......
......@@ -44,25 +44,47 @@ public class Geary.Imap.FetchBodyDataType {
}
private SectionPart section_part;
private int[]? part_number;
private int partial_start;
private int partial_count;
private string[]? field_names;
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
* and must be null for all other SectionParts.
*/
public FetchBodyDataType(SectionPart section_part, string[]? field_names) {
init(section_part, field_names, false);
public FetchBodyDataType(SectionPart section_part, int[]? part_number, int partial_start,
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.
*/
public FetchBodyDataType.peek(SectionPart section_part, string[]? field_names) {
init(section_part, field_names, true);
public FetchBodyDataType.peek(SectionPart section_part, int[]? part_number, int partial_start,
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) {
case SectionPart.HEADER_FIELDS:
case SectionPart.HEADER_FIELDS_NOT:
......@@ -74,7 +96,13 @@ public class Geary.Imap.FetchBodyDataType {
break;
}
if (partial_start >= 0)
assert(partial_count > 0);
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.is_peek = is_peek;
}
......@@ -89,6 +117,25 @@ public class Geary.Imap.FetchBodyDataType {
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() {
if (field_names == null || field_names.length == 0)
return "";
......@@ -105,6 +152,10 @@ public class Geary.Imap.FetchBodyDataType {
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) {
string strd = items.value.down();
......@@ -112,8 +163,11 @@ public class Geary.Imap.FetchBodyDataType {
}
public string to_string() {
return (!is_peek ? "body[%s%s]" : "body.peek[%s%s]").printf(section_part.serialize(),
serialize_field_names());
return (!is_peek ? "body[%s%s%s]%s" : "body.peek[%s%s%s]%s").printf(
serialize_part_number(),
section_part.serialize(),
serialize_field_names(),
serialize_partial());
}
}
......@@ -4,7 +4,6 @@
* (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 {
UID,
FLAGS,
......
......@@ -73,6 +73,11 @@ public class Geary.Imap.StringParameter : Geary.Imap.Parameter {
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() {
return value;
}
......
......@@ -5,8 +5,8 @@
*/
public class Geary.Imap.ClientSessionManager {
public const int MIN_POOL_SIZE = 2;
public const int SELECTED_KEEPALIVE_SEC = 5;
private const int MIN_POOL_SIZE = 2;
private const int SELECTED_KEEPALIVE_SEC = 60;
private Endpoint endpoint;
private Credentials credentials;
......
......@@ -5,6 +5,20 @@
*/
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 int exists { get { return context.exists; } }
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);
}
public async Gee.List<Geary.Email>? list_set_async(Geary.Folder folder, MessageSet msg_set,
Geary.Email.Field fields, Cancellable? cancellable = null) throws Error {
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);
if (fields == Geary.Email.Field.NONE)
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<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,
body_data_type_list);
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(),
resp.to_string());
int plain_id = batch.add(new MailboxOperation(context, fetch_cmd));
int preview_id = NonblockingBatch.INVALID_ID;
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>();