2485 lines
98 KiB
Vala
2485 lines
98 KiB
Vala
/* Copyright 2016 Software Freedom Conservancy Inc.
|
|
*
|
|
* This software is licensed under the GNU Lesser General Public License
|
|
* (version 2.1 or later). See the COPYING file in this distribution.
|
|
*/
|
|
|
|
/**
|
|
* ImapDB.Folder provides an interface for retrieving messages from the local store in methods
|
|
* that are synonymous with Geary.Folder's interface, but with some differences that deal with
|
|
* how IMAP addresses and organizes email.
|
|
*
|
|
* One important note about ImapDB.Folder: if an EmailIdentifier is returned (either by itself
|
|
* or attached to a Geary.Email), it will always be an ImapDB.EmailIdentifier and it will always
|
|
* have a valid Imap.UID present. This is not the case for EmailIdentifiers returned from
|
|
* ImapDB.Account, as those EmailIdentifiers aren't associated with a Folder, which UIDs require.
|
|
*/
|
|
|
|
private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|
|
|
/**
|
|
* Fields required for a message to be stored in the database.
|
|
*/
|
|
public const Geary.Email.Field REQUIRED_FIELDS = (
|
|
// Required for primary duplicate detection done with properties
|
|
Email.Field.PROPERTIES |
|
|
// Required for secondary duplicate detection via UID
|
|
Email.Field.REFERENCES |
|
|
// Required to ensure the unread count is up to date and so
|
|
// that when moving a message, the new copy turns back up as
|
|
// being not deleted.
|
|
Email.Field.FLAGS
|
|
);
|
|
|
|
/**
|
|
* Fields required for a message to be considered for full-text indexing.
|
|
*/
|
|
public const Geary.Email.Field REQUIRED_FTS_FIELDS = Geary.Email.REQUIRED_FOR_MESSAGE;
|
|
|
|
private const int LIST_EMAIL_WITH_MESSAGE_CHUNK_COUNT = 10;
|
|
private const int LIST_EMAIL_METADATA_COUNT = 100;
|
|
private const int LIST_EMAIL_FIELDS_CHUNK_COUNT = 500;
|
|
private const int REMOVE_COMPLETE_LOCATIONS_CHUNK_COUNT = 500;
|
|
private const int CREATE_MERGE_EMAIL_CHUNK_COUNT = 25;
|
|
private const int OLD_MSG_DETACH_BATCH_SIZE = 1000;
|
|
|
|
[Flags]
|
|
public enum ListFlags {
|
|
NONE = 0,
|
|
PARTIAL_OK,
|
|
INCLUDE_MARKED_FOR_REMOVE,
|
|
INCLUDING_ID,
|
|
OLDEST_TO_NEWEST,
|
|
ONLY_INCOMPLETE;
|
|
|
|
public bool is_all_set(ListFlags flags) {
|
|
return (this & flags) == flags;
|
|
}
|
|
|
|
public bool include_marked_for_remove() {
|
|
return is_all_set(INCLUDE_MARKED_FOR_REMOVE);
|
|
}
|
|
|
|
public static ListFlags from_folder_flags(Geary.Folder.ListFlags flags) {
|
|
ListFlags result = NONE;
|
|
|
|
if (flags.is_all_set(Geary.Folder.ListFlags.INCLUDING_ID))
|
|
result |= INCLUDING_ID;
|
|
|
|
if (flags.is_all_set(Geary.Folder.ListFlags.OLDEST_TO_NEWEST))
|
|
result |= OLDEST_TO_NEWEST;
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private class LocationIdentifier {
|
|
public int64 message_id;
|
|
public Imap.UID uid;
|
|
public ImapDB.EmailIdentifier email_id;
|
|
public bool marked_removed;
|
|
|
|
public LocationIdentifier(int64 message_id, Imap.UID uid, bool marked_removed) {
|
|
this.message_id = message_id;
|
|
this.uid = uid;
|
|
this.email_id = new ImapDB.EmailIdentifier(message_id, uid);
|
|
this.marked_removed = marked_removed;
|
|
}
|
|
}
|
|
|
|
protected int manual_ref_count { get; protected set; }
|
|
|
|
private Geary.Db.Database db;
|
|
private Geary.FolderPath path;
|
|
private GLib.File attachments_path;
|
|
private string account_owner_email;
|
|
private int64 folder_id;
|
|
private Geary.Imap.FolderProperties properties;
|
|
|
|
/**
|
|
* Fired after one or more emails have been fetched with all Fields, and
|
|
* saved locally.
|
|
*/
|
|
public signal void email_complete(Gee.Collection<Geary.EmailIdentifier> email_ids);
|
|
|
|
/**
|
|
* Fired when an email's unread (aka seen) status has changed. This allows the account to
|
|
* change the unread count for other folders that contain the email.
|
|
*/
|
|
public signal void unread_updated(Gee.Map<ImapDB.EmailIdentifier, bool> unread_status);
|
|
|
|
internal Folder(Geary.Db.Database db,
|
|
Geary.FolderPath path,
|
|
GLib.File attachments_path,
|
|
string account_owner_email,
|
|
int64 folder_id,
|
|
Geary.Imap.FolderProperties properties) {
|
|
this.db = db;
|
|
this.path = path;
|
|
this.attachments_path = attachments_path;
|
|
// Update to use all addresses on the account. Bug 768779
|
|
this.account_owner_email = account_owner_email;
|
|
this.folder_id = folder_id;
|
|
this.properties = properties;
|
|
}
|
|
|
|
public unowned Geary.FolderPath get_path() {
|
|
return path;
|
|
}
|
|
|
|
public Geary.Imap.FolderProperties get_properties() {
|
|
return properties;
|
|
}
|
|
|
|
internal void set_properties(Geary.Imap.FolderProperties properties) {
|
|
this.properties = properties;
|
|
}
|
|
|
|
public async int get_email_count_async(ListFlags flags, Cancellable? cancellable) throws Error {
|
|
int count = 0;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
count = do_get_email_count(cx, flags, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* 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())
|
|
* unless update_uid_info is true.
|
|
*/
|
|
public async void update_folder_status(Geary.Imap.FolderProperties remote_properties,
|
|
bool respect_marked_for_remove,
|
|
Cancellable? cancellable)
|
|
throws Error {
|
|
// adjust for marked remove, but don't write these adjustments to the database -- they're
|
|
// only reflected in memory via the properties
|
|
int adjust_unseen = 0;
|
|
int adjust_total = 0;
|
|
|
|
yield this.db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
|
if (respect_marked_for_remove) {
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT flags
|
|
FROM MessageTable
|
|
WHERE id IN (
|
|
SELECT message_id
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ? AND remove_marker = ?
|
|
)
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_bool(1, true);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
while (!results.finished) {
|
|
adjust_total++;
|
|
|
|
Imap.EmailFlags flags = new Imap.EmailFlags(Imap.MessageFlags.deserialize(
|
|
results.string_at(0)));
|
|
if (flags.contains(EmailFlags.UNREAD))
|
|
adjust_unseen++;
|
|
|
|
results.next(cancellable);
|
|
}
|
|
}
|
|
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE FolderTable SET attributes=?, unread_count=? WHERE id=?");
|
|
stmt.bind_string(0, remote_properties.attrs.serialize());
|
|
stmt.bind_int(1, remote_properties.email_unread);
|
|
stmt.bind_rowid(2, this.folder_id);
|
|
stmt.exec(cancellable);
|
|
|
|
if (remote_properties.status_messages >= 0) {
|
|
do_update_last_seen_status_total(
|
|
cx, remote_properties.status_messages, cancellable
|
|
);
|
|
}
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
|
|
// update appropriate local properties
|
|
this.properties.set_status_unseen(
|
|
Numeric.int_floor(remote_properties.unseen - adjust_unseen, 0)
|
|
);
|
|
this.properties.recent = remote_properties.recent;
|
|
this.properties.attrs = remote_properties.attrs;
|
|
|
|
// only update STATUS MESSAGES count if previously set, but use this count as the
|
|
// "authoritative" value until another SELECT/EXAMINE or MESSAGES response
|
|
if (remote_properties.status_messages >= 0) {
|
|
this.properties.set_status_message_count(
|
|
Numeric.int_floor(remote_properties.status_messages - adjust_total, 0),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(Geary.Imap.FolderProperties remote_properties,
|
|
Cancellable? cancellable)
|
|
throws Error {
|
|
yield this.db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
|
do_update_uid_info(cx, remote_properties, cancellable);
|
|
|
|
if (remote_properties.select_examine_messages >= 0) {
|
|
do_update_last_seen_select_examine_total(
|
|
cx, remote_properties.select_examine_messages, cancellable
|
|
);
|
|
}
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
|
|
// update appropriate local properties
|
|
this.properties.set_status_unseen(remote_properties.unseen);
|
|
this.properties.recent = remote_properties.recent;
|
|
this.properties.uid_validity = remote_properties.uid_validity;
|
|
this.properties.uid_next = remote_properties.uid_next;
|
|
|
|
if (remote_properties.select_examine_messages >= 0) {
|
|
this.properties.set_select_examine_message_count(
|
|
remote_properties.select_examine_messages
|
|
);
|
|
}
|
|
}
|
|
|
|
// Updates both the FolderProperties and the value in the local store. Must be called while
|
|
// open.
|
|
public async void update_remote_selected_message_count(int count, Cancellable? cancellable) throws Error {
|
|
if (count < 0)
|
|
return;
|
|
|
|
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
|
do_update_last_seen_select_examine_total(cx, count, cancellable);
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
|
|
properties.set_select_examine_message_count(count);
|
|
}
|
|
|
|
// Returns a Map with the created or merged email as the key and the result of the operation
|
|
// (true if created, false if merged) as the value. Note that every email
|
|
// object passed in's EmailIdentifier will be fully filled out by this
|
|
// function (see ImapDB.EmailIdentifier.promote_with_message_id). This
|
|
// means if you've hashed the collection of EmailIdentifiers prior, you may
|
|
// not be able to find them after this function. Be warned.
|
|
public async Gee.Map<Email, bool>
|
|
create_or_merge_email_async(Gee.Collection<Email> emails,
|
|
bool update_totals,
|
|
ContactHarvester harvester,
|
|
GLib.Cancellable? cancellable)
|
|
throws GLib.Error {
|
|
Gee.HashMap<Geary.Email, bool> results = new Gee.HashMap<Geary.Email, bool>();
|
|
|
|
Gee.ArrayList<Geary.Email> list = traverse<Geary.Email>(emails).to_array_list();
|
|
int index = 0;
|
|
while (index < list.size) {
|
|
int stop = Numeric.int_ceiling(index + CREATE_MERGE_EMAIL_CHUNK_COUNT, list.size);
|
|
Gee.List<Geary.Email> slice = list.slice(index, stop);
|
|
|
|
Gee.ArrayList<Geary.EmailIdentifier> complete_ids = new Gee.ArrayList<Geary.EmailIdentifier>();
|
|
int total_unread_change = 0;
|
|
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
|
foreach (Geary.Email email in slice) {
|
|
Geary.Email.Field pre_fields;
|
|
Geary.Email.Field post_fields;
|
|
int unread_change = 0;
|
|
bool created = do_create_or_merge_email(
|
|
cx, email,
|
|
out pre_fields, out post_fields,
|
|
ref unread_change,
|
|
cancellable
|
|
);
|
|
|
|
results.set(email, created);
|
|
|
|
// in essence, only fire the "email-completed" signal if the local version didn't
|
|
// have all the fields but after the create/merge now does
|
|
if (post_fields.is_all_set(Geary.Email.Field.ALL) && !pre_fields.is_all_set(Geary.Email.Field.ALL))
|
|
complete_ids.add(email.id);
|
|
|
|
if (update_totals) {
|
|
// Update unread count in DB.
|
|
do_add_to_unread_count(cx, unread_change, cancellable);
|
|
total_unread_change += unread_change;
|
|
}
|
|
}
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
|
|
if (update_totals) {
|
|
// Update the email_unread properties.
|
|
properties.set_status_unseen(
|
|
(properties.email_unread + total_unread_change).clamp(0, int.MAX)
|
|
);
|
|
}
|
|
|
|
if (complete_ids.size > 0)
|
|
email_complete(complete_ids);
|
|
|
|
index = stop;
|
|
if (index < list.size)
|
|
yield Scheduler.sleep_ms_async(100);
|
|
}
|
|
|
|
yield harvester.harvest_from_email(
|
|
results.keys, cancellable
|
|
);
|
|
|
|
return results;
|
|
}
|
|
|
|
public async Gee.List<Geary.Email>? list_email_by_id_async(ImapDB.EmailIdentifier? initial_id,
|
|
int count, Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable)
|
|
throws Error {
|
|
if (count <= 0)
|
|
return null;
|
|
|
|
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);
|
|
|
|
// 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 = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
// convert initial_id into UID to start walking the list
|
|
Imap.UID? start_uid = null;
|
|
if (initial_id != null) {
|
|
// use INCLUDE_MARKED_FOR_REMOVE because this is a ranged list ...
|
|
// do_results_to_location() will deal with removing EmailIdentifiers if necessary
|
|
LocationIdentifier? location = do_get_location_for_id(cx, initial_id,
|
|
ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
|
if (location == null)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
start_uid = location.uid;
|
|
|
|
// deal with exclusive searches
|
|
if (!including_id) {
|
|
if (oldest_to_newest)
|
|
start_uid = start_uid.next(false);
|
|
else
|
|
start_uid = start_uid.previous(false);
|
|
}
|
|
} else if (oldest_to_newest) {
|
|
start_uid = new Imap.UID(Imap.UID.MIN);
|
|
} else {
|
|
start_uid = new Imap.UID(Imap.UID.MAX);
|
|
}
|
|
|
|
if (!start_uid.is_valid())
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
StringBuilder sql = new StringBuilder("""
|
|
SELECT MessageLocationTable.message_id, ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ?
|
|
""");
|
|
|
|
if (oldest_to_newest)
|
|
sql.append("AND ordering >= ? ");
|
|
else
|
|
sql.append("AND ordering <= ? ");
|
|
|
|
if (oldest_to_newest)
|
|
sql.append("ORDER BY ordering ASC ");
|
|
else
|
|
sql.append("ORDER BY ordering DESC ");
|
|
|
|
if (count != int.MAX)
|
|
sql.append("LIMIT ? ");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, start_uid.value);
|
|
if (count != int.MAX)
|
|
stmt.bind_int(2, count);
|
|
|
|
locations = do_results_to_locations(stmt.exec(cancellable), count, flags, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
// remove complete locations (emails with all fields downloaded)
|
|
if (only_incomplete)
|
|
locations = yield remove_complete_locations_in_chunks_async(locations, cancellable);
|
|
|
|
// Next, read in email in chunks
|
|
return yield list_email_in_chunks_async(locations, required_fields, flags, cancellable);
|
|
}
|
|
|
|
// ListFlags.OLDEST_TO_NEWEST is ignored. INCLUDING_ID means including *both* identifiers.
|
|
// Without this flag, neither are considered as part of the range.
|
|
public async Gee.List<Geary.Email>? list_email_by_range_async(ImapDB.EmailIdentifier start_id,
|
|
ImapDB.EmailIdentifier end_id, Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable)
|
|
throws Error {
|
|
bool including_id = flags.is_all_set(ListFlags.INCLUDING_ID);
|
|
|
|
// 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 = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
// use INCLUDE_MARKED_FOR_REMOVE because this is a ranged list ...
|
|
// do_results_to_location() will deal with removing EmailIdentifiers if necessary
|
|
LocationIdentifier? start_location = do_get_location_for_id(cx, start_id,
|
|
ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
|
if (start_location == null)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
Imap.UID start_uid = start_location.uid;
|
|
|
|
// see note above about INCLUDE_MARKED_FOR_REMOVE
|
|
LocationIdentifier? end_location = do_get_location_for_id(cx, end_id,
|
|
ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
|
if (end_location == null)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
Imap.UID end_uid = end_location.uid;
|
|
|
|
if (!including_id) {
|
|
start_uid = start_uid.next(false);
|
|
end_uid = end_uid.previous(false);
|
|
}
|
|
|
|
if (!start_uid.is_valid() || !end_uid.is_valid() || start_uid.compare_to(end_uid) > 0)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT message_id, ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ? AND ordering >= ? AND ordering <= ?
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, start_uid.value);
|
|
stmt.bind_int64(2, end_uid.value);
|
|
|
|
locations = do_results_to_locations(stmt.exec(cancellable), int.MAX, flags, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
// Next, read in email in chunks
|
|
return yield list_email_in_chunks_async(locations, required_fields, flags, cancellable);
|
|
}
|
|
|
|
// ListFlags.OLDEST_TO_NEWEST is ignored. INCLUDING_ID means including *both* identifiers.
|
|
// Without this flag, neither are considered as part of the range.
|
|
public async Gee.List<Geary.Email>? list_email_by_uid_range_async(Imap.UID start,
|
|
Imap.UID end, 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);
|
|
|
|
Imap.UID start_uid = start;
|
|
Imap.UID end_uid = end;
|
|
|
|
if (!including_id) {
|
|
start_uid = start_uid.next(false);
|
|
end_uid = end_uid.previous(false);
|
|
}
|
|
|
|
if (!start_uid.is_valid() || !end_uid.is_valid() || start_uid.compare_to(end_uid) > 0)
|
|
return null;
|
|
|
|
// 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 = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
StringBuilder sql = new StringBuilder("""
|
|
SELECT MessageLocationTable.message_id, ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
""");
|
|
|
|
sql.append("WHERE folder_id = ? AND ordering >= ? AND ordering <= ? ");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, start_uid.value);
|
|
stmt.bind_int64(2, end_uid.value);
|
|
|
|
locations = do_results_to_locations(stmt.exec(cancellable), int.MAX, flags, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
// remove complete locations (emails with all fields downloaded)
|
|
if (only_incomplete)
|
|
locations = yield remove_complete_locations_in_chunks_async(locations, cancellable);
|
|
|
|
// Next, read in email in chunks
|
|
return yield list_email_in_chunks_async(locations, required_fields, flags, cancellable);
|
|
}
|
|
|
|
public async Gee.List<Geary.Email>? list_email_by_sparse_id_async(Gee.Collection<ImapDB.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) => {
|
|
// convert ids into LocationIdentifiers
|
|
Gee.List<LocationIdentifier>? locs = do_get_locations_for_ids(cx, ids, flags,
|
|
cancellable);
|
|
if (locs == null || locs.size == 0)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
StringBuilder sql = new StringBuilder("""
|
|
SELECT MessageLocationTable.message_id, ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
""");
|
|
|
|
if (locs.size != 1) {
|
|
sql.append("WHERE ordering IN (");
|
|
bool first = true;
|
|
foreach (LocationIdentifier location in locs) {
|
|
if (!first)
|
|
sql.append(",");
|
|
|
|
sql.append(location.uid.to_string());
|
|
first = false;
|
|
}
|
|
sql.append(")");
|
|
} else {
|
|
sql.append_printf("WHERE ordering = '%s' ", locs[0].uid.to_string());
|
|
}
|
|
|
|
sql.append("AND folder_id = ? ");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
locations = do_results_to_locations(stmt.exec(cancellable), int.MAX, flags, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
// remove complete locations (emails with all fields downloaded)
|
|
if (only_incomplete)
|
|
locations = yield remove_complete_locations_in_chunks_async(locations, cancellable);
|
|
|
|
// Next, read in email in chunks
|
|
return yield list_email_in_chunks_async(locations, required_fields, flags, cancellable);
|
|
}
|
|
|
|
private async Gee.List<LocationIdentifier>? remove_complete_locations_in_chunks_async(
|
|
Gee.List<LocationIdentifier>? locations, Cancellable? cancellable) throws Error {
|
|
if (locations == null || locations.size == 0)
|
|
return locations;
|
|
|
|
Gee.List<LocationIdentifier> incomplete_locations = new Gee.ArrayList<LocationIdentifier>();
|
|
|
|
// remove complete locations in chunks to avoid locking the database for long periods of
|
|
// time
|
|
int start = 0;
|
|
for (;;) {
|
|
if (start >= locations.size)
|
|
break;
|
|
|
|
int end = (start + REMOVE_COMPLETE_LOCATIONS_CHUNK_COUNT).clamp(0, locations.size);
|
|
Gee.List<LocationIdentifier> slice = locations.slice(start, end);
|
|
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
do_remove_complete_locations(cx, slice, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
incomplete_locations.add_all(slice);
|
|
|
|
start = end;
|
|
}
|
|
|
|
return (incomplete_locations.size > 0) ? incomplete_locations : null;
|
|
}
|
|
|
|
private async Gee.List<Geary.Email>? list_email_in_chunks_async(Gee.List<LocationIdentifier>? ids,
|
|
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error {
|
|
if (ids == null || ids.size == 0)
|
|
return null;
|
|
|
|
// chunk count depends on whether or not the message -- body + headers -- is being fetched
|
|
int chunk_count = required_fields.requires_any(Email.Field.BODY | Email.Field.HEADER)
|
|
? LIST_EMAIL_WITH_MESSAGE_CHUNK_COUNT : LIST_EMAIL_METADATA_COUNT;
|
|
|
|
int length_rounded_up = Numeric.int_round_up(ids.size, chunk_count);
|
|
|
|
Gee.List<Geary.Email> results = new Gee.ArrayList<Geary.Email>();
|
|
for (int start = 0; start < length_rounded_up; start += chunk_count) {
|
|
// stop is the index *after* the end of the slice
|
|
int stop = Numeric.int_ceiling((start + chunk_count), ids.size);
|
|
|
|
Gee.List<LocationIdentifier>? slice = ids.slice(start, stop);
|
|
assert(slice != null && slice.size > 0);
|
|
|
|
Gee.List<Geary.Email>? list = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => {
|
|
list = do_list_email(cx, slice, required_fields, flags, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
if (list != null)
|
|
results.add_all(list);
|
|
}
|
|
|
|
if (results.size != ids.size)
|
|
debug("list_email_in_chunks_async: Requested %d email, returned %d", ids.size, results.size);
|
|
|
|
return (results.size > 0) ? results : null;
|
|
}
|
|
|
|
public async Geary.Email fetch_email_async(ImapDB.EmailIdentifier id,
|
|
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error {
|
|
Geary.Email? email = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
LocationIdentifier? location = do_get_location_for_id(cx, id, flags, cancellable);
|
|
if (location == null)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
email = do_location_to_email(cx, location, required_fields, flags, cancellable);
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
if (email == null) {
|
|
throw new EngineError.NOT_FOUND("No message ID %s in folder %s", id.to_string(),
|
|
to_string());
|
|
}
|
|
|
|
return email;
|
|
}
|
|
|
|
// Note that this does INCLUDES messages marked for removal
|
|
// TODO: Let the user request a SortedSet, or have them provide the Set to add to
|
|
public async Gee.Set<Imap.UID>? list_uids_by_range_async(Imap.UID first_uid, Imap.UID last_uid,
|
|
bool include_marked_for_removal, Cancellable? cancellable) throws Error {
|
|
// order correctly
|
|
Imap.UID start, end;
|
|
if (first_uid.compare_to(last_uid) < 0) {
|
|
start = first_uid;
|
|
end = last_uid;
|
|
} else {
|
|
start = last_uid;
|
|
end = first_uid;
|
|
}
|
|
|
|
Gee.Set<Imap.UID> uids = new Gee.HashSet<Imap.UID>();
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ? AND ordering >= ? AND ordering <= ?
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, start.value);
|
|
stmt.bind_int64(2, end.value);
|
|
|
|
Db.Result result = stmt.exec(cancellable);
|
|
while (!result.finished) {
|
|
if (include_marked_for_removal || !result.bool_at(1))
|
|
uids.add(new Imap.UID(result.int64_at(0)));
|
|
|
|
result.next(cancellable);
|
|
}
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return (uids.size > 0) ? uids : null;
|
|
}
|
|
|
|
// pos is 1-based. This method does not respect messages marked for removal.
|
|
public async ImapDB.EmailIdentifier? get_id_at_async(int64 pos, Cancellable? cancellable) throws Error {
|
|
assert(pos >= 1);
|
|
|
|
ImapDB.EmailIdentifier? id = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT message_id, ordering
|
|
FROM MessageLocationTable
|
|
WHERE folder_id=?
|
|
ORDER BY ordering
|
|
LIMIT 1
|
|
OFFSET ?
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, pos - 1);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
if (!results.finished)
|
|
id = new ImapDB.EmailIdentifier(results.rowid_at(0), new Imap.UID(results.int64_at(1)));
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return id;
|
|
}
|
|
|
|
public async Imap.UID? get_uid_async(ImapDB.EmailIdentifier id, ListFlags flags,
|
|
Cancellable? cancellable) throws Error {
|
|
// Always look up the UID rather than pull the one from the EmailIdentifier; it could be
|
|
// for another Folder
|
|
Imap.UID? uid = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
LocationIdentifier? location = do_get_location_for_id(cx, id, flags, cancellable);
|
|
if (location != null)
|
|
uid = location.uid;
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return uid;
|
|
}
|
|
|
|
public async Gee.Set<Imap.UID>? get_uids_async(Gee.Collection<ImapDB.EmailIdentifier> ids,
|
|
ListFlags flags, Cancellable? cancellable) throws Error {
|
|
// Always look up the UID rather than pull the one from the EmailIdentifier; it could be
|
|
// for another Folder
|
|
Gee.Set<Imap.UID> uids = new Gee.HashSet<Imap.UID>();
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
Gee.List<LocationIdentifier>? locs = do_get_locations_for_ids(cx, ids, flags,
|
|
cancellable);
|
|
if (locs != null) {
|
|
foreach (LocationIdentifier location in locs)
|
|
uids.add(location.uid);
|
|
}
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return (uids.size > 0) ? uids : null;
|
|
}
|
|
|
|
// Returns null if the UID is not found in this Folder.
|
|
public async ImapDB.EmailIdentifier? get_id_async(Imap.UID uid, ListFlags flags,
|
|
Cancellable? cancellable) throws Error {
|
|
ImapDB.EmailIdentifier? id = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
LocationIdentifier? location = do_get_location_for_uid(cx, uid, flags,
|
|
cancellable);
|
|
if (location != null)
|
|
id = location.email_id;
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return id;
|
|
}
|
|
|
|
public async Gee.Set<ImapDB.EmailIdentifier>? get_ids_async(Gee.Collection<Imap.UID> uids,
|
|
ListFlags flags, Cancellable? cancellable) throws Error {
|
|
Gee.Set<ImapDB.EmailIdentifier> ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
Gee.List<LocationIdentifier>? locs = do_get_locations_for_uids(cx, uids, flags,
|
|
cancellable);
|
|
if (locs != null) {
|
|
foreach (LocationIdentifier location in locs)
|
|
ids.add(location.email_id);
|
|
}
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return (ids.size > 0) ? ids : null;
|
|
}
|
|
|
|
// This does not respect messages marked for removal.
|
|
public async ImapDB.EmailIdentifier? get_earliest_id_async(Cancellable? cancellable) throws Error {
|
|
return yield get_id_extremes_async(true, cancellable);
|
|
}
|
|
|
|
// This does not respect messages marked for removal.
|
|
public async ImapDB.EmailIdentifier? get_latest_id_async(Cancellable? cancellable) throws Error {
|
|
return yield get_id_extremes_async(false, cancellable);
|
|
}
|
|
|
|
private async ImapDB.EmailIdentifier? get_id_extremes_async(bool earliest, Cancellable? cancellable)
|
|
throws Error {
|
|
ImapDB.EmailIdentifier? id = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
Db.Statement stmt;
|
|
if (earliest)
|
|
stmt = cx.prepare("SELECT MIN(ordering), message_id FROM MessageLocationTable WHERE folder_id=?");
|
|
else
|
|
stmt = cx.prepare("SELECT MAX(ordering), message_id FROM MessageLocationTable WHERE folder_id=?");
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
// MIN and MAX return NULL if the result set being examined is zero-length
|
|
if (!results.finished && !results.is_null_at(0))
|
|
id = new ImapDB.EmailIdentifier(results.rowid_at(1), new Imap.UID(results.int64_at(0)));
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return id;
|
|
}
|
|
|
|
public async void detach_multiple_emails_async(Gee.Collection<ImapDB.EmailIdentifier> ids,
|
|
Cancellable? cancellable) throws Error {
|
|
int unread_count = 0;
|
|
// TODO: Right now, deleting an email is merely detaching its association with a folder
|
|
// (since it may be located in multiple folders). This means at some point in the future
|
|
// a vacuum will be required to remove emails that are completely unassociated with the
|
|
// account.
|
|
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
|
Gee.List<LocationIdentifier>? locs = do_get_locations_for_ids(cx, ids,
|
|
ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
|
if (locs == null || locs.size == 0)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
unread_count = do_get_unread_count_for_ids(cx, ids, cancellable);
|
|
do_add_to_unread_count(cx, -unread_count, cancellable);
|
|
|
|
StringBuilder sql = new StringBuilder("""
|
|
DELETE FROM MessageLocationTable WHERE message_id IN (
|
|
""");
|
|
Gee.Iterator<LocationIdentifier> iter = locs.iterator();
|
|
while (iter.next()) {
|
|
sql.append_printf("%s", iter.get().message_id.to_string());
|
|
if (iter.has_next())
|
|
sql.append(", ");
|
|
}
|
|
sql.append(") AND folder_id=?");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
stmt.exec(cancellable);
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
|
|
if (unread_count > 0)
|
|
properties.set_status_unseen(properties.email_unread - unread_count);
|
|
}
|
|
|
|
public async void detach_all_emails_async(Cancellable? cancellable) throws Error {
|
|
yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => {
|
|
Db.Statement stmt = cx.prepare(
|
|
"DELETE FROM MessageLocationTable WHERE folder_id=?");
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
stmt.exec(cancellable);
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
}
|
|
|
|
public async Gee.Collection<Geary.EmailIdentifier>? detach_emails_before_timestamp(DateTime cutoff,
|
|
GLib.Cancellable? cancellable) throws Error {
|
|
debug("Detaching emails before %s for folder ID %s", cutoff.to_string(), this.folder_id.to_string());
|
|
Gee.ArrayList<ImapDB.EmailIdentifier>? deleted_email_ids = null;
|
|
Gee.ArrayList<string> deleted_primary_keys = null;
|
|
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
// MessageLocationTable.ordering isn't relied on due to IMAP folder
|
|
// UIDs not guaranteed to be in order.
|
|
StringBuilder sql = new StringBuilder();
|
|
sql.append("""
|
|
SELECT id, message_id, ordering
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ?
|
|
AND message_id IN (
|
|
SELECT id
|
|
FROM MessageTable
|
|
INDEXED BY MessageTableInternalDateTimeTIndex
|
|
WHERE internaldate_time_t < ?
|
|
)
|
|
""");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, cutoff.to_unix());
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
|
|
while (!results.finished) {
|
|
if (deleted_email_ids == null) {
|
|
deleted_email_ids = new Gee.ArrayList<ImapDB.EmailIdentifier>();
|
|
deleted_primary_keys = new Gee.ArrayList<string>();
|
|
}
|
|
|
|
deleted_email_ids.add(
|
|
new ImapDB.EmailIdentifier(results.int64_at(1),
|
|
new Imap.UID(results.int64_at(2)))
|
|
);
|
|
deleted_primary_keys.add(results.rowid_at(0).to_string());
|
|
|
|
results.next(cancellable);
|
|
}
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
if (deleted_email_ids != null) {
|
|
// Delete in batches to avoid hiting SQLite maximum query
|
|
// length (although quite unlikely)
|
|
int delete_index = 0;
|
|
while (delete_index < deleted_primary_keys.size) {
|
|
int batch_counter = 0;
|
|
|
|
StringBuilder message_location_ids_sql_sublist = new StringBuilder();
|
|
StringBuilder message_ids_sql_sublist = new StringBuilder();
|
|
while (delete_index < deleted_primary_keys.size
|
|
&& batch_counter < OLD_MSG_DETACH_BATCH_SIZE) {
|
|
if (batch_counter > 0) {
|
|
message_location_ids_sql_sublist.append(",");
|
|
message_ids_sql_sublist.append(",");
|
|
}
|
|
message_location_ids_sql_sublist.append(
|
|
deleted_primary_keys.get(delete_index)
|
|
);
|
|
message_ids_sql_sublist.append(
|
|
deleted_email_ids.get(delete_index).message_id.to_string()
|
|
);
|
|
delete_index++;
|
|
batch_counter++;
|
|
}
|
|
|
|
yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => {
|
|
StringBuilder sql = new StringBuilder();
|
|
sql.append("""
|
|
DELETE FROM MessageLocationTable
|
|
WHERE id IN (
|
|
""");
|
|
sql.append(message_location_ids_sql_sublist.str);
|
|
sql.append(")");
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
|
|
stmt.exec(cancellable);
|
|
|
|
sql = new StringBuilder();
|
|
sql.append("""
|
|
DELETE FROM MessageSearchTable
|
|
WHERE docid IN (
|
|
""");
|
|
sql.append(message_ids_sql_sublist.str);
|
|
sql.append(")");
|
|
stmt = cx.prepare(sql.str);
|
|
|
|
stmt.exec(cancellable);
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
}
|
|
}
|
|
|
|
return deleted_email_ids;
|
|
}
|
|
|
|
public async void mark_email_async(Gee.Collection<ImapDB.EmailIdentifier> to_mark,
|
|
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, Cancellable? cancellable)
|
|
throws Error {
|
|
int unread_change = 0; // Negative means messages are read, positive means unread.
|
|
Gee.Map<ImapDB.EmailIdentifier, bool> unread_status = new Gee.HashMap<ImapDB.EmailIdentifier, bool>();
|
|
yield db.exec_transaction_async(Db.TransactionType.RW, (cx, cancellable) => {
|
|
// fetch flags for each email
|
|
Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? map = do_get_email_flags(cx,
|
|
to_mark, cancellable);
|
|
if (map == null)
|
|
return Db.TransactionOutcome.COMMIT;
|
|
|
|
// update flags according to arguments
|
|
foreach (ImapDB.EmailIdentifier id in map.keys) {
|
|
Geary.Imap.EmailFlags flags = ((Geary.Imap.EmailFlags) map.get(id));
|
|
|
|
if (flags_to_add != null) {
|
|
foreach (Geary.NamedFlag flag in flags_to_add.get_all()) {
|
|
if (flags.contains(flag))
|
|
continue;
|
|
|
|
flags.add(flag);
|
|
|
|
if (flag.equal_to(Geary.EmailFlags.UNREAD)) {
|
|
unread_change++;
|
|
unread_status.set(id, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (flags_to_remove != null) {
|
|
foreach (Geary.NamedFlag flag in flags_to_remove.get_all()) {
|
|
if (!flags.contains(flag))
|
|
continue;
|
|
|
|
flags.remove(flag);
|
|
|
|
if (flag.equal_to(Geary.EmailFlags.UNREAD)) {
|
|
unread_change--;
|
|
unread_status.set(id, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// write them all back out
|
|
do_set_email_flags(cx, map, cancellable);
|
|
|
|
// Update unread count.
|
|
do_add_to_unread_count(cx, unread_change, cancellable);
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
|
|
// Update the email_unread properties.
|
|
properties.set_status_unseen((properties.email_unread + unread_change).clamp(0, int.MAX));
|
|
|
|
// Signal changes so other folders can be updated.
|
|
if (unread_status.size > 0)
|
|
unread_updated(unread_status);
|
|
}
|
|
|
|
internal async Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? get_email_flags_async(
|
|
Gee.Collection<EmailIdentifier> ids, Cancellable? cancellable) throws Error {
|
|
Gee.Map<EmailIdentifier, Geary.EmailFlags>? map = null;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => {
|
|
map = do_get_email_flags(cx, ids, cancellable);
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
return map;
|
|
}
|
|
|
|
public async void set_email_flags_async(Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags> map,
|
|
Cancellable? cancellable) throws Error {
|
|
Error? error = null;
|
|
int unread_change = 0; // Negative means messages are read, positive means unread.
|
|
|
|
try {
|
|
yield db.exec_transaction_async(Db.TransactionType.RW, (cx, cancellable) => {
|
|
// TODO get current flags, compare to ones being set
|
|
Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? existing_map =
|
|
do_get_email_flags(cx, map.keys, cancellable);
|
|
|
|
if (existing_map != null) {
|
|
foreach(ImapDB.EmailIdentifier id in map.keys) {
|
|
Geary.EmailFlags? existing_flags = existing_map.get(id);
|
|
if (existing_flags == null)
|
|
continue;
|
|
|
|
Geary.EmailFlags new_flags = map.get(id);
|
|
if (!existing_flags.contains(Geary.EmailFlags.UNREAD) &&
|
|
new_flags.contains(Geary.EmailFlags.UNREAD))
|
|
unread_change++;
|
|
else if (existing_flags.contains(Geary.EmailFlags.UNREAD) &&
|
|
!new_flags.contains(Geary.EmailFlags.UNREAD))
|
|
unread_change--;
|
|
}
|
|
}
|
|
|
|
do_set_email_flags(cx, map, cancellable);
|
|
|
|
// Update unread count.
|
|
do_add_to_unread_count(cx, unread_change, cancellable);
|
|
|
|
// TODO set db unread count
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
} catch (Error e) {
|
|
error = e;
|
|
}
|
|
|
|
// Update the email_unread properties.
|
|
if (error == null) {
|
|
properties.set_status_unseen((properties.email_unread + unread_change).clamp(0, int.MAX));
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async void detach_single_email_async(ImapDB.EmailIdentifier id, Cancellable? cancellable,
|
|
out bool is_marked) throws Error {
|
|
bool internal_is_marked = false;
|
|
bool was_unread = false;
|
|
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
|
LocationIdentifier? location = do_get_location_for_id(cx, id, ListFlags.INCLUDE_MARKED_FOR_REMOVE,
|
|
cancellable);
|
|
if (location == null) {
|
|
throw new EngineError.NOT_FOUND("Message %s cannot be removed from %s: not found",
|
|
id.to_string(), to_string());
|
|
}
|
|
|
|
// Check to see if message is unread (this only affects non-marked emails.)
|
|
if (do_get_unread_count_for_ids(cx,
|
|
Geary.iterate<ImapDB.EmailIdentifier>(id).to_array_list(), cancellable) > 0) {
|
|
do_add_to_unread_count(cx, -1, cancellable);
|
|
was_unread = true;
|
|
}
|
|
|
|
internal_is_marked = location.marked_removed;
|
|
|
|
do_remove_association_with_folder(cx, location, cancellable);
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
|
|
is_marked = internal_is_marked;
|
|
|
|
if (was_unread)
|
|
properties.set_status_unseen(properties.email_unread - 1);
|
|
}
|
|
|
|
// Mark messages as removed (but not expunged) from the folder. Marked messages are skipped
|
|
// on most operations unless ListFlags.INCLUDE_MARKED_REMOVED is true. Use detach_email_async()
|
|
// to formally remove the messages from the folder.
|
|
//
|
|
// If ids is null, all messages are marked for removal.
|
|
//
|
|
// Returns a collection of ImapDB.EmailIdentifiers *with the UIDs set* for this folder.
|
|
// Supplied EmailIdentifiers not in this Folder will not be included.
|
|
public async Gee.Set<ImapDB.EmailIdentifier>? mark_removed_async(
|
|
Gee.Collection<ImapDB.EmailIdentifier>? ids, bool mark_removed, Cancellable? cancellable)
|
|
throws Error {
|
|
int total_changed = 0;
|
|
int unread_count = 0;
|
|
Gee.Set<ImapDB.EmailIdentifier> removed_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
|
|
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
|
Gee.List<LocationIdentifier?> locs;
|
|
if (ids != null)
|
|
locs = do_get_locations_for_ids(cx, ids, ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
|
else
|
|
locs = do_get_all_locations(cx, ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
|
|
|
|
if (locs == null || locs.size == 0)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
total_changed = locs.size;
|
|
unread_count = do_get_unread_count_for_ids(cx, ids, cancellable);
|
|
|
|
Gee.HashSet<Imap.UID> uids = new Gee.HashSet<Imap.UID>();
|
|
foreach (LocationIdentifier location in locs) {
|
|
uids.add(location.uid);
|
|
removed_ids.add(location.email_id);
|
|
}
|
|
|
|
do_mark_unmark_removed(cx, uids, mark_removed, cancellable);
|
|
do_add_to_unread_count(cx, -unread_count, cancellable);
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
|
|
// Update the folder properties so client sees the changes
|
|
// right away
|
|
|
|
// Email total
|
|
if (mark_removed) {
|
|
total_changed = -total_changed;
|
|
}
|
|
int total = this.properties.select_examine_messages + total_changed;
|
|
if (total >= 0) {
|
|
this.properties.set_select_examine_message_count(total);
|
|
}
|
|
|
|
// Unread total
|
|
if (unread_count > 0)
|
|
properties.set_status_unseen(properties.email_unread - unread_count);
|
|
|
|
return (removed_ids.size > 0) ? removed_ids : null;
|
|
}
|
|
|
|
// Returns the number of messages marked for removal in this folder
|
|
public async int get_marked_for_remove_count_async(Cancellable? cancellable) throws Error {
|
|
int count = 0;
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
count = do_get_marked_removed_count(cx, cancellable);
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return count;
|
|
}
|
|
|
|
public async Gee.Set<ImapDB.EmailIdentifier>? get_marked_ids_async(Cancellable? cancellable)
|
|
throws Error {
|
|
Gee.Set<ImapDB.EmailIdentifier> ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT message_id, ordering
|
|
FROM MessageLocationTable
|
|
WHERE folder_id=? AND remove_marker<>?
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_bool(1, false);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
while (!results.finished) {
|
|
ids.add(new ImapDB.EmailIdentifier(results.rowid_at(0), new Imap.UID(results.int64_at(1))));
|
|
|
|
results.next(cancellable);
|
|
}
|
|
|
|
return Db.TransactionOutcome.DONE;
|
|
}, cancellable);
|
|
|
|
return ids.size > 0 ? ids : null;
|
|
}
|
|
|
|
// Clears all remove markers from the folder except those in the exceptions Collection
|
|
public async void clear_remove_markers_async(Gee.Collection<ImapDB.EmailIdentifier>? exceptions,
|
|
Cancellable? cancellable) throws Error {
|
|
yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => {
|
|
StringBuilder sql = new StringBuilder();
|
|
sql.append("""
|
|
UPDATE MessageLocationTable
|
|
SET remove_marker=?
|
|
WHERE folder_id=? AND remove_marker <> ?
|
|
""");
|
|
|
|
if (exceptions != null && exceptions.size > 0) {
|
|
sql.append("""
|
|
AND message_id NOT IN (
|
|
""");
|
|
Gee.Iterator<ImapDB.EmailIdentifier> iter = exceptions.iterator();
|
|
while (iter.next()) {
|
|
sql.append(iter.get().message_id.to_string());
|
|
if (iter.has_next())
|
|
sql.append(", ");
|
|
}
|
|
sql.append(")");
|
|
}
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_bool(0, false);
|
|
stmt.bind_rowid(1, folder_id);
|
|
stmt.bind_bool(2, false);
|
|
|
|
stmt.exec(cancellable);
|
|
|
|
return Db.TransactionOutcome.COMMIT;
|
|
}, cancellable);
|
|
}
|
|
|
|
public async Gee.Map<ImapDB.EmailIdentifier, Geary.Email.Field>? list_email_fields_by_id_async(
|
|
Gee.Collection<ImapDB.EmailIdentifier> ids, ListFlags flags, Cancellable? cancellable)
|
|
throws Error {
|
|
if (ids.size == 0)
|
|
return null;
|
|
|
|
Gee.HashMap<ImapDB.EmailIdentifier,Geary.Email.Field> map = new Gee.HashMap<
|
|
ImapDB.EmailIdentifier,Geary.Email.Field>();
|
|
|
|
// Break up the work
|
|
Gee.List<ImapDB.EmailIdentifier> list = new Gee.ArrayList<ImapDB.EmailIdentifier>();
|
|
Gee.Iterator<ImapDB.EmailIdentifier> iter = ids.iterator();
|
|
while (iter.next()) {
|
|
list.add(iter.get());
|
|
if (list.size < LIST_EMAIL_FIELDS_CHUNK_COUNT && iter.has_next())
|
|
continue;
|
|
|
|
yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => {
|
|
Gee.List<LocationIdentifier>? locs = do_get_locations_for_ids(cx, ids, flags,
|
|
cancellable);
|
|
if (locs == null || locs.size == 0)
|
|
return Db.TransactionOutcome.DONE;
|
|
|
|
Db.Statement fetch_stmt = cx.prepare(
|
|
"SELECT fields FROM MessageTable WHERE id = ?");
|
|
|
|
// TODO: Unroll loop
|
|
foreach (LocationIdentifier location in locs) {
|
|
fetch_stmt.reset(Db.ResetScope.CLEAR_BINDINGS);
|
|
fetch_stmt.bind_rowid(0, location.message_id);
|
|
|
|
Db.Result results = fetch_stmt.exec(cancellable);
|
|
if (!results.finished)
|
|
map.set(location.email_id, (Geary.Email.Field) results.int_at(0));
|
|
}
|
|
|
|
return Db.TransactionOutcome.SUCCESS;
|
|
}, cancellable);
|
|
|
|
list.clear();
|
|
}
|
|
assert(list.size == 0);
|
|
|
|
return (map.size > 0) ? map : null;
|
|
}
|
|
|
|
public string to_string() {
|
|
return path.to_string();
|
|
}
|
|
|
|
//
|
|
// Database transaction helper methods
|
|
// These should only be called from within a TransactionMethod.
|
|
//
|
|
|
|
private int do_get_email_count(Db.Connection cx, ListFlags flags, Cancellable? cancellable)
|
|
throws Error {
|
|
Db.Statement stmt = cx.prepare(
|
|
"SELECT COUNT(*) FROM MessageLocationTable WHERE folder_id=?");
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
if (results.finished)
|
|
return 0;
|
|
|
|
int marked = !flags.include_marked_for_remove() ? do_get_marked_removed_count(cx, cancellable) : 0;
|
|
|
|
return Numeric.int_floor(results.int_at(0) - marked, 0);
|
|
}
|
|
|
|
private int do_get_marked_removed_count(Db.Connection cx, Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare(
|
|
"SELECT COUNT(*) FROM MessageLocationTable WHERE folder_id=? AND remove_marker <> ?");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_bool(1, false);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
|
|
return !results.finished ? results.int_at(0) : 0;
|
|
}
|
|
|
|
// TODO: Unroll loop
|
|
private void do_mark_unmark_removed(Db.Connection cx, Gee.Collection<Imap.UID> uids,
|
|
bool mark_removed, Cancellable? cancellable) throws Error {
|
|
// prepare Statement for reuse
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageLocationTable SET remove_marker=? WHERE folder_id=? AND ordering=?");
|
|
stmt.bind_bool(0, mark_removed);
|
|
stmt.bind_rowid(1, folder_id);
|
|
|
|
foreach (Imap.UID uid in uids) {
|
|
stmt.bind_int64(2, uid.value);
|
|
|
|
stmt.exec(cancellable);
|
|
|
|
// keep folder_id and mark_removed, replace UID each iteration
|
|
stmt.reset(Db.ResetScope.SAVE_BINDINGS);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the id of any existing message matching the given.
|
|
*
|
|
* Searches for an existing message that matches `email`, based on
|
|
* its message attributes. Currently, since ImapDB only requests
|
|
* the IMAP internal date and RFC822 message size, these are the
|
|
* only attributes used.
|
|
*
|
|
* The unique, internal message ID of the first matching message
|
|
* is returned, else `-1` if no matching message was found.
|
|
*
|
|
* This should only be called on messages obtained via the IMAP
|
|
* stack.
|
|
*/
|
|
private int64 do_search_for_duplicates(Db.Connection cx,
|
|
Geary.Email email,
|
|
ImapDB.EmailIdentifier email_id,
|
|
Cancellable? cancellable)
|
|
throws Error {
|
|
int64 id = -1;
|
|
// if fields not present, then no duplicate can reliably be found
|
|
if (!email.fields.is_all_set(REQUIRED_FIELDS)) {
|
|
debug(
|
|
"%s: Unable to detect duplicates for %s, fields available: %s",
|
|
this.to_string(),
|
|
email.id.to_string(),
|
|
email.fields.to_string()
|
|
);
|
|
return id;
|
|
}
|
|
|
|
// what's more, actually need all those fields to be available, not merely attempted,
|
|
// to err on the side of safety
|
|
Imap.EmailProperties? imap_properties = (Imap.EmailProperties) email.properties;
|
|
string? internaldate = (imap_properties != null && imap_properties.internaldate != null)
|
|
? imap_properties.internaldate.serialize() : null;
|
|
int64 rfc822_size = (imap_properties != null) ? imap_properties.rfc822_size.value : -1;
|
|
|
|
if (String.is_empty(internaldate) || rfc822_size < 0) {
|
|
debug(
|
|
"Unable to detect duplicates for %s (%s available but invalid)",
|
|
email.id.to_string(),
|
|
email.fields.to_string()
|
|
);
|
|
return id;
|
|
}
|
|
|
|
// look for duplicate in IMAP message properties
|
|
Db.Statement stmt;
|
|
if (email.message_id != null)
|
|
stmt = cx.prepare("SELECT id FROM MessageTable WHERE internaldate=? AND rfc822_size=? AND message_id=?");
|
|
else
|
|
stmt = cx.prepare("SELECT id FROM MessageTable WHERE internaldate=? AND rfc822_size=?");
|
|
|
|
stmt.bind_string(0, internaldate);
|
|
stmt.bind_int64(1, rfc822_size);
|
|
if (email.message_id != null)
|
|
stmt.bind_string(2, email.message_id.to_string());
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
if (!results.finished) {
|
|
id = results.int64_at(0);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Adds a message to the folder.
|
|
*
|
|
* Note: does NOT check if message is already associated with this
|
|
* folder.
|
|
*/
|
|
private void do_associate_with_folder(Db.Connection cx,
|
|
int64 message_id,
|
|
Imap.UID uid,
|
|
Cancellable? cancellable)
|
|
throws Error {
|
|
Db.Statement stmt = cx.prepare(
|
|
"INSERT INTO MessageLocationTable (message_id, folder_id, ordering) VALUES (?, ?, ?)");
|
|
stmt.bind_rowid(0, message_id);
|
|
stmt.bind_rowid(1, this.folder_id);
|
|
stmt.bind_int64(2, uid.value);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
private void do_remove_association_with_folder(Db.Connection cx, LocationIdentifier location,
|
|
Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare(
|
|
"DELETE FROM MessageLocationTable WHERE folder_id=? AND message_id=?");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, location.message_id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
/**
|
|
* Adds a single message to the folder, creating or merging it.
|
|
*
|
|
* This creates the message and appends it to the folder if the
|
|
* message does not already exist, else appends and merges if the
|
|
* message exists but not in the given position in this folder,
|
|
* else it exists in the given position, so simply merges it.
|
|
*
|
|
* Returns `true` if created, else was merged and returns `false`.
|
|
*/
|
|
private bool do_create_or_merge_email(Db.Connection cx,
|
|
Geary.Email email,
|
|
out Geary.Email.Field pre_fields,
|
|
out Geary.Email.Field post_fields,
|
|
ref int unread_count_change,
|
|
GLib.Cancellable? cancellable)
|
|
throws GLib.Error {
|
|
|
|
// This should only ever get invoked for messages that came
|
|
// from the IMAP layer, which should not have a message id,
|
|
// but should have a UID.
|
|
ImapDB.EmailIdentifier? email_id = email.id as ImapDB.EmailIdentifier;
|
|
if (email_id == null ||
|
|
email_id.message_id != Db.INVALID_ROWID ||
|
|
email_id.uid == null) {
|
|
throw new EngineError.BAD_PARAMETERS(
|
|
"IMAP message with UID required"
|
|
);
|
|
}
|
|
|
|
int64 message_id = -1;
|
|
bool is_associated = false;
|
|
|
|
// First, look for the same message at the same location
|
|
LocationIdentifier? location = do_get_location_for_uid(
|
|
cx,
|
|
email_id.uid,
|
|
ListFlags.INCLUDE_MARKED_FOR_REMOVE,
|
|
cancellable
|
|
);
|
|
if (location != null) {
|
|
// Already at the specified location, so no need to create
|
|
// or associate with this folder — just merge it
|
|
message_id = location.message_id;
|
|
is_associated = true;
|
|
} else {
|
|
// Not already at the specified location, so look for the
|
|
// same message in other locations or other folders
|
|
message_id = do_search_for_duplicates(
|
|
cx, email, email_id, cancellable
|
|
);
|
|
if (message_id >= 0) {
|
|
location = new LocationIdentifier(
|
|
message_id, email_id.uid, false
|
|
);
|
|
}
|
|
}
|
|
|
|
bool was_created = false;
|
|
if (location != null) {
|
|
// Found the same or a duplicate message, so merge it. We
|
|
// special-case flag-only updates, which happens often and
|
|
// will only write to the DB if necessary.
|
|
if (email.fields != Geary.Email.Field.FLAGS) {
|
|
do_merge_email(
|
|
cx, location, email,
|
|
out pre_fields, out post_fields,
|
|
ref unread_count_change,
|
|
cancellable
|
|
);
|
|
|
|
// Already associated with folder and flags were known.
|
|
if (is_associated && pre_fields.is_all_set(Geary.Email.Field.FLAGS))
|
|
unread_count_change = 0;
|
|
} else {
|
|
do_merge_email_flags(
|
|
cx, location, email,
|
|
out pre_fields, out post_fields,
|
|
ref unread_count_change, cancellable
|
|
);
|
|
}
|
|
} else {
|
|
// Message was not found, so create a new message for it
|
|
was_created = true;
|
|
MessageRow row = new MessageRow.from_email(email);
|
|
pre_fields = Geary.Email.Field.NONE;
|
|
post_fields = email.fields;
|
|
|
|
Db.Statement stmt = cx.prepare(
|
|
"INSERT INTO MessageTable "
|
|
+ "(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, preview, flags, "
|
|
+ "internaldate, internaldate_time_t, rfc822_size) "
|
|
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
stmt.bind_int(0, row.fields);
|
|
stmt.bind_string(1, row.date);
|
|
stmt.bind_int64(2, row.date_time_t);
|
|
stmt.bind_string(3, row.from);
|
|
stmt.bind_string(4, row.sender);
|
|
stmt.bind_string(5, row.reply_to);
|
|
stmt.bind_string(6, row.to);
|
|
stmt.bind_string(7, row.cc);
|
|
stmt.bind_string(8, row.bcc);
|
|
stmt.bind_string(9, row.message_id);
|
|
stmt.bind_string(10, row.in_reply_to);
|
|
stmt.bind_string(11, row.references);
|
|
stmt.bind_string(12, row.subject);
|
|
stmt.bind_string_buffer(13, row.header);
|
|
stmt.bind_string_buffer(14, row.body);
|
|
stmt.bind_string(15, row.preview);
|
|
stmt.bind_string(16, row.email_flags);
|
|
stmt.bind_string(17, row.internaldate);
|
|
stmt.bind_int64(18, row.internaldate_time_t);
|
|
stmt.bind_int64(19, row.rfc822_size);
|
|
|
|
message_id = stmt.exec_insert(cancellable);
|
|
|
|
// write out attachments, if any
|
|
// TODO: Because this involves saving files, it potentially means holding up access to the
|
|
// database while they're being written; may want to do this outside of transaction.
|
|
if (email.fields.fulfills(Attachment.REQUIRED_FIELDS)) {
|
|
Attachment.save_attachments(
|
|
cx,
|
|
this.attachments_path,
|
|
message_id,
|
|
email.get_message().get_attachments(),
|
|
cancellable
|
|
);
|
|
}
|
|
|
|
do_add_email_to_search_table(cx, message_id, email, cancellable);
|
|
|
|
// Update unread count if our new email is unread.
|
|
if (email.email_flags != null && email.email_flags.is_unread())
|
|
unread_count_change++;
|
|
}
|
|
|
|
// Finally, update the email's message id and add it to the
|
|
// folder, if needed
|
|
email_id.promote_with_message_id(message_id);
|
|
if (!is_associated) {
|
|
do_associate_with_folder(cx, message_id, email_id.uid, cancellable);
|
|
}
|
|
|
|
return was_created;
|
|
}
|
|
|
|
internal static void do_add_email_to_search_table(Db.Connection cx, int64 message_id,
|
|
Geary.Email email, Cancellable? cancellable) throws Error {
|
|
string? body = null;
|
|
try {
|
|
body = email.get_message().get_searchable_body();
|
|
} catch (Error e) {
|
|
// Ignore.
|
|
}
|
|
string? recipients = null;
|
|
try {
|
|
recipients = email.get_message().get_searchable_recipients();
|
|
} catch (Error e) {
|
|
// Ignore.
|
|
}
|
|
|
|
// Often when Geary first adds a message to the FTS table
|
|
// these fields will all be null or empty strings. Check that
|
|
// this isn't the case beforehand to avoid the IO overhead.
|
|
|
|
string? attachments = email.get_searchable_attachment_list();
|
|
string? subject = email.subject != null ? email.subject.to_searchable_string() : null;
|
|
string? from = email.from != null ? email.from.to_searchable_string() : null;
|
|
string? cc = email.cc != null ? email.cc.to_searchable_string() : null;
|
|
string? bcc = email.bcc != null ? email.bcc.to_searchable_string() : null;
|
|
|
|
if (!Geary.String.is_empty(body) ||
|
|
!Geary.String.is_empty(attachments) ||
|
|
!Geary.String.is_empty(subject) ||
|
|
!Geary.String.is_empty(from) ||
|
|
!Geary.String.is_empty(recipients) ||
|
|
!Geary.String.is_empty(cc) ||
|
|
!Geary.String.is_empty(bcc)) {
|
|
|
|
Db.Statement stmt = cx.prepare("""
|
|
INSERT INTO MessageSearchTable
|
|
(docid, body, attachment, subject, from_field, receivers, cc, bcc)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""");
|
|
stmt.bind_rowid(0, message_id);
|
|
stmt.bind_string(1, body);
|
|
stmt.bind_string(2, attachments);
|
|
stmt.bind_string(3, subject);
|
|
stmt.bind_string(4, from);
|
|
stmt.bind_string(5, recipients);
|
|
stmt.bind_string(6, cc);
|
|
stmt.bind_string(7, bcc);
|
|
|
|
stmt.exec_insert(cancellable);
|
|
}
|
|
}
|
|
|
|
private static bool do_check_for_message_search_row(Db.Connection cx, int64 message_id,
|
|
Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare("SELECT 'TRUE' FROM MessageSearchTable WHERE docid=?");
|
|
stmt.bind_rowid(0, message_id);
|
|
|
|
Db.Result result = stmt.exec(cancellable);
|
|
return !result.finished;
|
|
}
|
|
|
|
private Gee.List<Geary.Email>? do_list_email(Db.Connection cx, Gee.List<LocationIdentifier> locations,
|
|
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error {
|
|
Gee.List<Geary.Email> emails = new Gee.ArrayList<Geary.Email>();
|
|
foreach (LocationIdentifier location in locations) {
|
|
try {
|
|
emails.add(do_location_to_email(cx, location, required_fields, flags, cancellable));
|
|
} catch (EngineError err) {
|
|
if (err is EngineError.NOT_FOUND) {
|
|
debug("Warning: Message not found, dropping: %s", err.message);
|
|
} else if (!(err is EngineError.INCOMPLETE_MESSAGE)) {
|
|
// if not all required_fields available, simply drop with no comment; it's up to
|
|
// the caller to detect and fulfill from the network
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (emails.size > 0) ? emails : null;
|
|
}
|
|
|
|
// Throws EngineError.NOT_FOUND if message_id is invalid. Note that this does not verify that
|
|
// the message is indeed in this folder.
|
|
internal static MessageRow do_fetch_message_row(Db.Connection cx, int64 message_id,
|
|
Geary.Email.Field requested_fields, out Geary.Email.Field db_fields,
|
|
Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare(
|
|
"SELECT %s FROM MessageTable WHERE id=?".printf(fields_to_columns(requested_fields)));
|
|
stmt.bind_rowid(0, message_id);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
if (results.finished)
|
|
throw new EngineError.NOT_FOUND("No message ID %s found in database", message_id.to_string());
|
|
|
|
db_fields = (Geary.Email.Field) results.int_for("fields");
|
|
return new MessageRow.from_result(requested_fields, results);
|
|
}
|
|
|
|
private Geary.Email do_location_to_email(Db.Connection cx, LocationIdentifier location,
|
|
Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error {
|
|
if (!flags.include_marked_for_remove() && location.marked_removed) {
|
|
throw new EngineError.NOT_FOUND("Message %s marked as removed in %s",
|
|
location.email_id.to_string(), to_string());
|
|
}
|
|
|
|
// look for perverse case
|
|
if (required_fields == Geary.Email.Field.NONE)
|
|
return new Geary.Email(location.email_id);
|
|
|
|
Geary.Email.Field db_fields;
|
|
MessageRow row = do_fetch_message_row(cx, location.message_id, required_fields,
|
|
out db_fields, cancellable);
|
|
if (!flags.is_all_set(ListFlags.PARTIAL_OK) && !row.fields.fulfills(required_fields)) {
|
|
throw new EngineError.INCOMPLETE_MESSAGE(
|
|
"Message %s in folder %s only fulfills %Xh fields (required: %Xh)",
|
|
location.email_id.to_string(), to_string(), row.fields, required_fields);
|
|
}
|
|
|
|
Geary.Email email = row.to_email(location.email_id);
|
|
Attachment.add_attachments(
|
|
cx, this.attachments_path, email, location.message_id, cancellable
|
|
);
|
|
return email;
|
|
}
|
|
|
|
private static string fields_to_columns(Geary.Email.Field fields) {
|
|
// always pull the rowid and fields of the message
|
|
StringBuilder builder = new StringBuilder("id, fields");
|
|
foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
|
|
unowned string? append = null;
|
|
if (fields.is_all_set(fields)) {
|
|
switch (field) {
|
|
case Geary.Email.Field.DATE:
|
|
append = "date_field, date_time_t";
|
|
break;
|
|
|
|
case Geary.Email.Field.ORIGINATORS:
|
|
append = "from_field, sender, reply_to";
|
|
break;
|
|
|
|
case Geary.Email.Field.RECEIVERS:
|
|
append = "to_field, cc, bcc";
|
|
break;
|
|
|
|
case Geary.Email.Field.REFERENCES:
|
|
append = "message_id, in_reply_to, reference_ids";
|
|
break;
|
|
|
|
case Geary.Email.Field.SUBJECT:
|
|
append = "subject";
|
|
break;
|
|
|
|
case Geary.Email.Field.HEADER:
|
|
append = "header";
|
|
break;
|
|
|
|
case Geary.Email.Field.BODY:
|
|
append = "body";
|
|
break;
|
|
|
|
case Geary.Email.Field.PREVIEW:
|
|
append = "preview";
|
|
break;
|
|
|
|
case Geary.Email.Field.FLAGS:
|
|
append = "flags";
|
|
break;
|
|
|
|
case Geary.Email.Field.PROPERTIES:
|
|
append = "internaldate, internaldate_time_t, rfc822_size";
|
|
break;
|
|
|
|
case NONE:
|
|
// no-op
|
|
break;
|
|
|
|
case ENVELOPE:
|
|
case ALL:
|
|
// XXX hmm
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (append != null) {
|
|
builder.append(", ");
|
|
builder.append(append);
|
|
}
|
|
}
|
|
|
|
return builder.str;
|
|
}
|
|
|
|
private Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? do_get_email_flags(Db.Connection cx,
|
|
Gee.Collection<ImapDB.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
|
|
Gee.List<LocationIdentifier>? locs = do_get_locations_for_ids(cx, ids, ListFlags.NONE,
|
|
cancellable);
|
|
if (locs == null || locs.size == 0)
|
|
return null;
|
|
|
|
// prepare Statement for reuse
|
|
Db.Statement fetch_stmt = cx.prepare("SELECT flags FROM MessageTable WHERE id=?");
|
|
|
|
Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags> map = new Gee.HashMap<
|
|
ImapDB.EmailIdentifier, Geary.EmailFlags>();
|
|
// TODO: Unroll this loop
|
|
foreach (LocationIdentifier location in locs) {
|
|
fetch_stmt.reset(Db.ResetScope.CLEAR_BINDINGS);
|
|
fetch_stmt.bind_rowid(0, location.message_id);
|
|
|
|
Db.Result results = fetch_stmt.exec(cancellable);
|
|
if (results.finished || results.is_null_at(0))
|
|
continue;
|
|
|
|
map.set(location.email_id,
|
|
new Geary.Imap.EmailFlags(Geary.Imap.MessageFlags.deserialize(results.string_at(0))));
|
|
}
|
|
|
|
return (map.size > 0) ? map : null;
|
|
}
|
|
|
|
private Geary.EmailFlags? do_get_email_flags_single(Db.Connection cx, int64 message_id,
|
|
Cancellable? cancellable) throws Error {
|
|
Db.Statement fetch_stmt = cx.prepare("SELECT flags FROM MessageTable WHERE id=?");
|
|
fetch_stmt.bind_rowid(0, message_id);
|
|
|
|
Db.Result results = fetch_stmt.exec(cancellable);
|
|
|
|
if (results.finished || results.is_null_at(0))
|
|
return null;
|
|
|
|
return new Geary.Imap.EmailFlags(Geary.Imap.MessageFlags.deserialize(results.string_at(0)));
|
|
}
|
|
|
|
// TODO: Unroll loop
|
|
private void do_set_email_flags(Db.Connection cx, Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags> map,
|
|
Cancellable? cancellable) throws Error {
|
|
Db.Statement update_stmt = cx.prepare(
|
|
"UPDATE MessageTable SET flags=?, fields = fields | ? WHERE id=?");
|
|
|
|
foreach (ImapDB.EmailIdentifier id in map.keys) {
|
|
LocationIdentifier? location = do_get_location_for_id(
|
|
cx,
|
|
id,
|
|
// Could be setting a flag on a deleted message
|
|
ListFlags.INCLUDE_MARKED_FOR_REMOVE,
|
|
cancellable
|
|
);
|
|
if (location == null) {
|
|
throw new EngineError.NOT_FOUND(
|
|
"Email not found: %s", id.to_string()
|
|
);
|
|
}
|
|
|
|
Geary.Imap.EmailFlags? flags = map.get(id) as Geary.Imap.EmailFlags;
|
|
if (flags == null) {
|
|
throw new EngineError.BAD_PARAMETERS(
|
|
"Email with Geary.Imap.EmailFlags required"
|
|
);
|
|
}
|
|
|
|
update_stmt.reset(Db.ResetScope.CLEAR_BINDINGS);
|
|
update_stmt.bind_string(0, flags.message_flags.serialize());
|
|
update_stmt.bind_int(1, Geary.Email.Field.FLAGS);
|
|
update_stmt.bind_rowid(2, id.message_id);
|
|
|
|
update_stmt.exec(cancellable);
|
|
}
|
|
}
|
|
|
|
private bool do_fetch_email_fields(Db.Connection cx, int64 message_id, out Geary.Email.Field fields,
|
|
Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare("SELECT fields FROM MessageTable WHERE id=?");
|
|
stmt.bind_rowid(0, message_id);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
if (results.finished) {
|
|
fields = Geary.Email.Field.NONE;
|
|
|
|
return false;
|
|
}
|
|
|
|
fields = (Geary.Email.Field) results.int_at(0);
|
|
|
|
return true;
|
|
}
|
|
|
|
private void do_merge_message_row(Db.Connection cx,
|
|
MessageRow row,
|
|
out Geary.Email.Field new_fields,
|
|
ref int unread_count_change,
|
|
GLib.Cancellable? cancellable)
|
|
throws GLib.Error {
|
|
Geary.Email.Field available_fields;
|
|
if (!do_fetch_email_fields(cx, row.id, out available_fields, cancellable))
|
|
throw new EngineError.NOT_FOUND("No message with ID %s found in database", row.id.to_string());
|
|
|
|
// This calculates the fields in the row that are not in the database already and then adds
|
|
// any available mutable fields provided by the caller
|
|
new_fields = (row.fields ^ available_fields) & row.fields;
|
|
new_fields |= (row.fields & Geary.Email.MUTABLE_FIELDS);
|
|
if (new_fields == Geary.Email.Field.NONE) {
|
|
// nothing to add
|
|
return;
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.DATE)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET date_field=?, date_time_t=? WHERE id=?");
|
|
stmt.bind_string(0, row.date);
|
|
stmt.bind_int64(1, row.date_time_t);
|
|
stmt.bind_rowid(2, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.ORIGINATORS)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET from_field=?, sender=?, reply_to=? WHERE id=?");
|
|
stmt.bind_string(0, row.from);
|
|
stmt.bind_string(1, row.sender);
|
|
stmt.bind_string(2, row.reply_to);
|
|
stmt.bind_rowid(3, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.RECEIVERS)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET to_field=?, cc=?, bcc=? WHERE id=?");
|
|
stmt.bind_string(0, row.to);
|
|
stmt.bind_string(1, row.cc);
|
|
stmt.bind_string(2, row.bcc);
|
|
stmt.bind_rowid(3, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.REFERENCES)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET message_id=?, in_reply_to=?, reference_ids=? WHERE id=?");
|
|
stmt.bind_string(0, row.message_id);
|
|
stmt.bind_string(1, row.in_reply_to);
|
|
stmt.bind_string(2, row.references);
|
|
stmt.bind_rowid(3, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.SUBJECT)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET subject=? WHERE id=?");
|
|
stmt.bind_string(0, row.subject);
|
|
stmt.bind_rowid(1, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.HEADER)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET header=? WHERE id=?");
|
|
stmt.bind_string_buffer(0, row.header);
|
|
stmt.bind_rowid(1, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.BODY)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET body=? WHERE id=?");
|
|
stmt.bind_string_buffer(0, row.body);
|
|
stmt.bind_rowid(1, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.PREVIEW)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET preview=? WHERE id=?");
|
|
stmt.bind_string(0, row.preview);
|
|
stmt.bind_rowid(1, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.FLAGS)) {
|
|
// Fetch existing flags to update unread count
|
|
Geary.EmailFlags? old_flags = do_get_email_flags_single(cx, row.id, cancellable);
|
|
Geary.EmailFlags new_flags = new Geary.Imap.EmailFlags(
|
|
Geary.Imap.MessageFlags.deserialize(row.email_flags));
|
|
|
|
if (old_flags != null && (old_flags.is_unread() != new_flags.is_unread()))
|
|
unread_count_change += new_flags.is_unread() ? 1 : -1;
|
|
else if (new_flags.is_unread())
|
|
unread_count_change++;
|
|
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET flags=? WHERE id=?");
|
|
stmt.bind_string(0, row.email_flags);
|
|
stmt.bind_rowid(1, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.PROPERTIES)) {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET internaldate=?, internaldate_time_t=?, rfc822_size=? WHERE id=?");
|
|
stmt.bind_string(0, row.internaldate);
|
|
stmt.bind_int64(1, row.internaldate_time_t);
|
|
stmt.bind_int64(2, row.rfc822_size);
|
|
stmt.bind_rowid(3, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
// now merge the new fields in the row
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE MessageTable SET fields = fields | ? WHERE id=?");
|
|
stmt.bind_int(0, new_fields);
|
|
stmt.bind_rowid(1, row.id);
|
|
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
private void do_merge_email_in_search_table(Db.Connection cx, int64 message_id,
|
|
Geary.Email.Field new_fields, Geary.Email email, Cancellable? cancellable) throws Error {
|
|
|
|
// We can't simply issue an UPDATE here for the changed
|
|
// fields, since it will likely corrupt the
|
|
// MessageSearchTable. So instead do a SELECT to get the
|
|
// existing data, then do a DELETE and INSERT. See Bug 772522.
|
|
|
|
Db.Statement select = cx.prepare("""
|
|
SELECT body, attachment, subject, from_field, receivers, cc, bcc
|
|
FROM MessageSearchTable
|
|
WHERE docid=?
|
|
""");
|
|
select.bind_rowid(0, message_id);
|
|
Db.Result row = select.exec(cancellable);
|
|
|
|
string? body = row.string_at(0);
|
|
string? attachments = row.string_at(1);
|
|
string? subject = row.string_at(2);
|
|
string? from = row.string_at(3);
|
|
string? recipients = row.string_at(4);
|
|
string? cc = row.string_at(5);
|
|
string? bcc = row.string_at(6);
|
|
|
|
if (new_fields.is_any_set(Geary.Email.REQUIRED_FOR_MESSAGE) &&
|
|
email.fields.is_all_set(Geary.Email.REQUIRED_FOR_MESSAGE)) {
|
|
try {
|
|
body = email.get_message().get_searchable_body();
|
|
} catch (Error e) {
|
|
// Ignore.
|
|
}
|
|
try {
|
|
recipients = email.get_message().get_searchable_recipients();
|
|
} catch (Error e) {
|
|
// Ignore.
|
|
}
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.SUBJECT)) {
|
|
if (email.subject != null)
|
|
email.subject.to_searchable_string();
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.ORIGINATORS)) {
|
|
if (email.from != null)
|
|
from = email.from.to_searchable_string();
|
|
}
|
|
|
|
if (new_fields.is_any_set(Geary.Email.Field.RECEIVERS)) {
|
|
if (email.cc != null)
|
|
cc = email.cc.to_searchable_string();
|
|
if (email.bcc != null)
|
|
bcc = email.bcc.to_searchable_string();
|
|
}
|
|
|
|
Db.Statement del = cx.prepare(
|
|
"DELETE FROM MessageSearchTable WHERE docid=?"
|
|
);
|
|
del.bind_rowid(0, message_id);
|
|
del.exec(cancellable);
|
|
|
|
Db.Statement insert = cx.prepare("""
|
|
INSERT INTO MessageSearchTable
|
|
(docid, body, attachment, subject, from_field, receivers, cc, bcc)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""");
|
|
insert.bind_rowid(0, message_id);
|
|
insert.bind_string(1, body);
|
|
insert.bind_string(2, attachments);
|
|
insert.bind_string(3, subject);
|
|
insert.bind_string(4, from);
|
|
insert.bind_string(5, recipients);
|
|
insert.bind_string(6, cc);
|
|
insert.bind_string(7, bcc);
|
|
|
|
insert.exec_insert(cancellable);
|
|
}
|
|
|
|
// This *replaces* the stored flags, it does not OR them ... this is simply a fast-path over
|
|
// do_merge_email(), as updating FLAGS happens often and doesn't require a lot of extra work
|
|
private void do_merge_email_flags(Db.Connection cx,
|
|
LocationIdentifier location,
|
|
Geary.Email email,
|
|
out Geary.Email.Field pre_fields,
|
|
out Geary.Email.Field post_fields,
|
|
ref int unread_count_change,
|
|
GLib.Cancellable? cancellable)
|
|
throws GLib.Error {
|
|
assert(email.fields == Geary.Email.Field.FLAGS);
|
|
|
|
// fetch MessageRow and its fields, note that the fields now include FLAGS if they didn't
|
|
// already
|
|
MessageRow row = do_fetch_message_row(cx, location.message_id, Geary.Email.Field.FLAGS,
|
|
out pre_fields, cancellable);
|
|
post_fields = pre_fields;
|
|
|
|
// Only update if changed
|
|
Geary.Email row_email = row.to_email(location.email_id);
|
|
if (row_email.email_flags == null ||
|
|
!row_email.email_flags.equal_to(email.email_flags)) {
|
|
|
|
// Check for unread count changes
|
|
if (row_email.email_flags != null &&
|
|
row_email.email_flags.is_unread() != email.email_flags.is_unread()) {
|
|
unread_count_change += email.email_flags.is_unread() ? 1 : -1;
|
|
}
|
|
|
|
// do_set_email_flags requires a valid message location,
|
|
// but doesn't accept one as an arg, so despite knowing
|
|
// the location here, make sure we pass an id with a
|
|
// message_id in so it can look the location back up.
|
|
do_set_email_flags(
|
|
cx,
|
|
Collection.single_map<ImapDB.EmailIdentifier,Geary.EmailFlags>(
|
|
(ImapDB.EmailIdentifier) row_email.id, email.email_flags
|
|
),
|
|
cancellable
|
|
);
|
|
|
|
post_fields |= Geary.Email.Field.FLAGS;
|
|
|
|
}
|
|
}
|
|
|
|
private void do_merge_email(Db.Connection cx,
|
|
LocationIdentifier location,
|
|
Geary.Email email,
|
|
out Geary.Email.Field pre_fields,
|
|
out Geary.Email.Field post_fields,
|
|
ref int unread_count_change,
|
|
GLib.Cancellable? cancellable)
|
|
throws GLib.Error {
|
|
// fetch message from database and merge in this email
|
|
MessageRow row = do_fetch_message_row(cx, location.message_id,
|
|
email.fields | Email.REQUIRED_FOR_MESSAGE | Attachment.REQUIRED_FIELDS,
|
|
out pre_fields, cancellable);
|
|
Geary.Email.Field fetched_fields = row.fields;
|
|
post_fields = pre_fields | email.fields;
|
|
row.merge_from_remote(email);
|
|
|
|
if (email.fields == Geary.Email.Field.NONE)
|
|
return;
|
|
|
|
// Merge in any fields in the submitted email that aren't already in the database or are mutable
|
|
int new_unread_count = 0;
|
|
if (((fetched_fields & email.fields) != email.fields) ||
|
|
email.fields.is_any_set(Geary.Email.MUTABLE_FIELDS)) {
|
|
// Build the combined email from the merge, which will be used to save the attachments
|
|
Geary.Email combined_email = row.to_email(location.email_id);
|
|
|
|
// Update attachments if not already in the database
|
|
if (!fetched_fields.fulfills(Attachment.REQUIRED_FIELDS)
|
|
&& combined_email.fields.fulfills(Attachment.REQUIRED_FIELDS)) {
|
|
combined_email.add_attachments(
|
|
Attachment.save_attachments(
|
|
cx,
|
|
this.attachments_path,
|
|
location.message_id,
|
|
combined_email.get_message().get_attachments(),
|
|
cancellable
|
|
)
|
|
);
|
|
}
|
|
|
|
Geary.Email.Field new_fields;
|
|
do_merge_message_row(
|
|
cx, row,
|
|
out new_fields,
|
|
ref new_unread_count,
|
|
cancellable
|
|
);
|
|
|
|
if (do_check_for_message_search_row(cx, location.message_id, cancellable))
|
|
do_merge_email_in_search_table(cx, location.message_id, new_fields, combined_email, cancellable);
|
|
else
|
|
do_add_email_to_search_table(cx, location.message_id, combined_email, cancellable);
|
|
} else {
|
|
// If the email is ready to go, we still may need to update the unread count.
|
|
Geary.EmailFlags? combined_flags = do_get_email_flags_single(cx, location.message_id,
|
|
cancellable);
|
|
if (combined_flags != null && combined_flags.is_unread())
|
|
new_unread_count = 1;
|
|
}
|
|
|
|
unread_count_change += new_unread_count;
|
|
}
|
|
|
|
/**
|
|
* Adds a value to the unread count. If this makes the unread count negative, it will be
|
|
* set to zero.
|
|
*/
|
|
internal void do_add_to_unread_count(Db.Connection cx, int to_add, Cancellable? cancellable)
|
|
throws Error {
|
|
if (to_add == 0)
|
|
return; // Nothing to do.
|
|
|
|
Db.Statement update_stmt = cx.prepare(
|
|
"UPDATE FolderTable SET unread_count = CASE WHEN unread_count + ? < 0 THEN 0 ELSE " +
|
|
"unread_count + ? END WHERE id=?");
|
|
|
|
update_stmt.bind_int(0, to_add);
|
|
update_stmt.bind_int(1, to_add);
|
|
update_stmt.bind_rowid(2, folder_id);
|
|
|
|
update_stmt.exec(cancellable);
|
|
}
|
|
|
|
// Db.Result must include columns for "message_id", "ordering", and "remove_marker" from the
|
|
// MessageLocationTable
|
|
private Gee.List<LocationIdentifier> do_results_to_locations(Db.Result results, int count,
|
|
ListFlags flags, Cancellable? cancellable) throws Error {
|
|
Gee.List<LocationIdentifier> locations = new Gee.ArrayList<LocationIdentifier>();
|
|
|
|
if (results.finished)
|
|
return locations;
|
|
|
|
do {
|
|
LocationIdentifier location = new LocationIdentifier(results.rowid_for("message_id"),
|
|
new Imap.UID(results.int64_for("ordering")), results.bool_for("remove_marker"));
|
|
if (!flags.include_marked_for_remove() && location.marked_removed)
|
|
continue;
|
|
|
|
locations.add(location);
|
|
if (locations.size >= count)
|
|
break;
|
|
} while (results.next(cancellable));
|
|
|
|
return locations;
|
|
}
|
|
|
|
// Use a separate step to strip out complete emails because original implementation (using an
|
|
// INNER JOIN) was horribly slow under load
|
|
private void do_remove_complete_locations(Db.Connection cx, Gee.List<LocationIdentifier>? locations,
|
|
Cancellable? cancellable) throws Error {
|
|
if (locations == null || locations.size == 0)
|
|
return;
|
|
|
|
StringBuilder sql = new StringBuilder("""
|
|
SELECT id FROM MessageTable WHERE id IN (
|
|
""");
|
|
bool first = true;
|
|
foreach (LocationIdentifier location_id in locations) {
|
|
if (!first)
|
|
sql.append(",");
|
|
|
|
sql.append(location_id.message_id.to_string());
|
|
first = false;
|
|
}
|
|
sql.append(") AND fields <> ?");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_int(0, Geary.Email.Field.ALL);
|
|
|
|
Db.Result results = stmt.exec(cancellable);
|
|
|
|
Gee.HashSet<int64?> incomplete_locations = new Gee.HashSet<int64?>(Collection.int64_hash_func,
|
|
Collection.int64_equal_func);
|
|
while (!results.finished) {
|
|
incomplete_locations.add(results.int64_at(0));
|
|
results.next(cancellable);
|
|
}
|
|
|
|
if (incomplete_locations.size == 0) {
|
|
locations.clear();
|
|
|
|
return;
|
|
}
|
|
|
|
Gee.Iterator<LocationIdentifier> iter = locations.iterator();
|
|
while (iter.next()) {
|
|
if (!incomplete_locations.contains(iter.get().message_id))
|
|
iter.remove();
|
|
}
|
|
}
|
|
|
|
private LocationIdentifier? do_get_location_for_id(Db.Connection cx, ImapDB.EmailIdentifier id,
|
|
ListFlags flags, Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ? AND message_id = ?
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_rowid(1, id.message_id);
|
|
|
|
Db.Result result = stmt.exec(cancellable);
|
|
if (result.finished)
|
|
return null;
|
|
|
|
LocationIdentifier location = new LocationIdentifier(id.message_id,
|
|
new Imap.UID(result.int64_at(0)), result.bool_at(1));
|
|
|
|
return (!flags.include_marked_for_remove() && location.marked_removed) ? null : location;
|
|
}
|
|
|
|
private Gee.List<LocationIdentifier>? do_get_locations_for_ids(Db.Connection cx,
|
|
Gee.Collection<ImapDB.EmailIdentifier>? ids, ListFlags flags, Cancellable? cancellable)
|
|
throws Error {
|
|
if (ids == null || ids.size == 0)
|
|
return null;
|
|
|
|
StringBuilder sql = new StringBuilder("""
|
|
SELECT message_id, ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE message_id IN (
|
|
""");
|
|
bool first = true;
|
|
foreach (ImapDB.EmailIdentifier id in ids) {
|
|
if (!first)
|
|
sql.append(",");
|
|
sql.append_printf(id.message_id.to_string());
|
|
|
|
first = false;
|
|
}
|
|
sql.append(") AND folder_id = ?");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
Gee.List<LocationIdentifier> locs = do_results_to_locations(stmt.exec(cancellable), int.MAX,
|
|
flags, cancellable);
|
|
|
|
return (locs.size > 0) ? locs : null;
|
|
}
|
|
|
|
private LocationIdentifier? do_get_location_for_uid(Db.Connection cx, Imap.UID uid,
|
|
ListFlags flags, Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT message_id, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ? AND ordering = ?
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
stmt.bind_int64(1, uid.value);
|
|
|
|
Db.Result result = stmt.exec(cancellable);
|
|
if (result.finished)
|
|
return null;
|
|
|
|
LocationIdentifier location = new LocationIdentifier(result.rowid_at(0), uid, result.bool_at(1));
|
|
|
|
return (!flags.include_marked_for_remove() && location.marked_removed) ? null : location;
|
|
}
|
|
|
|
private Gee.List<LocationIdentifier>? do_get_locations_for_uids(Db.Connection cx,
|
|
Gee.Collection<Imap.UID>? uids, ListFlags flags, Cancellable? cancellable)
|
|
throws Error {
|
|
if (uids == null || uids.size == 0)
|
|
return null;
|
|
|
|
StringBuilder sql = new StringBuilder("""
|
|
SELECT message_id, ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE ordering IN (
|
|
""");
|
|
bool first = true;
|
|
foreach (Imap.UID uid in uids) {
|
|
if (!first)
|
|
sql.append(",");
|
|
sql.append(uid.value.to_string());
|
|
|
|
first = false;
|
|
}
|
|
sql.append(") AND folder_id = ?");
|
|
|
|
Db.Statement stmt = cx.prepare(sql.str);
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
Gee.List<LocationIdentifier> locs = do_results_to_locations(stmt.exec(cancellable), int.MAX,
|
|
flags, cancellable);
|
|
|
|
return (locs.size > 0) ? locs : null;
|
|
}
|
|
|
|
private Gee.List<LocationIdentifier>? do_get_all_locations(Db.Connection cx, ListFlags flags,
|
|
Cancellable? cancellable) throws Error {
|
|
Db.Statement stmt = cx.prepare("""
|
|
SELECT message_id, ordering, remove_marker
|
|
FROM MessageLocationTable
|
|
WHERE folder_id = ?
|
|
""");
|
|
stmt.bind_rowid(0, folder_id);
|
|
|
|
Gee.List<LocationIdentifier> locs = do_results_to_locations(stmt.exec(cancellable), int.MAX,
|
|
flags, cancellable);
|
|
|
|
return (locs.size > 0) ? locs : null;
|
|
}
|
|
|
|
private int do_get_unread_count_for_ids(Db.Connection cx,
|
|
Gee.Collection<ImapDB.EmailIdentifier>? ids, Cancellable? cancellable) throws Error {
|
|
if (ids == null || ids.size == 0)
|
|
return 0;
|
|
|
|
// Fetch flags for each email and update this folder's unread count.
|
|
// (Note that this only flags for emails which have NOT been marked for removal
|
|
// are included.)
|
|
Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? flag_map = do_get_email_flags(cx,
|
|
ids, cancellable);
|
|
if (flag_map != null)
|
|
return Geary.traverse<Geary.EmailFlags>(flag_map.values).count_matching(f => f.is_unread());
|
|
|
|
return 0;
|
|
}
|
|
|
|
// For SELECT/EXAMINE responses, not STATUS responses
|
|
private void do_update_last_seen_select_examine_total(Db.Connection cx,
|
|
int total,
|
|
Cancellable? cancellable)
|
|
throws Error {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE FolderTable SET last_seen_total=? WHERE id=?"
|
|
);
|
|
stmt.bind_int(0, Numeric.int_floor(total, 0));
|
|
stmt.bind_rowid(1, this.folder_id);
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
// For STATUS responses, not SELECT/EXAMINE responses
|
|
private void do_update_last_seen_status_total(Db.Connection cx,
|
|
int total,
|
|
Cancellable? cancellable)
|
|
throws Error {
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE FolderTable SET last_seen_status_total=? WHERE id=?");
|
|
stmt.bind_int(0, Numeric.int_floor(total, 0));
|
|
stmt.bind_rowid(1, this.folder_id);
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
private void do_update_uid_info(Db.Connection cx,
|
|
Imap.FolderProperties remote_properties,
|
|
Cancellable? cancellable)
|
|
throws Error {
|
|
int64 uid_validity = (remote_properties.uid_validity != null)
|
|
? remote_properties.uid_validity.value
|
|
: Imap.UIDValidity.INVALID;
|
|
int64 uid_next = (remote_properties.uid_next != null)
|
|
? remote_properties.uid_next.value
|
|
: Imap.UID.INVALID;
|
|
|
|
Db.Statement stmt = cx.prepare(
|
|
"UPDATE FolderTable SET uid_validity=?, uid_next=? WHERE id=?");
|
|
stmt.bind_int64(0, uid_validity);
|
|
stmt.bind_int64(1, uid_next);
|
|
stmt.bind_rowid(2, this.folder_id);
|
|
stmt.exec(cancellable);
|
|
}
|
|
|
|
}
|