/* Copyright 2011 Yorba Foundation * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ // TODO: This class currently deals with generic email storage as well as IMAP-specific issues; in // the future, to support other email services, will need to break this up. public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { private MailDatabase db; private FolderRow folder_row; private MessageTable message_table; private MessageLocationTable location_table; private ImapMessageLocationPropertiesTable imap_location_table; private string name; private bool opened = false; internal Folder(MailDatabase db, FolderRow folder_row) throws Error { this.db = db; this.folder_row = folder_row; name = folder_row.name; message_table = db.get_message_table(); location_table = db.get_message_location_table(); imap_location_table = db.get_imap_message_location_table(); } private void check_open() throws Error { if (!opened) throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); } public string get_name() { return name; } public Geary.FolderProperties? get_properties() { return null; } public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { if (opened) throw new EngineError.ALREADY_OPEN("%s already open", to_string()); opened = true; notify_opened(); } public async void close_async(Cancellable? cancellable = null) throws Error { if (!opened) return; opened = false; notify_closed(CloseReason.FOLDER_CLOSED); } public async int get_email_count(Cancellable? cancellable = null) throws Error { check_open(); // TODO return 0; } public async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { check_open(); Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location; // See if it already exists; first by UID (which is only guaranteed to be unique in a folder, // not account-wide) int64 message_id; if (yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id, out message_id, cancellable)) { throw new EngineError.ALREADY_EXISTS("Email with UID %s already exists in %s", location.uid.to_string(), get_name()); } message_id = yield message_table.create_async( new MessageRow.from_email(message_table, email), cancellable); MessageLocationRow location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id, folder_row.id, location.position); int64 location_id = yield location_table.create_async(location_row, cancellable); ImapMessageLocationPropertiesRow imap_location_row = new ImapMessageLocationPropertiesRow( imap_location_table, Row.INVALID_ID, location_id, location.uid); yield imap_location_table.create_async(imap_location_row, cancellable); } public async Gee.List? list_email_async(int low, int count, Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { assert(low >= 1); assert(count >= 1); check_open(); Gee.List? list = yield location_table.list_async(folder_row.id, low, count, cancellable); return yield list_email(list, required_fields, cancellable); } public async Gee.List? list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { check_open(); Gee.List? list = yield location_table.list_sparse_async(folder_row.id, by_position, cancellable); return yield list_email(list, required_fields, cancellable); } private async Gee.List? list_email(Gee.List? list, Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { check_open(); if (list == null || list.size == 0) return null; // TODO: As this loop involves multiple database operations to form an email, might make // sense in the future to launch each async method separately, putting the final results // together when all the information is fetched Gee.List emails = new Gee.ArrayList(); foreach (MessageLocationRow location_row in list) { // fetch the IMAP message location properties that are associated with the generic // message location ImapMessageLocationPropertiesRow? imap_location_row = yield imap_location_table.fetch_async( location_row.id, cancellable); assert(imap_location_row != null); // fetch the message itself MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, required_fields, cancellable); assert(message_row != null); // only add to the list if the email contains all the required fields if (!message_row.fields.is_set(required_fields)) continue; emails.add(message_row.to_email(new Geary.Imap.EmailLocation(location_row.position, imap_location_row.uid))); } return (emails.size > 0) ? emails : null; } public async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { assert(position >= 1); check_open(); MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position, cancellable); if (location_row == null) throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name); assert(location_row.position == position); ImapMessageLocationPropertiesRow? imap_location_row = yield imap_location_table.fetch_async( location_row.id, cancellable); if (imap_location_row == null) { throw new EngineError.NOT_FOUND("No IMAP location properties at position %d in %s", position, name); } assert(imap_location_row.location_id == location_row.id); MessageRow? message_row = yield message_table.fetch_async(location_row.message_id, required_fields, cancellable); if (message_row == null) throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name); if (!message_row.fields.is_set(required_fields)) { throw new EngineError.INCOMPLETE_MESSAGE( "Message at position %d in folder %s only fulfills %Xh fields", position, to_string(), message_row.fields); } return message_row.to_email(new Geary.Imap.EmailLocation(location_row.position, imap_location_row.uid)); } public async bool is_email_present_at(int position, out Geary.Email.Field available_fields, Cancellable? cancellable = null) throws Error { check_open(); available_fields = Geary.Email.Field.NONE; MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position, cancellable); if (location_row == null) return false; return yield message_table.fetch_fields_async(location_row.message_id, out available_fields, cancellable); } public async bool is_email_associated_async(Geary.Email email, Cancellable? cancellable = null) throws Error { check_open(); int64 message_id; return yield imap_location_table.search_uid_in_folder( ((Geary.Imap.EmailLocation) email.location).uid, folder_row.id, out message_id, cancellable); } public async void update_email_async(Geary.Email email, bool duplicate_okay, Cancellable? cancellable = null) throws Error { check_open(); Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location; // See if the message can be identified in the folder (which both reveals association and // a message_id that can be used for a merge; note that this works without a Message-ID) int64 message_id; bool associated = yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id, out message_id, cancellable); // If working around the lack of a Message-ID and not associated with this folder, treat // this operation as a create; otherwise, since a folder-association is determined, do // a merge if (email.message_id == null) { if (!associated) { if (!duplicate_okay) throw new EngineError.INCOMPLETE_MESSAGE("No Message-ID"); yield create_email_async(email, cancellable); } else { yield merge_email_async(message_id, email, cancellable); } return; } // If not associated, find message with matching Message-ID if (!associated) { Gee.List? list = yield message_table.search_message_id_async(email.message_id, cancellable); // If none found, this operation is a create if (list == null || list.size == 0) { yield create_email_async(email, cancellable); return; } // Too many found turns this operation into a create if (list.size != 1) { yield create_email_async(email, cancellable); return; } message_id = list[0]; } // Found a message. If not associated with this folder, associate now. // TODO: Need to lock the database during this operation, as these steps should be atomic. if (!associated) { // see if an email exists at this position MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, location.position); if (location_row != null) { throw new EngineError.ALREADY_EXISTS("Email already exists at position %d in %s", email.location.position, to_string()); } // insert email at supplied position location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id, folder_row.id, location.position); int64 location_id = yield location_table.create_async(location_row, cancellable); // update position propeties ImapMessageLocationPropertiesRow imap_location_row = new ImapMessageLocationPropertiesRow( imap_location_table, Row.INVALID_ID, location_id, location.uid); yield imap_location_table.create_async(imap_location_row, cancellable); } // Merge any new information with the existing message in the local store yield merge_email_async(message_id, email, cancellable); // Done. } // TODO: The database should be locked around this method, as it should be atomic. // TODO: Merge email properties private async void merge_email_async(int64 message_id, Geary.Email email, Cancellable? cancellable = null) throws Error { assert(message_id != Row.INVALID_ID); // if nothing to merge, nothing to do if (email.fields == Geary.Email.Field.NONE) return; MessageRow? message_row = yield message_table.fetch_async(message_id, email.fields, cancellable); assert(message_row != null); message_row.merge_from_network(email); // possible nothing has changed or been added if (message_row.fields != Geary.Email.Field.NONE) yield message_table.merge_async(message_row, cancellable); } }