Commit 7146ca47 authored by Eric Gregory's avatar Eric Gregory

Merge branch 'master' into feature/drafts

parents 3a699584 0df2c1fa
......@@ -12,3 +12,4 @@ install(FILES version-009.sql DESTINATION ${SQL_DEST})
install(FILES version-010.sql DESTINATION ${SQL_DEST})
install(FILES version-011.sql DESTINATION ${SQL_DEST})
install(FILES version-012.sql DESTINATION ${SQL_DEST})
install(FILES version-013.sql DESTINATION ${SQL_DEST})
--
-- Add the disposition column as a string so the client can decide which attachments to show.
-- Since all attachments up to this point have been non-inline, set it to that value (which
-- is defined in src/engine/api/geary-attachment.vala
--
ALTER TABLE MessageAttachmentTable ADD COLUMN disposition INTEGER;
UPDATE MessageAttachmentTable SET disposition=0;
......@@ -26,9 +26,21 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
}
public override string? get_sidebar_tooltip() {
return (folder.properties.email_unread == 0 ? null :
ngettext("%d unread message", "%d unread messages", folder.properties.email_unread).
printf(folder.properties.email_unread));
// Label displaying total number of email messages in a folder
string total_msg = ngettext("%d message", "%d messages", folder.properties.email_total).
printf(folder.properties.email_total);
if (folder.properties.email_unread == 0)
return total_msg;
/// Label displaying number of unread email messages in a folder
string unread_msg = ngettext("%d unread", "%d unread", folder.properties.email_unread).
printf(folder.properties.email_unread);
/// This string represents the divider between two messages: "n messages" and "n unread",
/// shown in the folder list as a tooltip. Please use your languages conventions for
/// combining the two, i.e. a comma (",") for English; "6 messages, 3 unread"
return _("%s, %s").printf(total_msg, unread_msg);
}
public override Icon? get_sidebar_icon() {
......
......@@ -15,6 +15,9 @@ public class ConversationViewer : Gtk.Box {
| Geary.Email.Field.FLAGS
| Geary.Email.Field.PREVIEW;
public const string INLINE_MIME_TYPES =
"image/png image/gif image/jpeg image/pjpeg image/bmp image/x-icon image/x-xbitmap image/x-xbm";
private const int ATTACHMENT_PREVIEW_SIZE = 50;
private const int SELECT_CONVERSATION_TIMEOUT_MSEC = 100;
private const string MESSAGE_CONTAINER_ID = "message_container";
......@@ -462,9 +465,10 @@ public class ConversationViewer : Gtk.Box {
}
}
// Set attachment icon and add the attachments container if we have any attachments.
set_attachment_icon(div_message, email.attachments.size > 0);
if (email.attachments.size > 0) {
// Set attachment icon and add the attachments container if there are displayed attachments.
int displayed = displayed_attachments(email);
set_attachment_icon(div_message, displayed > 0);
if (displayed > 0) {
insert_attachments(div_message, email.attachments);
}
......@@ -595,7 +599,7 @@ public class ConversationViewer : Gtk.Box {
string body_text = "";
remote_images = false;
try {
body_text = message.get_body(true);
body_text = message.get_body(true, inline_image_replacer);
body_text = insert_html_markup(body_text, message, out remote_images);
} catch (Error err) {
debug("Could not get message text. %s", err.message);
......@@ -631,6 +635,16 @@ public class ConversationViewer : Gtk.Box {
}
}
private static string? inline_image_replacer(string filename, string mimetype, Geary.Memory.Buffer buffer) {
if (!(mimetype in INLINE_MIME_TYPES))
return null;
uint8[] image_data = buffer.get_uint8_array();
return "<img src=\"%s\" alt=\"%s\" class=\"%s\" />".printf(
@"data:$mimetype;base64,$(Base64.encode(image_data))",
filename, "replaced_inline_image");
}
private void unhide_last_email() {
WebKit.DOM.HTMLElement last_email = (WebKit.DOM.HTMLElement) web_view.container.get_last_child().previous_sibling;
if (last_email != null) {
......@@ -1151,8 +1165,8 @@ public class ConversationViewer : Gtk.Box {
// where n is the minimum of the number of levels of the two domains.
string[] href_parts = href_match.fetch_all();
string[] text_parts = text_match.fetch_all();
string[] text_domain = text_parts[2].reverse().split(".");
string[] href_domain = href_parts[2].reverse().split(".");
string[] text_domain = text_parts[2].down().reverse().split(".");
string[] href_domain = href_parts[2].down().reverse().split(".");
for (int i = 0; i < text_domain.length && i < href_domain.length; i++) {
if (text_domain[i] != href_domain[i]) {
if (href_parts[1] == "")
......@@ -1298,7 +1312,7 @@ public class ConversationViewer : Gtk.Box {
save_attachment_item.activate.connect(() => save_attachment(attachment));
menu.append(save_attachment_item);
if (email.attachments.size > 1) {
if (displayed_attachments(email) > 1) {
Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(_("Save All A_ttachments..."));
save_all_item.activate.connect(() => save_attachments(email.attachments));
menu.append(save_all_item);
......@@ -1317,9 +1331,10 @@ public class ConversationViewer : Gtk.Box {
Gtk.Menu menu = new Gtk.Menu();
menu.selection_done.connect(on_message_menu_selection_done);
if (email.attachments.size > 0) {
int displayed = displayed_attachments(email);
if (displayed > 0) {
string mnemonic = ngettext("Save A_ttachment...", "Save All A_ttachments...",
email.attachments.size);
displayed);
Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(mnemonic);
save_all_item.activate.connect(() => save_attachments(email.attachments));
menu.append(save_all_item);
......@@ -1471,7 +1486,7 @@ public class ConversationViewer : Gtk.Box {
} else if (src.has_prefix("cid:")) {
string mime_id = src.substring(4);
Geary.Memory.Buffer image_content = message.get_content_by_mime_id(mime_id);
uint8[] image_data = image_content.get_bytes().get_data();
uint8[] image_data = image_content.get_uint8_array();
// Get the content type.
bool uncertain_content_type;
......@@ -1606,6 +1621,29 @@ public class ConversationViewer : Gtk.Box {
header_text += create_header_row(Geary.HTML.escape_markup(title), value, important);
}
private static bool should_show_attachment(Geary.Attachment attachment) {
switch (attachment.disposition) {
case Geary.Attachment.Disposition.ATTACHMENT:
return true;
case Geary.Attachment.Disposition.INLINE:
return !(attachment.mime_type in INLINE_MIME_TYPES);
default:
assert_not_reached();
}
}
private static int displayed_attachments(Geary.Email email) {
int ret = 0;
foreach (Geary.Attachment attachment in email.attachments) {
if (should_show_attachment(attachment)) {
ret++;
}
}
return ret;
}
private void insert_attachments(WebKit.DOM.HTMLElement email_container,
Gee.List<Geary.Attachment> attachments) {
......@@ -1636,6 +1674,9 @@ public class ConversationViewer : Gtk.Box {
// Create an attachment table for each attachment.
foreach (Geary.Attachment attachment in attachments) {
if (!should_show_attachment(attachment)) {
continue;
}
// Generate the attachment table.
WebKit.DOM.HTMLElement attachment_table = Util.DOM.clone_node(attachment_template);
string filename = Geary.String.is_empty_or_whitespace(attachment.filename) ?
......
......@@ -7,20 +7,58 @@
public class Geary.Attachment : BaseObject {
public const Email.Field REQUIRED_FIELDS = Email.REQUIRED_FOR_MESSAGE;
// NOTE: These values are persisted on disk and should not be modified unless you know what
// you're doing.
public enum Disposition {
ATTACHMENT = 0,
INLINE = 1;
public static Disposition? from_string(string? str) {
// Returns null to indicate an unknown disposition
if (str == null) {
return null;
}
switch (str.down()) {
case "attachment":
return ATTACHMENT;
case "inline":
return INLINE;
default:
return null;
}
}
public static Disposition from_int(int i) {
switch (i) {
case INLINE:
return INLINE;
case ATTACHMENT:
default:
return ATTACHMENT;
}
}
}
public string? filename { get; private set; }
public string filepath { get; private set; }
public string mime_type { get; private set; }
public int64 filesize { get; private set; }
public int64 id { get; private set; }
public Disposition disposition { get; private set; }
internal Attachment(File data_dir, string? filename, string mime_type, int64 filesize,
int64 message_id, int64 attachment_id) {
int64 message_id, int64 attachment_id, Disposition disposition) {
this.filename = filename;
this.mime_type = mime_type;
this.filesize = filesize;
this.filepath = get_path(data_dir, message_id, attachment_id, filename);
this.id = attachment_id;
this.disposition = disposition;
}
internal static string get_path(File data_dir, int64 message_id, int64 attachment_id,
......
......@@ -41,7 +41,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
}
/**
* Returns true if this {@link FolderPath} is the root folder.
* Returns true if this {@link FolderPath} is a root folder.
*
* This means that the FolderPath ''should'' be castable into {@link FolderRoot}, which is
* enforced through the constructor and accessor styles of this class. However, this test
......
......@@ -367,6 +367,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
folder.email_appended.disconnect(on_folder_email_appended);
folder.email_removed.disconnect(on_folder_email_removed);
folder.email_flags_changed.disconnect(on_folder_email_flags_changed);
folder.email_count_changed.disconnect(on_folder_email_count_changed);
folder.opened.disconnect(on_folder_opened);
folder.closed.disconnect(on_folder_closed);
folder.account.email_locally_complete.disconnect(on_account_email_locally_complete);
......
......@@ -52,7 +52,7 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
check_cancelled("VersionedDatabase.open", cancellable);
try {
debug("Upgrading database to to version %d with %s", db_version, upgrade_script.get_path());
debug("Upgrading database to version %d with %s", db_version, upgrade_script.get_path());
cx.exec_transaction(TransactionType.EXCLUSIVE, (cx) => {
cx.exec_file(upgrade_script, cancellable);
cx.set_user_version_number(db_version);
......
......@@ -157,10 +157,11 @@ private class Geary.ImapDB.Account : BaseObject {
/**
* Only updates folder's STATUS message count, attributes, recent, and unseen; UIDVALIDITY and UIDNEXT
* updated when the folder is SELECT/EXAMINED (see update_folder_select_examine_async())
* updated when the folder is SELECT/EXAMINED (see update_folder_select_examine_async()) unless
* update_uid_info is true.
*/
public async void update_folder_status_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable)
throws Error {
public async void update_folder_status_async(Geary.Imap.Folder imap_folder, bool update_uid_info,
Cancellable? cancellable) throws Error {
check_open();
Geary.Imap.FolderProperties properties = imap_folder.properties;
......@@ -190,7 +191,10 @@ private class Geary.ImapDB.Account : BaseObject {
stmt.bind_string(2, path.basename);
}
stmt.exec();
stmt.exec(cancellable);
if (update_uid_info)
do_update_uid_info(cx, properties, parent_id, path, cancellable);
if (properties.status_messages >= 0) {
do_update_last_seen_status_total(cx, parent_id, path.basename, properties.status_messages,
......@@ -209,13 +213,18 @@ private class Geary.ImapDB.Account : BaseObject {
local_properties.recent = properties.recent;
local_properties.attrs = properties.attrs;
if (update_uid_info) {
local_properties.uid_validity = properties.uid_validity;
local_properties.uid_next = properties.uid_next;
}
if (properties.status_messages >= 0)
local_properties.set_status_message_count(properties.status_messages, false);
}
}
/**
* Only updates folder's SELECT/EXAMINE message count, UIDVALIDITY, UIDNEXT, unseen, and recent.
* Updates folder's SELECT/EXAMINE message count, UIDVALIDITY, UIDNEXT, unseen, and recent.
* See also update_folder_status_async().
*/
public async void update_folder_select_examine_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable)
......@@ -233,28 +242,7 @@ private class Geary.ImapDB.Account : BaseObject {
return Db.TransactionOutcome.ROLLBACK;
}
int64 uid_validity = (properties.uid_validity != null) ? properties.uid_validity.value
: Imap.UIDValidity.INVALID;
int64 uid_next = (properties.uid_next != null) ? properties.uid_next.value
: Imap.UID.INVALID;
Db.Statement stmt;
if (parent_id != Db.INVALID_ROWID) {
stmt = cx.prepare(
"UPDATE FolderTable SET uid_validity=?, uid_next=? WHERE parent_id=? AND name=?");
stmt.bind_int64(0, uid_validity);
stmt.bind_int64(1, uid_next);
stmt.bind_rowid(2, parent_id);
stmt.bind_string(3, path.basename);
} else {
stmt = cx.prepare(
"UPDATE FolderTable SET uid_validity=?, uid_next=? WHERE parent_id IS NULL AND name=?");
stmt.bind_int64(0, uid_validity);
stmt.bind_int64(1, uid_next);
stmt.bind_string(2, path.basename);
}
stmt.exec();
do_update_uid_info(cx, properties, parent_id, path, cancellable);
if (properties.select_examine_messages >= 0) {
do_update_last_seen_select_examine_total(cx, parent_id, path.basename,
......@@ -825,7 +813,7 @@ private class Geary.ImapDB.Account : BaseObject {
}
private async void populate_search_table_async(Cancellable? cancellable) {
debug("Populating search table");
debug("%s: Populating search table", account_information.email);
try {
int total = yield get_email_count_async(cancellable);
search_index_monitor.set_interval(0, total);
......@@ -840,17 +828,19 @@ private class Geary.ImapDB.Account : BaseObject {
yield Geary.Scheduler.sleep_ms_async(50);
}
} catch (Error e) {
debug("Error populating search table: %s", e.message);
debug("Error populating %s search table: %s", account_information.email, e.message);
}
if (search_index_monitor.is_in_progress)
search_index_monitor.notify_finish();
debug("Done populating search table");
debug("%s: Done populating search table", account_information.email);
}
private async bool populate_search_table_batch_async(int limit = 100,
Cancellable? cancellable) throws Error {
debug("%s: Searching for missing indexed messages...", account_information.email);
int count = 0;
yield db.exec_transaction_async(Db.TransactionType.RW, (cx, cancellable) => {
Db.Statement stmt = cx.prepare("""
......@@ -897,6 +887,9 @@ private class Geary.ImapDB.Account : BaseObject {
search_index_monitor.increment(count);
if (count > 0)
debug("%s: Found %d/%d missing indexed messages...", account_information.email, count, limit);
return (count < limit);
}
......@@ -1065,7 +1058,7 @@ private class Geary.ImapDB.Account : BaseObject {
// For a message row id, return a set of all folders it's in, or null if
// it's not in any folders.
private Gee.Set<Geary.FolderPath>? do_find_email_folders(Db.Connection cx, int64 message_id,
private static Gee.Set<Geary.FolderPath>? do_find_email_folders(Db.Connection cx, int64 message_id,
Cancellable? cancellable) throws Error {
Db.Statement stmt = cx.prepare("SELECT folder_id FROM MessageLocationTable WHERE message_id=?");
stmt.bind_int64(0, message_id);
......@@ -1090,7 +1083,7 @@ private class Geary.ImapDB.Account : BaseObject {
// For a folder row id, return the folder path (constructed with default
// separator and case sensitivity) of that folder, or null in the event
// it's not found.
private Geary.FolderPath? do_find_folder_path(Db.Connection cx, int64 folder_id,
private static Geary.FolderPath? do_find_folder_path(Db.Connection cx, int64 folder_id,
Cancellable? cancellable) throws Error {
Db.Statement stmt = cx.prepare("SELECT parent_id, name FROM FolderTable WHERE id=?");
stmt.bind_int64(0, folder_id);
......@@ -1147,6 +1140,32 @@ private class Geary.ImapDB.Account : BaseObject {
stmt.exec(cancellable);
}
private void do_update_uid_info(Db.Connection cx, Imap.FolderProperties properties,
int64 parent_id, FolderPath path, Cancellable? cancellable) throws Error {
int64 uid_validity = (properties.uid_validity != null) ? properties.uid_validity.value
: Imap.UIDValidity.INVALID;
int64 uid_next = (properties.uid_next != null) ? properties.uid_next.value
: Imap.UID.INVALID;
Db.Statement stmt;
if (parent_id != Db.INVALID_ROWID) {
stmt = cx.prepare(
"UPDATE FolderTable SET uid_validity=?, uid_next=? WHERE parent_id=? AND name=?");
stmt.bind_int64(0, uid_validity);
stmt.bind_int64(1, uid_next);
stmt.bind_rowid(2, parent_id);
stmt.bind_string(3, path.basename);
} else {
stmt = cx.prepare(
"UPDATE FolderTable SET uid_validity=?, uid_next=? WHERE parent_id IS NULL AND name=?");
stmt.bind_int64(0, uid_validity);
stmt.bind_int64(1, uid_next);
stmt.bind_string(2, path.basename);
}
stmt.exec(cancellable);
}
private int do_get_email_count(Db.Connection cx, Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare(
......
......@@ -43,6 +43,10 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
case 12:
post_upgrade_populate_internal_date_time_t();
break;
case 13:
post_upgrade_populate_additional_attachments();
break;
}
}
......@@ -186,7 +190,54 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
return Db.TransactionOutcome.COMMIT;
});
} catch (Error e) {
debug("Error populating internaldate_time_t column during upgrade to database schema 11: %s",
debug("Error populating internaldate_time_t column during upgrade to database schema 12: %s",
e.message);
}
}
// Version 13.
private void post_upgrade_populate_additional_attachments() {
try {
exec_transaction(Db.TransactionType.RW, (cx) => {
Db.Statement stmt = cx.prepare("""
SELECT id, header, body
FROM MessageTable
WHERE (fields & ?) = ?
""");
stmt.bind_int(0, Geary.Email.REQUIRED_FOR_MESSAGE);
stmt.bind_int(1, Geary.Email.REQUIRED_FOR_MESSAGE);
Db.Result select = stmt.exec();
while (!select.finished) {
int64 id = select.rowid_at(0);
Geary.Memory.Buffer header = select.string_buffer_at(1);
Geary.Memory.Buffer body = select.string_buffer_at(2);
try {
Geary.RFC822.Message message = new Geary.RFC822.Message.from_parts(
new RFC822.Header(header), new RFC822.Text(body));
Geary.Attachment.Disposition? target_disposition = null;
if (message.get_sub_messages().is_empty)
target_disposition = Geary.Attachment.Disposition.INLINE;
Geary.ImapDB.Folder.do_save_attachments_db(cx, id,
message.get_attachments(target_disposition), this, null);
} catch (Error e) {
debug("Error fetching inline Mime parts: %s", e.message);
}
select.next();
pump_event_loop();
}
// additionally, because this schema change (and code changes as well) introduces
// two new types of attachments as well as processing for all MIME text sections
// of messages (not just the first one), blow away the search table and let the
// search indexer start afresh
cx.exec("DELETE FROM MessageSearchTable");
return Db.TransactionOutcome.COMMIT;
});
} catch (Error e) {
debug("Error populating old inline attachments during upgrade to database schema 13: %s",
e.message);
}
}
......
......@@ -16,7 +16,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
PARTIAL_OK,
INCLUDE_MARKED_FOR_REMOVE,
INCLUDING_ID,
OLDEST_TO_NEWEST;
OLDEST_TO_NEWEST,
ONLY_INCOMPLETE;
public bool is_all_set(ListFlags flags) {
return (this & flags) == flags;
......@@ -281,6 +282,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
bool including_id = flags.is_all_set(ListFlags.INCLUDING_ID);
bool oldest_to_newest = flags.is_all_set(ListFlags.OLDEST_TO_NEWEST);
bool only_incomplete = flags.is_all_set(ListFlags.ONLY_INCOMPLETE);
int64 start;
if (initial_id != null) {
......@@ -301,24 +303,35 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
// database ... first, gather locations of all emails in database
Gee.List<LocationIdentifier> ids = new Gee.ArrayList<LocationIdentifier>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Statement stmt;
if (oldest_to_newest) {
stmt = cx.prepare("""
SELECT message_id, ordering
FROM MessageLocationTable
WHERE folder_id = ? AND ordering >= ?
ORDER BY ordering ASC
LIMIT ?
""");
} else {
stmt = cx.prepare("""
SELECT message_id, ordering
FROM MessageLocationTable
WHERE folder_id = ? AND ordering <= ?
ORDER BY ordering DESC
LIMIT ?
""");
StringBuilder sql = new StringBuilder("""
SELECT MessageLocationTable.message_id, ordering
FROM MessageLocationTable
""");
if (only_incomplete) {
sql.append("""
INNER JOIN MessageTable
ON MessageTable.id = MessageLocationTable.message_id
""");
}
sql.append("WHERE folder_id = ? ");
if (oldest_to_newest)
sql.append("AND ordering >= ? ");
else
sql.append("AND ordering <= ? ");
if (only_incomplete)
sql.append_printf("AND fields != %d ", Geary.Email.Field.ALL);
if (oldest_to_newest)
sql.append("ORDER BY ordering ASC ");
else
sql.append("ORDER BY ordering DESC ");
sql.append("LIMIT ?");
Db.Statement stmt = cx.prepare(sql.str);
stmt.bind_rowid(0, folder_id);
stmt.bind_int64(1, start);
stmt.bind_int(2, count);
......@@ -351,6 +364,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
Geary.EmailIdentifier end_id, Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable)
throws Error {
bool including_id = flags.is_all_set(ListFlags.INCLUDING_ID);
bool only_incomplete = flags.is_all_set(ListFlags.ONLY_INCOMPLETE);
int64 start = ((Geary.Imap.EmailIdentifier) start_id).uid.value;
if (!including_id)
......@@ -367,11 +381,23 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
// database ... first, gather locations of all emails in database
Gee.List<LocationIdentifier> ids = new Gee.ArrayList<LocationIdentifier>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Statement stmt = cx.prepare("""
SELECT message_id, ordering
StringBuilder sql = new StringBuilder("""
SELECT MessageLocationTable.message_id, ordering
FROM MessageLocationTable
WHERE folder_id = ? AND ordering >= ? AND ordering <= ?
""");
if (only_incomplete) {
sql.append("""
INNER JOIN MessageTable
ON MessageTable.id = MessageLocationTable.message_id
""");
}
sql.append("WHERE folder_id = ? AND ordering >= ? AND ordering <= ? ");
if (only_incomplete)
sql.append_printf(" AND fields != %d ", Geary.Email.Field.ALL);
Db.Statement stmt = cx.prepare(sql.str);
stmt.bind_rowid(0, folder_id);
stmt.bind_int64(1, start);
stmt.bind_int64(2, end);
......@@ -399,6 +425,70 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
return yield list_email_in_chunks_async(ids, required_fields, flags, cancellable);
}
public async Gee.List<Geary.Email>? list_email_by_sparse_id_async(Gee.Collection<Geary.EmailIdentifier> ids,
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error {
if (ids.size == 0)
return null;
bool only_incomplete = flags.is_all_set(ListFlags.ONLY_INCOMPLETE);
// Break up work so all reading isn't done in single transaction that locks up the
// database ... first, gather locations of all emails in database
Gee.List<LocationIdentifier> locations = new Gee.ArrayList<LocationIdentifier>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
StringBuilder sql = new StringBuilder("""
SELECT MessageLocationTable.message_id, ordering
FROM MessageLocationTable
""");
if (only_incomplete) {
sql.append("""
INNER JOIN MessageTable
ON MessageTable.id = MessageLocationTable.message_id
""");
}
sql.append("WHERE folder_id = ? ");
if (only_incomplete)
sql.append_printf(" AND fields != %d ", Geary.Email.Field.ALL);
sql.append("AND ordering IN (");
bool first = true;
foreach (Geary.EmailIdentifier id in ids) {
if (!first)
sql.append(", ");
sql.append(id.ordering.to_string());
first = false;
}
sql.append(")");
Db.Statement stmt = cx.prepare(sql.str);
stmt.bind_rowid(0, folder_id);
Db.Result results = stmt.exec(cancellable);
if (results.finished)
return Db.TransactionOutcome.SUCCESS;
do {
int64 ordering = results.int64_at(1);
Geary.EmailIdentifier email_id = new Imap.EmailIdentifier(new Imap.UID(ordering), path);
LocationIdentifier location = new LocationIdentifier(results.rowid_at(0), ordering,
path, email_id);