diff --git a/.gitignore b/.gitignore index a7659743..c61ea732 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ /gearyd /geary-mailer /geary-console +/geary-db-test *.xz *.swp vapi/gmime-2.6/gmime-2.6.gi diff --git a/Makefile.in b/Makefile.in index b01b9613..12ec9159 100644 --- a/Makefile.in +++ b/Makefile.in @@ -3,7 +3,7 @@ # Copyright 2012 Yorba Foundation BUILD_DIR := build -BINARIES := geary gearyd geary-console geary-mailer +BINARIES := geary gearyd geary-console geary-mailer geary-db-test BUILD_BINARIES := $(addprefix $(BUILD_DIR)/,$(BINARIES)) diff --git a/debian/control b/debian/control index b75c1f11..6c6271a1 100644 --- a/debian/control +++ b/debian/control @@ -13,7 +13,7 @@ Build-Depends: debhelper (>= 7), libgmime-2.6-dev (>= 2.6.0), valac-0.18 (>= 0.17.2), cmake (>= 2.8.0), - libsqlheavy0.1-dev (>= 0.1.1), + libsqlite3-dev (>= 3.7.4) libgnome-keyring-dev (>= 3.2.0) Standards-Version: 3.8.3 Homepage: http://www.yorba.org @@ -29,7 +29,7 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, libcanberra0 (>= 0.28), libwebkitgtk-3.0-0 (>= 1.4.3), libxml2 (>= 2.6.32), - libsqlheavy0.1-0 (>= 0.1.1) + libsqlite3 (>= 3.7.4) Description: Email client Geary is an email client built for the GNOME desktop environment. It allows you to read and write email with a simple, modern interface. diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 3366a621..24b73c9e 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -1,6 +1,6 @@ set(SQL_DEST share/geary/sql) -install(FILES Create.sql DESTINATION ${SQL_DEST}) -install(FILES Version-002.sql DESTINATION ${SQL_DEST}) -install(FILES Version-003.sql DESTINATION ${SQL_DEST}) - +install(FILES version-001.sql DESTINATION ${SQL_DEST}) +install(FILES version-002.sql DESTINATION ${SQL_DEST}) +install(FILES version-003.sql DESTINATION ${SQL_DEST}) +install(FILES version-004.sql DESTINATION ${SQL_DEST}) diff --git a/sql/Create.sql b/sql/version-001.sql similarity index 100% rename from sql/Create.sql rename to sql/version-001.sql diff --git a/sql/Version-002.sql b/sql/version-002.sql similarity index 100% rename from sql/Version-002.sql rename to sql/version-002.sql diff --git a/sql/Version-003.sql b/sql/version-003.sql similarity index 100% rename from sql/Version-003.sql rename to sql/version-003.sql diff --git a/sql/version-004.sql b/sql/version-004.sql new file mode 100644 index 00000000..93d7239a --- /dev/null +++ b/sql/version-004.sql @@ -0,0 +1,42 @@ + +-- +-- Migrate ImapFolderPropertiesTable into FolderTable +-- + +ALTER TABLE FolderTable ADD COLUMN last_seen_total INTEGER; +ALTER TABLE FolderTable ADD COLUMN uid_validity INTEGER; +ALTER TABLE FolderTable ADD COLUMN uid_next INTEGER; +ALTER TABLE FolderTable ADD COLUMN attributes TEXT; + +UPDATE FolderTable + SET + last_seen_total = (SELECT ImapFolderPropertiesTable.last_seen_total FROM ImapFolderPropertiesTable WHERE ImapFolderPropertiesTable.folder_id = FolderTable.id), + uid_validity = (SELECT ImapFolderPropertiesTable.uid_validity FROM ImapFolderPropertiesTable WHERE ImapFolderPropertiesTable.folder_id = FolderTable.id), + uid_next = (SELECT ImapFolderPropertiesTable.uid_next FROM ImapFolderPropertiesTable WHERE ImapFolderPropertiesTable.folder_id = FolderTable.id), + attributes = (SELECT ImapFolderPropertiesTable.attributes FROM ImapFolderPropertiesTable WHERE ImapFolderPropertiesTable.folder_id = FolderTable.id) + WHERE EXISTS + (SELECT * FROM ImapFolderPropertiesTable WHERE ImapFolderPropertiesTable.folder_id = FolderTable.id); + +DROP TABLE ImapFolderPropertiesTable; + +-- +-- Migrate ImapMessagePropertiesTable into MessageTable +-- + +ALTER TABLE MessageTable ADD COLUMN flags TEXT; +ALTER TABLE MessageTable ADD COLUMN internaldate TEXT; +ALTER TABLE MessageTable ADD COLUMN rfc822_size INTEGER; + +CREATE INDEX MessageTableInternalDateIndex ON MessageTable(internaldate); +CREATE INDEX MessageTableRfc822SizeIndex ON MessageTable(rfc822_size); + +UPDATE MessageTable + SET + flags = (SELECT ImapMessagePropertiesTable.flags FROM ImapMessagePropertiesTable WHERE ImapMessagePropertiesTable.message_id = MessageTable.id), + internaldate = (SELECT ImapMessagePropertiesTable.internaldate FROM ImapMessagePropertiesTable WHERE ImapMessagePropertiesTable.message_id = MessageTable.id), + rfc822_size = (SELECT ImapMessagePropertiesTable.rfc822_size FROM ImapMessagePropertiesTable WHERE ImapMessagePropertiesTable.message_id = MessageTable.ID) + WHERE EXISTS + (SELECT * FROM ImapMessagePropertiesTable WHERE ImapMessagePropertiesTable.message_id = MessageTable.id); + +DROP TABLE ImapMessagePropertiesTable; + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d3908f50..79afdf82 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,7 @@ common/common-yorba-application.vala set(ENGINE_SRC engine/api/geary-account.vala engine/api/geary-account-information.vala +engine/api/geary-account-settings.vala engine/api/geary-attachment.vala engine/api/geary-batch-operations.vala engine/api/geary-composed-email.vala @@ -42,6 +43,19 @@ engine/api/geary-special-folder-type.vala engine/common/common-message-data.vala +engine/db/database-error.vala +engine/db/db.vala +engine/db/db-connection.vala +engine/db/db-context.vala +engine/db/db-database.vala +engine/db/db-result.vala +engine/db/db-statement.vala +engine/db/db-synchronous-mode.vala +engine/db/db-transaction-async-job.vala +engine/db/db-transaction-outcome.vala +engine/db/db-transaction-type.vala +engine/db/db-versioned-database.vala + engine/imap/api/imap-account.vala engine/imap/api/imap-email-flags.vala engine/imap/api/imap-email-identifier.vala @@ -87,6 +101,15 @@ engine/imap/transport/imap-mailbox.vala engine/imap/transport/imap-serializable.vala engine/imap/transport/imap-serializer.vala +engine/imap-db/imap-db-account.vala +engine/imap-db/imap-db-database.vala +engine/imap-db/imap-db-folder.vala +engine/imap-db/imap-db-message-row.vala +engine/imap-db/outbox/smtp-outbox-email-identifier.vala +engine/imap-db/outbox/smtp-outbox-email-properties.vala +engine/imap-db/outbox/smtp-outbox-folder.vala +engine/imap-db/outbox/smtp-outbox-folder-root.vala + engine/impl/geary-abstract-account.vala engine/impl/geary-abstract-folder.vala engine/impl/geary-email-flag-watcher.vala @@ -104,9 +127,6 @@ engine/impl/geary-replay-queue.vala engine/impl/geary-send-replay-operations.vala engine/impl/geary-yahoo-account.vala engine/impl/geary-yahoo-folder.vala -engine/impl/outbox/smtp-outbox-folder.vala -engine/impl/outbox/smtp-outbox-email-identifier.vala -engine/impl/outbox/smtp-outbox-email-properties.vala engine/nonblocking/nonblocking-abstract-semaphore.vala engine/nonblocking/nonblocking-batch.vala @@ -138,29 +158,6 @@ engine/smtp/smtp-response.vala engine/smtp/smtp-response-code.vala engine/smtp/smtp-response-line.vala -engine/sqlite/abstract/sqlite-database.vala -engine/sqlite/abstract/sqlite-row.vala -engine/sqlite/abstract/sqlite-table.vala -engine/sqlite/abstract/sqlite-transaction.vala -engine/sqlite/api/sqlite-account.vala -engine/sqlite/api/sqlite-folder.vala -engine/sqlite/email/sqlite-folder-row.vala -engine/sqlite/email/sqlite-folder-table.vala -engine/sqlite/email/sqlite-mail-database.vala -engine/sqlite/email/sqlite-message-attachment-row.vala -engine/sqlite/email/sqlite-message-attachment-table.vala -engine/sqlite/email/sqlite-message-location-row.vala -engine/sqlite/email/sqlite-message-location-table.vala -engine/sqlite/email/sqlite-message-row.vala -engine/sqlite/email/sqlite-message-table.vala -engine/sqlite/imap/sqlite-imap-database.vala -engine/sqlite/imap/sqlite-imap-folder-properties-row.vala -engine/sqlite/imap/sqlite-imap-folder-properties-table.vala -engine/sqlite/imap/sqlite-imap-message-properties-row.vala -engine/sqlite/imap/sqlite-imap-message-properties-table.vala -engine/sqlite/smtp/sqlite-smtp-outbox-row.vala -engine/sqlite/smtp/sqlite-smtp-outbox-table.vala - engine/state/state-machine-descriptor.vala engine/state/state-machine.vala engine/state/state-mapping.vala @@ -224,6 +221,10 @@ set(MAILER_SRC mailer/main.vala ) +set(DBTEST_SRC +db-test/main.vala +) + set(DBUSSERVICE_SRC dbusservice/controller.vala dbusservice/database.vala @@ -251,14 +252,13 @@ pkg_check_modules(DEPS REQUIRED libnotify>=0.7.5 libcanberra>=0.28 sqlite3>=3.7.4 - sqlheavy-0.1>=0.1.1 gmime-2.6>=2.6.0 gnome-keyring-1>=2.32.0 webkitgtk-3.0>=1.4.3 ) set(ENGINE_PACKAGES - glib-2.0 gee-1.0 gio-2.0 sqlheavy-0.1 gmime-2.6 unique-3.0 posix + glib-2.0 gee-1.0 gio-2.0 gmime-2.6 unique-3.0 posix sqlite3 ) set(CLIENT_PACKAGES @@ -397,6 +397,31 @@ add_custom_command( ${CMAKE_COMMAND} -E copy geary-mailer ${CMAKE_BINARY_DIR}/ ) +# db-test app +################################################# +vala_precompile(DBTEST_VALA_C + ${DBTEST_SRC} +PACKAGES + ${ENGINE_PACKAGES} + geary-static +OPTIONS + --vapidir=${CMAKE_SOURCE_DIR}/vapi + --vapidir=${CMAKE_BINARY_DIR}/src + --thread + --enable-checking + --fatal-warnings +) + +add_executable(geary-db-test ${DBTEST_VALA_C}) +target_link_libraries(geary-db-test ${DEPS_LIBRARIES} gthread-2.0 geary-static) +add_custom_command( + TARGET + geary-db-test + POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E copy geary-db-test ${CMAKE_BINARY_DIR}/ +) + # DBus Service ################################################# vala_precompile(DBUS_VALA_C diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index 1dd95241..a3a47b7b 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -373,7 +373,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., case Geary.Account.Problem.LOGIN_FAILED: debug("Login failed."); - Geary.Credentials old_credentials = account.get_account_information().credentials; + Geary.Credentials old_credentials = account.settings.credentials; account.report_problem.disconnect(on_report_problem); open_account(old_credentials.user, old_credentials.pass); break; diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 294dc0d7..bbe718a9 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -298,7 +298,7 @@ public class GearyController { account.folders_added_removed.connect(on_folders_added_removed); account.email_sent.connect(on_sent); - if (account.get_account_information().service_provider == Geary.ServiceProvider.YAHOO) + if (account.settings.service_provider == Geary.ServiceProvider.YAHOO) main_window.title = GearyApplication.NAME + "!"; main_window.folder_list.set_user_folders_root_name(_("Labels")); @@ -1243,8 +1243,7 @@ public class GearyController { error("Unable to get username. Error: %s", e.message); } - Geary.AccountInformation acct_info = account.get_account_information(); - return new Geary.RFC822.MailboxAddress(acct_info.real_name, username); + return new Geary.RFC822.MailboxAddress(account.settings.real_name, username); } private Geary.RFC822.MailboxAddresses get_from() { diff --git a/src/db-test/main.vala b/src/db-test/main.vala new file mode 100644 index 00000000..511bac01 --- /dev/null +++ b/src/db-test/main.vala @@ -0,0 +1,122 @@ +/* Copyright 2012 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. + */ + +class DbTest : MainAsync { + public const string DB_FILENAME="db-test.db"; + + public DbTest(string[] args) { + base (args); + } + + protected async override int exec_async() throws Error { + if (args.length < 2) { + stderr.printf("usage: db-test \n"); + + return 1; + } + + Geary.Logging.log_to(stdout); + + File schema_dir = + File.new_for_commandline_arg(args[0]).get_parent().get_child("src").get_child("db-test"); + File data_file = File.new_for_commandline_arg(args[1]).get_child(DB_FILENAME); + + debug("schema_dir=%s data_file=%s\n", schema_dir.get_path(), data_file.get_path()); + + Geary.Db.VersionedDatabase db = new Geary.Db.VersionedDatabase(data_file, schema_dir); + + debug("Opening %s...", db.db_file.get_path()); + db.open(Geary.Db.DatabaseFlags.CREATE_DIRECTORY | Geary.Db.DatabaseFlags.CREATE_FILE, + on_prepare_connection); + + Geary.Db.Connection cx = db.open_connection(); + + debug("Sync select..."); + Geary.Db.TransactionOutcome outcome = cx.exec_transaction(Geary.Db.TransactionType.RO, (cx) => { + Geary.Db.Result result = cx.prepare("SELECT str FROM AnotherTable").exec(); + + int ctr = 0; + while (!result.finished) { + stdout.printf("[%d] %s\n", ctr++, result.string_at(0)); + result.next(); + } + + return Geary.Db.TransactionOutcome.COMMIT; + }); + debug("Sync select: %s", outcome.to_string()); + + outcome = cx.exec_transaction(Geary.Db.TransactionType.WO, (cx) => { + for (int ctr = 0; ctr < 10; ctr++) + cx.prepare("INSERT INTO AnotherTable (str) VALUES (?)").bind_string(0, ctr.to_string()).exec(); + + return Geary.Db.TransactionOutcome.COMMIT; + }); + + debug("Async select"); + outcome = yield db.exec_transaction_async(Geary.Db.TransactionType.RO, (cx) => { + Geary.Db.Result result = cx.prepare("SELECT str FROM AnotherTable").exec(); + + int ctr = 0; + while (!result.finished) { + stdout.printf("[%d]a %s\n", ctr++, result.string_at(0)); + result.next(); + } + + return Geary.Db.TransactionOutcome.COMMIT; + }, null); + debug("Async select finished"); + + debug("Multi async write"); + db.exec("DELETE FROM MultiTable"); + db.exec_transaction_async.begin(Geary.Db.TransactionType.RW, (cx) => { + return do_insert_async(cx, 0); + }, null, on_async_completed); + db.exec_transaction_async.begin(Geary.Db.TransactionType.RW, (cx) => { + return do_insert_async(cx, 100); + }, null, on_async_completed); + db.exec_transaction_async.begin(Geary.Db.TransactionType.RW, (cx) => { + return do_insert_async(cx, 1000); + }, null, on_async_completed); + + yield; + + debug("Exiting..."); + + return 0; + } + + private Geary.Db.TransactionOutcome do_insert_async(Geary.Db.Connection cx, int start) throws Error { + for (int ctr = start; ctr < (start + 10); ctr++) { + cx.prepare("INSERT INTO MultiTable (str) VALUES (?)").bind_int(0, ctr).exec(); + + debug("%d sleeping...", start); + Thread.usleep(10); + debug("%d woke up", start); + } + + return Geary.Db.TransactionOutcome.COMMIT; + } + + private void on_async_completed(Object? source, AsyncResult result) { + Geary.Db.Database db = (Geary.Db.Database) source; + + try { + stdout.printf("Completed: %s\n", db.exec_transaction_async.end(result).to_string()); + } catch (Error err) { + stdout.printf("Completed w/ err: %s\n", err.message); + } + } + + private void on_prepare_connection(Geary.Db.Connection cx) throws Error { + cx.set_busy_timeout_msec(1000); + cx.set_synchronous(Geary.Db.SynchronousMode.OFF); + } +} + +int main(string[] args) { + return new DbTest(args).exec(); +} + diff --git a/src/db-test/version-001.sql b/src/db-test/version-001.sql new file mode 100644 index 00000000..c13b79cc --- /dev/null +++ b/src/db-test/version-001.sql @@ -0,0 +1,9 @@ + +CREATE TABLE TestTable ( + id INTEGER PRIMARY KEY, + str TEXT, + num INTEGER +); + +CREATE INDEX TestTableIntIndex ON TestTable(num); + diff --git a/src/db-test/version-002.sql b/src/db-test/version-002.sql new file mode 100644 index 00000000..59e65c86 --- /dev/null +++ b/src/db-test/version-002.sql @@ -0,0 +1,7 @@ + +CREATE TABLE AnotherTable ( + id INTEGER PRIMARY KEY, + str TEXT +); + +CREATE INDEX AnotherTableIndex ON AnotherTable(str) diff --git a/src/db-test/version-003.sql b/src/db-test/version-003.sql new file mode 100644 index 00000000..a10bf5a9 --- /dev/null +++ b/src/db-test/version-003.sql @@ -0,0 +1,5 @@ + +CREATE TABLE MultiTable ( + str TEXT +); + diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala index dd8c0631..da65afd4 100644 --- a/src/engine/api/geary-account-information.vala +++ b/src/engine/api/geary-account-information.vala @@ -74,10 +74,10 @@ public class Geary.AccountInformation : Object { } public async bool validate_async(Cancellable? cancellable = null) throws EngineError { - Geary.Endpoint endpoint = get_imap_endpoint(); + AccountSettings settings = new AccountSettings(this); - Geary.Imap.ClientSessionManager client_session_manager = - new Geary.Imap.ClientSessionManager(endpoint, credentials, this, 0); + Geary.Imap.ClientSessionManager client_session_manager = new Geary.Imap.ClientSessionManager( + settings, 0); Geary.Imap.ClientSession? client_session = null; try { client_session = yield client_session_manager.get_authorized_session_async(cancellable); @@ -87,8 +87,7 @@ public class Geary.AccountInformation : Object { if (client_session != null) { string current_mailbox; - Geary.Imap.ClientSession.Context context = client_session.get_context(out current_mailbox); - return context == Geary.Imap.ClientSession.Context.AUTHORIZED; + return client_session.get_context(out current_mailbox) == Geary.Imap.ClientSession.Context.AUTHORIZED; } return false; @@ -139,26 +138,23 @@ public class Geary.AccountInformation : Object { } public Geary.EngineAccount get_account() throws EngineError { - Geary.Sqlite.Account sqlite_account = - new Geary.Sqlite.Account(credentials.user); - Endpoint imap_endpoint = get_imap_endpoint(); - Endpoint smtp_endpoint = get_smtp_endpoint(); + AccountSettings settings = new AccountSettings(this); + + ImapDB.Account local_account = new ImapDB.Account(settings); + Imap.Account remote_account = new Imap.Account(settings); switch (service_provider) { case ServiceProvider.GMAIL: - return new GmailAccount("Gmail account %s".printf(credentials.to_string()), - credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account(imap_endpoint, - smtp_endpoint, credentials, this), sqlite_account); + return new GmailAccount("Gmail account %s".printf(credentials.to_string()), settings, + remote_account, local_account); case ServiceProvider.YAHOO: - return new YahooAccount("Yahoo account %s".printf(credentials.to_string()), - credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account(imap_endpoint, - smtp_endpoint, credentials, this), sqlite_account); + return new YahooAccount("Yahoo account %s".printf(credentials.to_string()), settings, + remote_account, local_account); case ServiceProvider.OTHER: - return new OtherAccount("Other account %s".printf(credentials.to_string()), - credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account(imap_endpoint, - smtp_endpoint, credentials, this), sqlite_account); + return new OtherAccount("Other account %s".printf(credentials.to_string()), settings, + remote_account, local_account); default: throw new EngineError.NOT_FOUND("Service provider of type %s not known", diff --git a/src/engine/api/geary-account-settings.vala b/src/engine/api/geary-account-settings.vala new file mode 100644 index 00000000..100a2983 --- /dev/null +++ b/src/engine/api/geary-account-settings.vala @@ -0,0 +1,31 @@ +/* Copyright 2012 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. + */ + +/** + * AccountSettings is a complement to AccountInformation. AccountInformation stores these settings + * as well as defaults and provides validation and persistence functionality. Settings is simply + * the values loaded from a backing store, perhaps chosen from defaults, and validated filtered + * down to a set of working settings for the Account to use. + */ + +public class Geary.AccountSettings { + public string real_name { get; private set; } + public Geary.Credentials credentials { get; private set; } + public Geary.ServiceProvider service_provider { get; private set; } + public bool imap_server_pipeline { get; private set; } + public Endpoint imap_endpoint { get; private set; } + public Endpoint smtp_endpoint { get; private set; } + + internal AccountSettings(AccountInformation info) throws EngineError { + real_name = info.real_name; + credentials = info.credentials; + service_provider = info.service_provider; + imap_server_pipeline = info.imap_server_pipeline; + imap_endpoint = info.get_imap_endpoint(); + smtp_endpoint = info.get_smtp_endpoint(); + } +} + diff --git a/src/engine/api/geary-batch-operations.vala b/src/engine/api/geary-batch-operations.vala index 5681da1d..b495a6e2 100755 --- a/src/engine/api/geary-batch-operations.vala +++ b/src/engine/api/geary-batch-operations.vala @@ -12,13 +12,13 @@ * is stored in the created property. */ private class Geary.CreateLocalEmailOperation : Geary.NonblockingBatchOperation { - public Geary.Sqlite.Folder folder { get; private set; } + public ImapDB.Folder folder { get; private set; } public Geary.Email email { get; private set; } public Geary.Email.Field required_fields { get; private set; } public bool created { get; private set; default = false; } public Geary.Email? merged { get; private set; default = null; } - public CreateLocalEmailOperation(Geary.Sqlite.Folder folder, Geary.Email email, + public CreateLocalEmailOperation(ImapDB.Folder folder, Geary.Email email, Geary.Email.Field required_fields) { this.folder = folder; this.email = email; @@ -26,12 +26,12 @@ private class Geary.CreateLocalEmailOperation : Geary.NonblockingBatchOperation } public override async Object? execute_async(Cancellable? cancellable) throws Error { - created = yield folder.create_email_async(email, cancellable); + created = yield folder.create_or_merge_email_async(email, cancellable); if (email.fields.fulfills(required_fields)) { merged = email; } else { - merged = yield folder.fetch_email_async(email.id, required_fields, Sqlite.Folder.ListFlags.NONE, + merged = yield folder.fetch_email_async(email.id, required_fields, ImapDB.Folder.ListFlags.NONE, cancellable); } @@ -47,16 +47,19 @@ private class Geary.CreateLocalEmailOperation : Geary.NonblockingBatchOperation * returned value. */ private class Geary.RemoveLocalEmailOperation : Geary.NonblockingBatchOperation { - public Geary.Sqlite.Folder folder { get; private set; } + public ImapDB.Folder folder { get; private set; } public Geary.EmailIdentifier email_id { get; private set; } - public RemoveLocalEmailOperation(Geary.Sqlite.Folder folder, Geary.EmailIdentifier email_id) { + public RemoveLocalEmailOperation(ImapDB.Folder folder, Geary.EmailIdentifier email_id) { this.folder = folder; this.email_id = email_id; } public override async Object? execute_async(Cancellable? cancellable) throws Error { - yield folder.remove_single_email_async(email_id, cancellable); + Gee.List list = new Gee.ArrayList(); + list.add(email_id); + + yield folder.remove_email_async(list, cancellable); return null; } diff --git a/src/engine/api/geary-email.vala b/src/engine/api/geary-email.vala index d970911a..7f3f30a1 100644 --- a/src/engine/api/geary-email.vala +++ b/src/engine/api/geary-email.vala @@ -242,7 +242,11 @@ public class Geary.Email : Object { public void add_attachment(Geary.Attachment attachment) { attachments.add(attachment); } - + + public void add_attachments(Gee.Collection attachments) { + this.attachments.add_all(attachments); + } + /** * This method requires Geary.Email.Field.HEADER and Geary.Email.Field.BODY be present. * If not, EngineError.INCOMPLETE_MESSAGE is thrown. diff --git a/src/engine/api/geary-engine-account.vala b/src/engine/api/geary-engine-account.vala index d15e2919..54b531cc 100644 --- a/src/engine/api/geary-engine-account.vala +++ b/src/engine/api/geary-engine-account.vala @@ -5,28 +5,21 @@ */ public abstract class Geary.EngineAccount : Geary.AbstractAccount { - private AccountInformation account_information; + public virtual Geary.AccountSettings settings { get; private set; } public virtual signal void email_sent(Geary.RFC822.Message rfc822) { } - public EngineAccount(string name, string username, AccountInformation account_information, - File user_data_dir) { + internal EngineAccount(string name, AccountSettings settings) { base (name); - this.account_information = account_information; + this.settings = settings; } protected virtual void notify_email_sent(Geary.RFC822.Message rfc822) { email_sent(rfc822); } - public virtual AccountInformation get_account_information() { - return account_information; - } - - public abstract bool delete_is_archive(); - public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) throws Error; } diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala index f84da01e..898bedc5 100644 --- a/src/engine/api/geary-engine.vala +++ b/src/engine/api/geary-engine.vala @@ -5,19 +5,22 @@ */ public class Geary.Engine { - private static bool gmime_inited = false; public static File? user_data_dir { get; private set; default = null; } public static File? resource_dir { get; private set; default = null; } + + private static bool inited = false; public static void init(File _user_data_dir, File _resource_dir) { + if (inited) + return; + user_data_dir = _user_data_dir; resource_dir = _resource_dir; // Initialize GMime - if (!gmime_inited) { - GMime.init(0); - gmime_inited = true; - } + GMime.init(0); + + inited = true; } // Returns a list of usernames associated with Geary. diff --git a/src/engine/db/database-error.vala b/src/engine/db/database-error.vala new file mode 100755 index 00000000..6c27d4ef --- /dev/null +++ b/src/engine/db/database-error.vala @@ -0,0 +1,19 @@ +/* Copyright 2012 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. + */ + +public errordomain DatabaseError { + GENERAL, + OPEN_REQUIRED, + BUSY, + BACKING, + MEMORY, + ABORT, + INTERRUPT, + LIMITS, + TYPESPEC, + FINISHED +} + diff --git a/src/engine/db/db-connection.vala b/src/engine/db/db-connection.vala new file mode 100755 index 00000000..d3496c40 --- /dev/null +++ b/src/engine/db/db-connection.vala @@ -0,0 +1,334 @@ +/* Copyright 2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A Connection represents a connection to an open database. Because SQLite uses a + * synchronous interface, all calls are blocking. Db.Database offers asynchronous queries by + * pooling connections and invoking queries from background threads. + * + * Connections are associated with a Database. Use Database.open_connection() to create + * one. + * + * A Connection will close when its last reference is dropped. + */ + +public class Geary.Db.Connection : Geary.Db.Context { + public const int DEFAULT_BUSY_TIMEOUT_MSEC = 0; + + private const string PRAGMA_FOREIGN_KEYS = "foreign_keys"; + private const string PRAGMA_RECURSIVE_TRIGGERS = "recursive_triggers"; + private const string PRAGMA_USER_VERSION = "user_version"; + private const string PRAGMA_SCHEMA_VERSION = "schema_version"; + private const string PRAGMA_SECURE_DELETE = "secure_delete"; + private const string PRAGMA_SYNCHRONOUS = "synchronous"; + + /** + * See http://www.sqlite.org/c3ref/last_insert_rowid.html + */ + public int64 last_insert_rowid { get { + return db.last_insert_rowid(); + } } + + /** + * See http://www.sqlite.org/c3ref/changes.html + */ + public int last_modified_rows { get { + return db.changes(); + } } + + /** + * See http://www.sqlite.org/c3ref/total_changes.html + */ + public int total_modified_rows { get { + return db.total_changes(); + } } + + public Database database { get; private set; } + + internal Sqlite.Database db; + + private int busy_timeout_msec = DEFAULT_BUSY_TIMEOUT_MSEC; + + internal Connection(Database database, int sqlite_flags, Cancellable? cancellable) throws Error { + this.database = database; + + check_cancelled("Connection.ctor", cancellable); + + throw_on_error("Connection.ctor", Sqlite.Database.open_v2(database.db_file.get_path(), + out db, sqlite_flags, null)); + } + + /** + * Execute a plain text SQL statement. More than one SQL statement may be in the string. See + * http://www.sqlite.org/lang.html for more information on SQLite's SQL syntax. + * + * There is no way to retrieve a result iterator from this call. + * + * This may be called from a TransactionMethod called within exec_transaction() or + * Db.Database.exec_transaction_async(). + * + * See http://www.sqlite.org/c3ref/exec.html + */ + public void exec(string sql, Cancellable? cancellable = null) throws Error { + check_cancelled("Connection.exec", cancellable); + + throw_on_error("Connection.exec", db.exec(sql), sql); + } + + /** + * Loads a text file of SQL commands into memory and executes them at once with exec(). + * + * There is no way to retrieve a result iterator from this call. + * + * This can be called from a TransactionMethod called within exec_transaction() or + * Db.Database.exec_transaction_async(). + */ + public void exec_file(File file, Cancellable? cancellable = null) throws Error { + check_cancelled("Connection.exec_file", cancellable); + + string sql; + FileUtils.get_contents(file.get_path(), out sql); + + exec(sql, cancellable); + } + + /** + * Executes a plain text SQL statement and returns a Result object directly. + * This call creates an intermediate Statement object which may be fetched from Result.statement. + */ + public Result query(string sql, Cancellable? cancellable = null) throws Error { + return (new Statement(this, sql)).exec(cancellable); + } + + /** + * Prepares a Statement which may have values bound to it and executed. See + * http://www.sqlite.org/c3ref/prepare.html + */ + public Statement prepare(string sql) throws DatabaseError { + return new Statement(this, sql); + } + + /** + * See set_busy_timeout_msec(). + */ + public int get_busy_timeout_msec() { + return busy_timeout_msec; + } + + /** + * Sets SQLite's internal busy timeout handler. This is imperative for exec_transaction() and + * Db.Database.exec_transaction_async(), because those calls will throw a DatabaseError.BUSY + * call immediately if another transaction has acquired the reserved or exclusive locks. + * With this set, SQLite will attempt a retry later, guarding against BUSY under normal + * conditions. See http://www.sqlite.org/c3ref/busy_timeout.html + */ + public void set_busy_timeout_msec(int busy_timeout_msec) throws Error { + if (this.busy_timeout_msec == busy_timeout_msec) + return; + + throw_on_error("Database.set_busy_timeout", db.busy_timeout(busy_timeout_msec)); + this.busy_timeout_msec = busy_timeout_msec; + } + + /** + * Returns the result of a PRAGMA as a boolean. See http://www.sqlite.org/pragma.html. + * + * Note that if the PRAGMA does not return a boolean, the results are undefined. A boolean + * in SQLite, however, includes 1 and 0, so an integer may be mistaken as a boolean. + */ + public bool get_pragma_bool(string name) throws Error { + string response = query("PRAGMA %s".printf(name)).string_at(0); + switch (response.down()) { + case "1": + case "yes": + case "true": + case "on": + return true; + + case "0": + case "no": + case "false": + case "off": + return false; + + default: + debug("Db.Connection.get_pragma_bool: unknown PRAGMA boolean response \"%s\"", + response); + + return false; + } + } + + /** + * Sets a boolean PRAGMA value to either "true" or "false". + */ + public void set_pragma_bool(string name, bool b) throws Error { + exec("PRAGMA %s=%s".printf(name, b ? "true" : "false")); + } + + /** + * Returns the result of a PRAGMA as an integer. See http://www.sqlite.org/pragma.html + * + * Note that if the PRAGMA does not return an integer, the results are undefined. Since a + * boolean in SQLite includes 1 and 0, it's possible for those values to be converted to an + * integer. + */ + public int get_pragma_int(string name) throws Error { + return query("PRAGMA %s".printf(name)).int_at(0); + } + + /** + * Sets an integer PRAGMA value. + */ + public void set_pragma_int(string name, int d) throws Error { + exec("PRAGMA %s=%d".printf(name, d)); + } + + /** + * Returns the result of a PRAGMA as a string. See http://www.sqlite.org/pragma.html + */ + public string get_pragma_string(string name) throws Error { + return query("PRAGMA %s".printf(name)).string_at(0); + } + + /** + * Sets a string PRAGMA value. + */ + public void set_pragma_string(string name, string str) throws Error { + exec("PRAGMA %s=%s".printf(name, str)); + } + + /** + * See set_user_version_number(). + */ + public int get_user_version_number() throws Error { + return get_pragma_int(PRAGMA_USER_VERSION); + } + + /** + * Sets the user version number, which is a private number maintained by the user. + * VersionedDatabase uses this to maintain the version number of the database. + * + * See http://www.sqlite.org/pragma.html#pragma_schema_version + */ + public void set_user_version_number(int version) throws Error { + set_pragma_int(PRAGMA_USER_VERSION, version); + } + + /** + * Gets the schema version number, which is maintained by SQLite. See + * http://www.sqlite.org/pragma.html#pragma_schema_version + * + * Since this number is maintained by SQLite, Geary.Db doesn't offer a way to set it. + */ + public int get_schema_version_number() throws Error { + return get_pragma_int(PRAGMA_SCHEMA_VERSION); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_foreign_keys + */ + public void set_foreign_keys(bool enabled) throws Error { + set_pragma_bool(PRAGMA_FOREIGN_KEYS, enabled); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_foreign_keys + */ + public bool get_foreign_keys() throws Error { + return get_pragma_bool(PRAGMA_FOREIGN_KEYS); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_recursive_triggers + */ + public void set_recursive_triggers(bool enabled) throws Error { + set_pragma_bool(PRAGMA_RECURSIVE_TRIGGERS, enabled); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_recursive_triggers + */ + public bool get_recursive_triggers() throws Error { + return get_pragma_bool(PRAGMA_RECURSIVE_TRIGGERS); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_secure_delete + */ + public void set_secure_delete(bool enabled) throws Error { + set_pragma_bool(PRAGMA_SECURE_DELETE, enabled); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_secure_delete + */ + public bool get_secure_delete() throws Error { + return get_pragma_bool(PRAGMA_SECURE_DELETE); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_synchronous + */ + public void set_synchronous(SynchronousMode mode) throws Error { + set_pragma_string(PRAGMA_SYNCHRONOUS, mode.sql()); + } + + /** + * See http://www.sqlite.org/pragma.html#pragma_synchronous + */ + public SynchronousMode get_synchronous() throws Error { + return SynchronousMode.parse(get_pragma_string(PRAGMA_SYNCHRONOUS)); + } + + /** + * Executes one or more queries inside an SQLite transaction. This call will initiate a + * transaction according to the TransactionType specified (although this is merely an + * optimization -- no matter the transaction type, SQLite guarantees the subsequent operations + * to be atomic). The commands executed inside the TransactionMethod against the + * supplied Db.Connection will be in the context of the transaction. If the TransactionMethod + * returns TransactionOutcome.COMMIT, the transaction will be committed to the database, + * otherwise it will be rolled back and the database left unchanged. + * + * It's inadvisable to call exec_transaction() inside exec_transaction(). SQLite has a notion + * of savepoints that allow for nested transactions; they are not currently supported. + * + * See http://www.sqlite.org/lang_transaction.html + */ + public TransactionOutcome exec_transaction(TransactionType type, TransactionMethod cb, + Cancellable? cancellable = null) throws Error { + // initiate the transaction + exec(type.sql(), cancellable); + + TransactionOutcome outcome = TransactionOutcome.ROLLBACK; + Error? caught_err = null; + try { + // perform the transaction + outcome = cb(this, cancellable); + } catch (Error err) { + debug("Connection.exec_transaction: transaction threw error %s", err.message); + caught_err = err; + } + + // commit/rollback + try { + exec(outcome.sql(), cancellable); + } catch (Error err) { + debug("Connection.exec_transaction: Unable to %s transaction: %s", outcome.to_string(), + err.message); + } + + if (caught_err != null) + throw caught_err; + + return outcome; + } + + public override Connection? get_connection() { + return this; + } +} + diff --git a/src/engine/db/db-context.vala b/src/engine/db/db-context.vala new file mode 100644 index 00000000..bb41250b --- /dev/null +++ b/src/engine/db/db-context.vala @@ -0,0 +1,36 @@ +/* Copyright 2012 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. + */ + +/** + * Context allows for an inspector or utility function to determine at runtime what Geary.Db + * objects are available to it. Primarily designed for logging, but could be used in other + * circumstances. + * + * Geary.Db's major classes (Database, Connection, Statement, and Result) inherit from Context. + */ + +public abstract class Geary.Db.Context : Object { + public virtual Database? get_database() { + return get_connection() != null ? get_connection().database : null; + } + + public virtual Connection? get_connection() { + return get_statement() != null ? get_statement().connection : null; + } + + public virtual Statement? get_statement() { + return get_result() != null ? get_result().statement : null; + } + + public virtual Result? get_result() { + return null; + } + + protected inline int throw_on_error(string? method, int result, string? raw = null) throws DatabaseError { + return Db.throw_on_error(this, method, result, raw); + } +} + diff --git a/src/engine/db/db-database.vala b/src/engine/db/db-database.vala new file mode 100644 index 00000000..d2fcd0e9 --- /dev/null +++ b/src/engine/db/db-database.vala @@ -0,0 +1,246 @@ +/* Copyright 2012 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. + */ + +/** + * Database represents an SQLite file. Multiple Connections may be opened to against the + * Database. + * + * Since it's often just more bookkeeping to maintain a single global connection, Database also + * offers a master connection which may be used to perform queries and transactions. + * + * Database also offers asynchronous transactions which work via connection and thread pools. + * + * NOTE: In-memory databases are currently unsupported. + */ + +public class Geary.Db.Database : Geary.Db.Context { + public const int DEFAULT_MAX_CONCURRENCY = 8; + + public File db_file { get; private set; } + public DatabaseFlags flags { get; private set; } + + private bool _is_open = false; + public bool is_open { + get { + lock (_is_open) { + return _is_open; + } + } + + private set { + lock (_is_open) { + _is_open = value; + } + } + } + + private Connection? master_connection = null; + private int outstanding_async_jobs = 0; + private ThreadPool? thread_pool = null; + private unowned PrepareConnection? prepare_cb = null; + + public Database(File db_file) { + this.db_file = db_file; + } + + ~Database() { + // Not thrilled about using lock in a dtor + lock (outstanding_async_jobs) { + assert(outstanding_async_jobs == 0); + } + } + + /** + * Opens the Database, creating any files and directories it may need in the process depending + * on the DatabaseFlags. + * + * NOTE: A Database may be closed, but the Connections it creates will always be valid as + * they hold a reference to their source Database. To release a Database's resources, drop all + * references to it and its associated Connections, Statements, and Results. + */ + public virtual void open(DatabaseFlags flags, PrepareConnection? prepare_cb, + Cancellable? cancellable = null) throws Error { + if (is_open) + return; + + this.flags = flags; + this.prepare_cb = prepare_cb; + + if ((flags & DatabaseFlags.CREATE_DIRECTORY) != 0) { + File db_dir = db_file.get_parent(); + if (!db_dir.query_exists(cancellable)) + db_dir.make_directory_with_parents(cancellable); + } + + if (threadsafe()) { + assert(thread_pool == null); + thread_pool = new ThreadPool.with_owned_data(on_async_job, + DEFAULT_MAX_CONCURRENCY, true); + } else { + warning("SQLite not thread-safe: asynchronous queries will not be available"); + } + + is_open = true; + } + + /** + * Closes the Database, releasing any resources it may hold, including the master connection. + * + * Note that closing a Database does not close or invalidate Connections it has spawned nor does + * it cancel any scheduled asynchronous jobs pending or in execution. All Connections, + * Statements, and Results will be able to communicate with the database. Only when they are + * destroyed is the Database object finally destroyed. + */ + public virtual void close(Cancellable? cancellable = null) throws Error { + if (!is_open) + return; + + // drop the master connection, which holds a ref back to this object + master_connection = null; + + is_open = false; + } + + private void check_open() throws Error { + if (!is_open) + throw new DatabaseError.OPEN_REQUIRED("Database %s not open", db_file.get_path()); + } + + /** + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public Connection open_connection(Cancellable? cancellable = null) throws Error { + return internal_open_connection(false, cancellable); + } + + private Connection internal_open_connection(bool master, Cancellable? cancellable = null) throws Error { + check_open(); + + int sqlite_flags = (flags & DatabaseFlags.READ_ONLY) != 0 ? Sqlite.OPEN_READONLY + : Sqlite.OPEN_READWRITE; + if ((flags & DatabaseFlags.CREATE_FILE) != 0) + sqlite_flags |= Sqlite.OPEN_CREATE; + + Connection cx = new Connection(this, sqlite_flags, cancellable); + if (prepare_cb != null) + prepare_cb(cx, master); + + return cx; + } + + /** + * The master connection is a general-use connection many of the calls in Database (including + * exec(), exec_file(), query(), prepare(), and exec_trnasaction()) use to perform their work. + * It can also be used by the caller if a dedicated Connection is not required. + * + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public Connection get_master_connection() throws Error { + if (master_connection == null) + master_connection = internal_open_connection(true); + + return master_connection; + } + + /** + * Calls Connection.exec() on the master connection. + * + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public void exec(string sql, Cancellable? cancellable = null) throws Error { + get_master_connection().exec(sql, cancellable); + } + + /** + * Calls Connection.exec_file() on the master connection. + * + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public void exec_file(File file, Cancellable? cancellable = null) throws Error { + get_master_connection().exec_file(file, cancellable); + } + + /** + * Calls Connection.prepare() on the master connection. + * + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public Statement prepare(string sql) throws Error { + return get_master_connection().prepare(sql); + } + + /** + * Calls Connection.query() on the master connection. + * + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public Result query(string sql, Cancellable? cancellable = null) throws Error { + return get_master_connection().query(sql, cancellable); + } + + /** + * Calls Connection.exec_transaction() on the master connection. + * + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public TransactionOutcome exec_transaction(TransactionType type, TransactionMethod cb, + Cancellable? cancellable = null) throws Error { + return get_master_connection().exec_transaction(type, cb, cancellable); + } + + /** + * Asynchronous transactions are handled via background threads using a pool of Connections. + * The background thread calls Connection.exec_transaction(); see that method for more + * information about coding a transaction. The only caveat is that the TransactionMethod + * must be thread-safe. + * + * Throws DatabaseError.OPEN_REQUIRED if not open. + */ + public async TransactionOutcome exec_transaction_async(TransactionType type, TransactionMethod cb, + Cancellable? cancellable) throws Error { + check_open(); + + if (thread_pool == null) + throw new DatabaseError.GENERAL("SQLite thread safety disabled, async operations unallowed"); + + // create job to execute in background thread + TransactionAsyncJob job = new TransactionAsyncJob(type, cb, cancellable); + + lock (outstanding_async_jobs) { + outstanding_async_jobs++; + } + + thread_pool.add(job); + + return yield job.wait_for_completion_async(cancellable); + } + + // This method must be thread-safe. + private void on_async_job(owned TransactionAsyncJob job) { + // create connection for this thread + // TODO: Use connection pool -- *never* use master connection + Connection? cx = null; + try { + cx = open_connection(); + } catch (Error err) { + debug("Warning: unable to open database connection to %s, cancelling AsyncJob: %s", + db_file.get_path(), err.message); + } + + if (cx != null) + job.execute(cx); + + lock (outstanding_async_jobs) { + assert(outstanding_async_jobs > 0); + --outstanding_async_jobs; + } + } + + public override Database? get_database() { + return this; + } +} + diff --git a/src/engine/db/db-result.vala b/src/engine/db/db-result.vala new file mode 100644 index 00000000..c4f2a5ca --- /dev/null +++ b/src/engine/db/db-result.vala @@ -0,0 +1,207 @@ +/* Copyright 2012 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. + */ + +public class Geary.Db.Result : Geary.Db.Context { + public bool finished { get; private set; default = false; } + + public Statement statement { get; private set; } + + // This results in an automatic first next(). + internal Result(Statement statement, Cancellable? cancellable) throws Error { + this.statement = statement; + + statement.resetted.connect(on_query_finished); + statement.bindings_cleared.connect(on_query_finished); + + next(cancellable); + } + + ~Result() { + statement.resetted.disconnect(on_query_finished); + statement.bindings_cleared.disconnect(on_query_finished); + } + + private void on_query_finished() { + finished = true; + } + + /** + * Returns true if results are waiting, false if finished, or throws a DatabaseError. + */ + public bool next(Cancellable? cancellable = null) throws Error { + check_cancelled("Result.step", cancellable); + + if (!finished) + finished = (throw_on_error("Result.step", statement.stmt.step())) != Sqlite.ROW; + + return !finished; + } + + /** + * column is zero-based. + */ + public double double_at(int column) throws DatabaseError { + verify_at(column); + + return statement.stmt.column_double(column); + } + + /** + * column is zero-based. + */ + public int int_at(int column) throws DatabaseError { + verify_at(column); + + return statement.stmt.column_int(column); + } + + /** + * column is zero-based. + */ + public uint uint_at(int column) throws DatabaseError { + return (uint) int64_at(column); + } + + /** + * column is zero-based. + */ + public long long_at(int column) throws DatabaseError { + return (long) int64_at(column); + } + + /** + * column is zero-based. + */ + public int64 int64_at(int column) throws DatabaseError { + verify_at(column); + + return statement.stmt.column_int64(column); + } + + /** + * Returns the column value as a bool. The value is treated as an int and converted into a + * bool: false == 0, true == !0. + * + * column is zero-based. + */ + public bool bool_at(int column) throws DatabaseError { + return int_at(column) != 0; + } + + /** + * column is zero-based. + * + * This is merely a front for int64_at(). It's provided to make the caller's code more verbose. + */ + public int64 rowid_at(int column) throws DatabaseError { + return int64_at(column); + } + + /** + * column is zero-based. + */ + public string string_at(int column) throws DatabaseError { + verify_at(column); + + return statement.stmt.column_text(column); + } + + private void verify_at(int column) throws DatabaseError { + if (finished) + throw new DatabaseError.FINISHED("Query finished"); + + if (column < 0) + throw new DatabaseError.LIMITS("column %d < 0", column); + + int count = statement.get_column_count(); + if (column >= count) + throw new DatabaseError.LIMITS("column %d >= %d", column, count); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + */ + public double double_for(string name) throws DatabaseError { + return double_at(convert_for(name)); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + */ + public int int_for(string name) throws DatabaseError { + return int_at(convert_for(name)); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + */ + public uint uint_for(string name) throws DatabaseError { + return (uint) int64_for(name); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + */ + public long long_for(string name) throws DatabaseError { + return long_at(convert_for(name)); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + */ + public int64 int64_for(string name) throws DatabaseError { + return int64_at(convert_for(name)); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + * + * See bool_at() for information on how the column's value is converted to a bool. + */ + public bool bool_for(string name) throws DatabaseError { + return bool_at(convert_for(name)); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + * + * This is merely a front for int64_at(). It's provided to make the caller's code more verbose. + */ + public int64 rowid_for(string name) throws DatabaseError { + return int64_for(name); + } + + /** + * name is the name of the column in the result set. See Statement.get_column_index() for name + * matching rules. + */ + public string string_for(string name) throws DatabaseError { + return string_at(convert_for(name)); + } + + private int convert_for(string name) throws DatabaseError { + if (finished) + throw new DatabaseError.FINISHED("Query finished"); + + int column = statement.get_column_index(name); + if (column < 0) + throw new DatabaseError.LIMITS("column \"%s\" not in result set", name); + + return column; + } + + public override Result? get_result() { + return this; + } +} + diff --git a/src/engine/db/db-statement.vala b/src/engine/db/db-statement.vala new file mode 100644 index 00000000..bcba5143 --- /dev/null +++ b/src/engine/db/db-statement.vala @@ -0,0 +1,233 @@ +/* Copyright 2012 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. + */ + +public class Geary.Db.Statement : Geary.Db.Context { + private unowned string? raw; + public unowned string sql { get { + return !String.is_empty(raw) ? raw : stmt.sql(); + } } + + public Connection connection { get; private set; } + + internal Sqlite.Statement stmt; + + private Gee.HashMap? column_map = null; + + /** + * Fired when the Statement is executed the first time (after creation or after a reset). + */ + public signal void executed(); + + /** + * Fired when the Statement is reset. + */ + public signal void resetted(); + + /** + * Fired when the Statement's bindings are cleared. + */ + public signal void bindings_cleared(); + + internal Statement(Connection connection, string sql) throws DatabaseError { + this.connection = connection; + // save for logging in case prepare_v2() fails + raw = sql; + + throw_on_error("Statement.ctor", connection.db.prepare_v2(sql, -1, out stmt, null), sql); + + // not needed any longer + raw = null; + } + + /** + * Reset the Statement for reuse, optionally clearing all bindings as well. If bindings are + * not cleared, valued bound previously will be maintained. + * + * See http://www.sqlite.org/c3ref/reset.html and http://www.sqlite.org/c3ref/clear_bindings.html + */ + public Statement reset(ResetScope reset_scope) throws DatabaseError { + if (reset_scope == ResetScope.CLEAR_BINDINGS) + throw_on_error("Statement.clear_bindings", stmt.clear_bindings()); + + throw_on_error("Statement.reset", stmt.reset()); + + // fire signals after Statement has been altered -- this prevents reentrancy while the + // Statement is in a halfway state + if (reset_scope == ResetScope.CLEAR_BINDINGS) + bindings_cleared(); + + resetted(); + + return this; + } + + /** + * Returns the number of columns the Statement will return in a Result. + */ + public int get_column_count() { + return stmt.column_count(); + } + + /** + * Returns the column name for column at the zero-based index. + * + * The name may be used with Result.int_for() (and other *_for() variants). + */ + public unowned string? get_column_name(int index) { + return stmt.column_name(index); + } + + /** + * Returns the zero-based column index matching the column name. Column names are + * case-insensitive. + * + * Returns -1 if column name is unknown. + */ + public int get_column_index(string name) { + // prepare column map only if names requested + if (column_map == null) { + column_map = new Gee.HashMap(Geary.String.stri_hash, Geary.String.stri_equal); + + int cols = stmt.column_count(); + for (int ctr = 0; ctr < cols; ctr++) { + string? column_name = stmt.column_name(ctr); + if (!String.is_empty(column_name)) + column_map.set(column_name, ctr); + } + } + + return column_map.has_key(name) ? column_map.get(name) : -1; + } + + /** + * Executes the Statement and returns a Result object. The Result starts pointing at the first + * row in the result set. If empty, Result.finished will be true. + */ + public Result exec(Cancellable? cancellable = null) throws Error { + Result results = new Result(this, cancellable); + + executed(); + + return results; + } + + /** + * Executes the Statement and returns the last inserted rowid. If this Statement is not + * an INSERT, it will return the rowid of the last prior INSERT. + * + * See Connection.last_insert_rowid. + */ + public int64 exec_insert(Cancellable? cancellable = null) throws Error { + new Result(this, cancellable); + int64 rowid = connection.last_insert_rowid; + + // fire signal after safely retrieving the rowid + executed(); + + return rowid; + } + + /** + * Executes the Statement and returns the number of rows modified by the operation. This + * Statement should be an INSERT, UPDATE, or DELETE, otherwise this will return the number + * of modified rows from the last INSERT, UPDATE, or DELETE. + * + * See Connection.last_modified_rows. + */ + public int exec_get_modified(Cancellable? cancellable = null) throws Error { + new Result(this, cancellable); + int modified = connection.last_modified_rows; + + // fire signal after safely retrieving the count + executed(); + + return modified; + } + + /** + * index is zero-based. + */ + public Statement bind_double(int index, double d) throws DatabaseError { + throw_on_error("Statement.bind_double", stmt.bind_double(index + 1, d)); + + return this; + } + + /** + * index is zero-based. + */ + public Statement bind_int(int index, int i) throws DatabaseError { + throw_on_error("Statement.bind_int", stmt.bind_int(index + 1, i)); + + return this; + } + + /** + * index is zero-based. + */ + public Statement bind_uint(int index, uint u) throws DatabaseError { + return bind_int64(index, (int64) u); + } + + /** + * index is zero-based. + */ + public Statement bind_long(int index, long l) throws DatabaseError { + return bind_int64(index, (int64) l); + } + + /** + * index is zero-based. + */ + public Statement bind_int64(int index, int64 i64) throws DatabaseError { + throw_on_error("Statement.bind_int64", stmt.bind_int64(index + 1, i64)); + + return this; + } + + /** + * Binds a bool to the column. A bool is stored as an integer, false == 0, true == 1. Note + * that fetching a bool via Result is more lenient; see Result.bool_at() and Result.bool_from(). + * + * index is zero-based. + */ + public Statement bind_bool(int index, bool b) throws DatabaseError { + return bind_int(index, b ? 1 : 0); + } + + /** + * index is zero-based. + * + * This is merely a front for bind_int64(). It's provided to offer more verbosity in the + * caller's code. + */ + public Statement bind_rowid(int index, int64 rowid) throws DatabaseError { + return bind_int64(index, rowid); + } + + /** + * index is zero-based. + */ + public Statement bind_null(int index) throws DatabaseError { + throw_on_error("Statement.bind_null", stmt.bind_null(index + 1)); + + return this; + } + + /** + * index is zero-based. + */ + public Statement bind_string(int index, string? s) throws DatabaseError { + throw_on_error("Statement.bind_string", stmt.bind_text(index + 1, s)); + + return this; + } + + public override Statement? get_statement() { + return this; + } +} + diff --git a/src/engine/db/db-synchronous-mode.vala b/src/engine/db/db-synchronous-mode.vala new file mode 100644 index 00000000..5a224eb6 --- /dev/null +++ b/src/engine/db/db-synchronous-mode.vala @@ -0,0 +1,40 @@ +/* Copyright 2012 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. + */ + +public enum Geary.Db.SynchronousMode { + OFF = 0, + NORMAL = 1, + FULL = 2; + + public unowned string sql() { + switch (this) { + case OFF: + return "off"; + + case NORMAL: + return "normal"; + + case FULL: + default: + return "full"; + } + } + + public static SynchronousMode parse(string str) { + switch (str.down()) { + case "off": + return OFF; + + case "normal": + return NORMAL; + + case "full": + default: + return FULL; + } + } +} + diff --git a/src/engine/db/db-transaction-async-job.vala b/src/engine/db/db-transaction-async-job.vala new file mode 100755 index 00000000..b3fffecb --- /dev/null +++ b/src/engine/db/db-transaction-async-job.vala @@ -0,0 +1,81 @@ +/* Copyright 2012 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. + */ + +private class Geary.Db.TransactionAsyncJob : Object { + private TransactionType type; + private unowned TransactionMethod cb; + private Cancellable cancellable; + private NonblockingEvent completed; + private TransactionOutcome outcome = TransactionOutcome.ROLLBACK; + private Error? caught_err = null; + + protected TransactionAsyncJob(TransactionType type, TransactionMethod cb, Cancellable? cancellable) { + this.type = type; + this.cb = cb; + this.cancellable = cancellable ?? new Cancellable(); + + completed = new NonblockingEvent(cancellable); + } + + public void cancel() { + cancellable.cancel(); + } + + public bool is_cancelled() { + return cancellable.is_cancelled(); + } + + // Called in background thread context + internal void execute(Connection cx) { + // execute transaction + try { + // possible was cancelled during interim of scheduling and execution + if (is_cancelled()) + throw new IOError.CANCELLED("Async transaction cancelled"); + + outcome = cx.exec_transaction(type, cb, cancellable); + } catch (Error err) { + debug("AsyncJob: transaction completed with error: %s", err.message); + caught_err = err; + } + + // notify foreground thread of completion + // because Idle doesn't hold a ref, manually keep this object alive + ref(); + + // NonblockingSemaphore and its brethren are not thread-safe, so need to signal notification + // of completion in the main thread + Idle.add(on_notify_completed); + } + + private bool on_notify_completed() { + try { + completed.notify(); + } catch (Error err) { + if (caught_err != null) { + debug("Unable to notify AsyncTransaction has completed w/ err %s: %s", + caught_err.message, err.message); + } else { + debug("Unable to notify AsyncTransaction has completed w/o err: %s", err.message); + } + } + + // manually unref; do NOT touch "this" once unref() returns, as this object may be freed + unref(); + + return false; + } + + public async TransactionOutcome wait_for_completion_async(Cancellable? cancellable = null) + throws Error { + yield completed.wait_async(cancellable); + if (caught_err != null) + throw caught_err; + + return outcome; + } +} + diff --git a/src/engine/db/db-transaction-outcome.vala b/src/engine/db/db-transaction-outcome.vala new file mode 100644 index 00000000..b5bccdc6 --- /dev/null +++ b/src/engine/db/db-transaction-outcome.vala @@ -0,0 +1,40 @@ +/* Copyright 2012 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. + */ + +public enum Geary.Db.TransactionOutcome { + ROLLBACK = 0, + COMMIT = 1, + + // coarse synonyms + SUCCESS = COMMIT, + FAILURE = ROLLBACK, + DONE = COMMIT; + + public unowned string sql() { + switch (this) { + case COMMIT: + return "COMMIT TRANSACTION"; + + case ROLLBACK: + default: + return "ROLLBACK TRANSACTION"; + } + } + + public string to_string() { + switch (this) { + case ROLLBACK: + return "rollback"; + + case COMMIT: + return "commit"; + + default: + return "(unknown: %d)".printf(this); + } + } +} + diff --git a/src/engine/db/db-transaction-type.vala b/src/engine/db/db-transaction-type.vala new file mode 100755 index 00000000..5f8c373c --- /dev/null +++ b/src/engine/db/db-transaction-type.vala @@ -0,0 +1,48 @@ +/* Copyright 2012 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. + */ + +public enum Geary.Db.TransactionType { + DEFERRED, + IMMEDIATE, + EXCLUSIVE, + + // coarse synonyms + RO = DEFERRED, + RW = IMMEDIATE, + WR = EXCLUSIVE, + WO = EXCLUSIVE; + + public unowned string sql() { + switch (this) { + case IMMEDIATE: + return "BEGIN IMMEDIATE"; + + case EXCLUSIVE: + return "BEGIN EXCLUSIVE"; + + case DEFERRED: + default: + return "BEGIN DEFERRED"; + } + } + + public string to_string() { + switch (this) { + case DEFERRED: + return "DEFERRED"; + + case IMMEDIATE: + return "IMMEDIATE"; + + case EXCLUSIVE: + return "EXCLUSIVE"; + + default: + return "(unknown: %d)".printf(this); + } + } +} + diff --git a/src/engine/db/db-versioned-database.vala b/src/engine/db/db-versioned-database.vala new file mode 100644 index 00000000..c6aeac20 --- /dev/null +++ b/src/engine/db/db-versioned-database.vala @@ -0,0 +1,73 @@ +/* Copyright 2012 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. + */ + +public class Geary.Db.VersionedDatabase : Geary.Db.Database { + public File schema_dir { get; private set; } + + public virtual signal void pre_upgrade(int version) { + } + + public virtual signal void post_upgrade(int version) { + } + + public VersionedDatabase(File db_file, File schema_dir) { + base (db_file); + + this.schema_dir = schema_dir; + } + + protected virtual void notify_pre_upgrade(int version) { + pre_upgrade(version); + } + + protected virtual void notify_post_upgrade(int version) { + post_upgrade(version); + } + + // TODO: Initialize database from version-001.sql and upgrade with version-nnn.sql + public override void open(DatabaseFlags flags, PrepareConnection? prepare_cb, + Cancellable? cancellable = null) throws Error { + base.open(flags, prepare_cb, cancellable); + + // get Connection for upgrade activity + Connection cx = open_connection(cancellable); + + int db_version = cx.get_user_version_number(); + debug("VersionedDatabase.upgrade: current database version %d", db_version); + + // Initialize new database to version 1 (note the preincrement in the loop below) + if (db_version < 0) + db_version = 0; + + // Go through all the version scripts in the schema directory and apply each of them. + for (;;) { + File upgrade_script = schema_dir.get_child("version-%03d.sql".printf(++db_version)); + if (!upgrade_script.query_exists(cancellable)) + break; + + notify_pre_upgrade(db_version); + + check_cancelled("VersionedDatabase.open", cancellable); + + try { + debug("Upgrading database to to version %d with %s", db_version, upgrade_script.get_path()); + cx.exec_transaction(TransactionType.EXCLUSIVE, (cx) => { + cx.exec_file(upgrade_script, cancellable); + cx.set_user_version_number(db_version); + + return TransactionOutcome.COMMIT; + }, cancellable); + } catch (Error err) { + warning("Error upgrading database to version %d: %s", db_version, err.message); + + throw err; + } + + notify_post_upgrade(db_version); + } + } +} + diff --git a/src/engine/db/db.vala b/src/engine/db/db.vala new file mode 100644 index 00000000..e810ca20 --- /dev/null +++ b/src/engine/db/db.vala @@ -0,0 +1,147 @@ +/* Copyright 2012 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. + */ + +/** + * Geary.Db is a simple wrapper around SQLite to make it more GObject-ish and easier to code in + * Vala. It also uses threads and some concurrency features of SQLite to allow for asynchronous + * access to the database. + * + * There is no attempt here to hide or genericize the backing database library; this is designed with + * SQLite in mind. As such, many of the calls are merely direct front-ends to the underlying + * SQLite call. + * + * The design of the classes and interfaces owes a debt to SQLHeavy (http://code.google.com/p/sqlheavy/). + */ + +namespace Geary.Db { + +public const int64 INVALID_ROWID = -1; + +[Flags] +public enum DatabaseFlags { + NONE = 0, + CREATE_DIRECTORY, + CREATE_FILE, + READ_ONLY +} + +public enum ResetScope { + SAVE_BINDINGS, + CLEAR_BINDINGS +} + +/* + * PrepareConnection is called from Database when a Connection is created. Database may pool + * Connections, especially for asynchronous queries, so this is only called when a new + * Connection is created and not when its reused. + * + * PrepareConnection may be used as an opportunity to modify or configure the Connection. + * This callback is called prior to the Connection being used, either internally or handed off to + * a caller for normal use. + * + * This callback may be called in the context of a background thread. + */ +public delegate void PrepareConnection(Connection cx, bool master) throws Error; + +/** + * See Connection.exec_transaction() for more information on how this delegate is used. + */ +public delegate TransactionOutcome TransactionMethod(Connection cx, Cancellable? cancellable) throws Error; + +/** + * See http://www.sqlite.org/c3ref/threadsafe.html + */ +public bool threadsafe() { + return Sqlite.threadsafe() != 0; +} + +/** + * See http://www.sqlite.org/c3ref/libversion.html + */ +public unowned string sqlite_version() { + return Sqlite.libversion(); +} + +/** + * See http://www.sqlite.org/c3ref/libversion.html + */ +public int sqlite_version_number() { + return Sqlite.libversion_number(); +} + +private void check_cancelled(string? method, Cancellable? cancellable) throws IOError { + if (cancellable != null && cancellable.is_cancelled()) + throw new IOError.CANCELLED("%s cancelled", !String.is_empty(method) ? method : "Operation"); +} + +// Returns result if exception is not thrown +private int throw_on_error(Context ctx, string? method, int result, string? raw = null) throws DatabaseError { + // fast-fail + switch (result) { + case Sqlite.OK: + case Sqlite.DONE: + case Sqlite.ROW: + return result; + } + + string location = !String.is_empty(method) ? "(%s) ".printf(method) : ""; + string errmsg = (ctx.get_connection() != null) ? " - %s".printf(ctx.get_connection().db.errmsg()) : ""; + string sql; + if (ctx.get_statement() != null) + sql = " (%s)".printf(ctx.get_statement().sql); + else if (!String.is_empty(raw)) + sql = " (%s)".printf(raw); + else + sql = ""; + + string msg = "%s[err=%d]%s%s".printf(location, result, errmsg, sql); + + switch (result) { + case Sqlite.BUSY: + throw new DatabaseError.BUSY(msg); + + case Sqlite.PERM: + case Sqlite.READONLY: + case Sqlite.IOERR: + case Sqlite.CORRUPT: + case Sqlite.CANTOPEN: + case Sqlite.NOLFS: + case Sqlite.AUTH: + case Sqlite.FORMAT: + case Sqlite.NOTADB: + throw new DatabaseError.BACKING(msg); + + case Sqlite.NOMEM: + throw new DatabaseError.MEMORY(msg); + + case Sqlite.ABORT: + case Sqlite.LOCKED: + throw new DatabaseError.ABORT(msg); + + case Sqlite.INTERRUPT: + throw new DatabaseError.INTERRUPT(msg); + + case Sqlite.FULL: + case Sqlite.EMPTY: + case Sqlite.TOOBIG: + case Sqlite.CONSTRAINT: + case Sqlite.RANGE: + throw new DatabaseError.LIMITS(msg); + + case Sqlite.SCHEMA: + case Sqlite.MISMATCH: + throw new DatabaseError.TYPESPEC(msg); + + case Sqlite.ERROR: + case Sqlite.INTERNAL: + case Sqlite.MISUSE: + default: + throw new DatabaseError.GENERAL(msg); + } +} + +} + diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala new file mode 100644 index 00000000..3c203a59 --- /dev/null +++ b/src/engine/imap-db/imap-db-account.vala @@ -0,0 +1,456 @@ +/* Copyright 2011-2012 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. + */ + +private class Geary.ImapDB.Account : Object { + private const int BUSY_TIMEOUT_MSEC = 1000; + + private class FolderReference : Geary.SmartReference { + public Geary.FolderPath path; + + public FolderReference(ImapDB.Folder folder, Geary.FolderPath path) { + base (folder); + + this.path = path; + } + } + + // Only available when the Account is opened + public SmtpOutboxFolder? outbox { get; private set; default = null; } + + private string name; + private AccountSettings settings; + private ImapDB.Database? db = null; + private Gee.HashMap folder_refs = + new Gee.HashMap(Hashable.hash_func, Equalable.equal_func); + + public Account(Geary.AccountSettings settings) { + this.settings = settings; + + name = "IMAP database account for %s".printf(settings.credentials.user); + } + + private void check_open() throws Error { + if (db == null) + throw new EngineError.OPEN_REQUIRED("Database not open"); + } + + public async void open_async(File user_data_dir, File schema_dir, Cancellable? cancellable) + throws Error { + if (db != null) + throw new EngineError.ALREADY_OPEN("IMAP database already open"); + + db = new ImapDB.Database(user_data_dir, schema_dir); + db.pre_upgrade.connect(on_pre_upgrade); + db.post_upgrade.connect(on_post_upgrade); + + try { + db.open(Db.DatabaseFlags.CREATE_DIRECTORY | Db.DatabaseFlags.CREATE_FILE, + on_prepare_database_connection, cancellable); + } catch (Error err) { + warning("Unable to open database: %s", err.message); + + // close database before exiting + db = null; + + throw err; + } + + // ImapDB.Account holds the Outbox, which is tied to the database it maintains + outbox = new SmtpOutboxFolder(db, settings); + + // Need to clear duplicate folders due to old bug that caused multiple folders to be + // created in the database ... benign due to other logic, but want to prevent this from + // happening if possible + clear_duplicate_folders(); + } + + public async void close_async(Cancellable? cancellable) throws Error { + if (db == null) + return; + + // close and always drop reference + try { + db.close(cancellable); + } finally { + db = null; + } + + outbox = null; + } + + private void on_prepare_database_connection(Db.Connection cx) throws Error { + cx.set_busy_timeout_msec(BUSY_TIMEOUT_MSEC); + cx.set_foreign_keys(true); + cx.set_recursive_triggers(true); + cx.set_synchronous(Db.SynchronousMode.OFF); + } + + public async void clone_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null) + throws Error { + check_open(); + + Geary.Imap.FolderProperties? properties = imap_folder.get_properties(); + + // properties *must* be available to perform a clone + assert(properties != null); + + Geary.FolderPath path = imap_folder.get_path(); + + yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => { + // get the parent of this folder, creating parents if necessary + int64 parent_id; + if (!do_fetch_parent_id(cx, path, true, out parent_id, cancellable)) + return Db.TransactionOutcome.ROLLBACK; + + // create the folder object + Db.Statement stmt = cx.prepare( + "INSERT INTO FolderTable (name, parent_id, last_seen_total, uid_validity, uid_next, attributes) " + + "VALUES (?, ?, ?, ?, ?)"); + stmt.bind_string(0, path.basename); + stmt.bind_rowid(1, parent_id); + stmt.bind_int(2, properties.messages); + stmt.bind_int64(3, properties.uid_validity.value); + stmt.bind_int64(4, properties.uid_next.value); + stmt.bind_string(5, properties.attrs.serialize()); + + stmt.exec(cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + } + + public async void update_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null) + throws Error { + check_open(); + + Geary.Imap.FolderProperties? properties = (Geary.Imap.FolderProperties?) imap_folder.get_properties(); + + // properties *must* be available + assert(properties != null); + + Geary.FolderPath path = imap_folder.get_path(); + + yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => { + int64 parent_id; + if (!do_fetch_parent_id(cx, path, true, out parent_id, cancellable)) + return Db.TransactionOutcome.ROLLBACK; + + Db.Statement stmt = cx.prepare( + "UPDATE FolderTable SET last_seen_total=?, uid_validity=?, uid_next=?, attributes=? " + + "WHERE parent_id=? AND name=?"); + stmt.bind_int(0, properties.messages); + stmt.bind_int64(1, properties.uid_validity.value); + stmt.bind_int64(2, properties.uid_next.value); + stmt.bind_string(3, properties.attrs.serialize()); + stmt.bind_rowid(4, parent_id); + stmt.bind_string(4, path.basename); + + stmt.exec(); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + + // update properties in the local folder + ImapDB.Folder? db_folder = get_local_folder(path); + if (db_folder != null) + db_folder.set_properties(properties); + } + + public async Gee.Collection list_folders_async(Geary.FolderPath? parent, + Cancellable? cancellable = null) throws Error { + check_open(); + + // TODO: A better solution here would be to only pull the FolderProperties if the Folder + // object itself doesn't already exist + Gee.HashMap id_map = new Gee.HashMap< + Geary.FolderPath, int64?>(Hashable.hash_func, Equalable.equal_func); + Gee.HashMap prop_map = new Gee.HashMap< + Geary.FolderPath, Geary.Imap.FolderProperties>(Hashable.hash_func, Equalable.equal_func); + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + int64 parent_id = Db.INVALID_ROWID; + if (parent != null) { + if (!do_fetch_folder_id(cx, parent, false, out parent_id, cancellable)) + return Db.TransactionOutcome.ROLLBACK; + + assert(parent_id != Db.INVALID_ROWID); + } + + Db.Statement stmt; + if (parent_id != Db.INVALID_ROWID) { + stmt = cx.prepare( + "SELECT id, name, last_seen_total, uid_validity, uid_next, attributes " + + "FROM FolderTable WHERE parent_id=?"); + stmt.bind_rowid(0, parent_id); + } else { + stmt = cx.prepare( + "SELECT id, name, last_seen_total, uid_validity, uid_next, attributes " + + "FROM FolderTable WHERE parent_id IS NULL"); + } + + Db.Result result = stmt.exec(cancellable); + while (!result.finished) { + string basename = result.string_for("name"); + Geary.FolderPath path = (parent != null) + ? parent.get_child(basename) + : new Geary.FolderRoot(basename, "/", Geary.Imap.Folder.CASE_SENSITIVE); + + Geary.Imap.FolderProperties properties = new Geary.Imap.FolderProperties( + result.int_for("last_seen_total"), 0, 0, + new Imap.UIDValidity(result.int64_for("uid_validity")), + new Imap.UID(result.int64_for("uid_next")), + Geary.Imap.MailboxAttributes.deserialize(result.string_for("attributes"))); + + id_map.set(path, result.rowid_for("id")); + prop_map.set(path, properties); + + result.next(cancellable); + } + + return Db.TransactionOutcome.DONE; + }, cancellable); + + assert(id_map.size == prop_map.size); + + if (id_map.size == 0) { + throw new EngineError.NOT_FOUND("No local folders in %s", + (parent != null) ? parent.get_fullpath() : "root"); + } + + Gee.Collection folders = new Gee.ArrayList(); + foreach (Geary.FolderPath path in id_map.keys) { + Geary.ImapDB.Folder? folder = get_local_folder(path); + if (folder == null) + folder = create_local_folder(path, id_map.get(path), prop_map.get(path)); + + folders.add(folder); + } + + return folders; + } + + public async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null) + throws Error { + check_open(); + + bool exists = false; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + try { + int64 folder_id; + if (do_fetch_folder_id(cx, path, false, out folder_id, cancellable)) + exists = (folder_id != Db.INVALID_ROWID); + } catch (EngineError err) { + // treat NOT_FOUND as non-exceptional situation + if (!(err is EngineError.NOT_FOUND)) + throw err; + } + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return exists; + } + + public async Geary.ImapDB.Folder fetch_folder_async(Geary.FolderPath path, + Cancellable? cancellable = null) throws Error { + check_open(); + + // check references table first + Geary.ImapDB.Folder? folder = get_local_folder(path); + if (folder != null) + return folder; + + int64 folder_id = Db.INVALID_ROWID; + Imap.FolderProperties? properties = null; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + if (!do_fetch_folder_id(cx, path, false, out folder_id, cancellable)) + return Db.TransactionOutcome.DONE; + + assert(folder_id != Db.INVALID_ROWID); + + Db.Statement stmt = cx.prepare( + "SELECT last_seen_total, uid_validity, uid_next, attributes FROM FolderTable WHERE id=?"); + stmt.bind_rowid(0, folder_id); + + Db.Result results = stmt.exec(cancellable); + if (!results.finished) { + properties = new Imap.FolderProperties(results.int_for("last_seen_total"), 0, 0, + new Imap.UIDValidity(results.int64_for("uid_validity")), + new Imap.UID(results.int64_for("uid_next")), + Geary.Imap.MailboxAttributes.deserialize(results.string_for("attributes"))); + } + + return Db.TransactionOutcome.DONE; + }, cancellable); + + if (folder_id == Db.INVALID_ROWID) + throw new EngineError.NOT_FOUND("%s not found in local database", path.to_string()); + + return create_local_folder(path, folder_id, properties); + } + + private Geary.ImapDB.Folder? get_local_folder(Geary.FolderPath path) { + FolderReference? folder_ref = folder_refs.get(path); + + return (folder_ref != null) ? (Geary.ImapDB.Folder) folder_ref.get_reference() : null; + } + + private Geary.ImapDB.Folder create_local_folder(Geary.FolderPath path, int64 folder_id, + Imap.FolderProperties? properties) throws Error { + // return current if already created + ImapDB.Folder? folder = get_local_folder(path); + if (folder != null) { + // update properties if available + if (properties != null) + folder.set_properties(properties); + + return folder; + } + + // create folder + folder = new Geary.ImapDB.Folder(db, path, folder_id, properties); + + // build a reference to it + FolderReference folder_ref = new FolderReference(folder, path); + folder_ref.reference_broken.connect(on_folder_reference_broken); + + // add to the references table + folder_refs.set(folder_ref.path, folder_ref); + + return folder; + } + + private void on_folder_reference_broken(Geary.SmartReference reference) { + FolderReference folder_ref = (FolderReference) reference; + + // drop from folder references table, all cleaned up + folder_refs.unset(folder_ref.path); + } + + private void on_pre_upgrade(int version){ + // TODO Add per-version data massaging. + } + + private void on_post_upgrade(int version) { + // TODO Add per-version data massaging. + } + + private void clear_duplicate_folders() { + int count = 0; + + try { + // Find all folders with duplicate names + Db.Result result = db.query("SELECT id, name FROM FolderTable WHERE name IN " + + "(SELECT name FROM FolderTable GROUP BY name HAVING (COUNT(name) > 1))"); + while (!result.finished) { + int64 id = result.int64_at(0); + + // see if any folders have this folder as a parent OR if there are messages associated + // with this folder + Db.Statement child_stmt = db.prepare("SELECT id FROM FolderTable WHERE parent_id=?"); + child_stmt.bind_int64(0, id); + Db.Result child_result = child_stmt.exec(); + + Db.Statement message_stmt = db.prepare( + "SELECT id FROM MessageLocationTable WHERE folder_id=?"); + message_stmt.bind_int64(0, id); + Db.Result message_result = message_stmt.exec(); + + if (child_result.finished && message_result.finished) { + // no children, delete it + Db.Statement delete_stmt = db.prepare("DELETE FROM FolderTable WHERE id=?"); + delete_stmt.bind_int64(0, id); + + delete_stmt.exec(); + count++; + } + + result.next(); + } + } catch (Error err) { + debug("Error attempting to clear duplicate folders from account: %s", err.message); + breakpoint(); + } + + if (count > 0) + debug("Deleted %d duplicate folders", count); + } + + // + // Transaction helper methods + // + + private bool do_fetch_folder_id(Db.Connection cx, Geary.FolderPath path, bool create, out int64 folder_id, + Cancellable? cancellable) throws Error { + check_open(); + + int length = path.get_path_length(); + if (length < 0) + throw new EngineError.BAD_PARAMETERS("Invalid path %s", path.to_string()); + + folder_id = Db.INVALID_ROWID; + int64 parent_id = Db.INVALID_ROWID; + + // walk the folder tree to the final node (which is at length - 1 - 1) + for (int ctr = 0; ctr < length; ctr++) { + string basename = path.get_folder_at(ctr).basename; + + Db.Statement stmt; + if (parent_id != Db.INVALID_ROWID) { + stmt = cx.prepare("SELECT id FROM FolderTable WHERE parent_id=? AND name=?"); + stmt.bind_rowid(0, parent_id); + stmt.bind_string(1, basename); + } else { + stmt = cx.prepare("SELECT id FROM FolderTable WHERE parent_id IS NULL AND name=?"); + stmt.bind_string(0, basename); + } + + int64 id = Db.INVALID_ROWID; + + Db.Result result = stmt.exec(cancellable); + if (!result.finished) { + id = result.rowid_at(0); + } else if (!create) { + return false; + } else { + // not found, create it + Db.Statement create_stmt = cx.prepare( + "INSERT INTO FolderTable (name, parent_id) VALUES (?, ?)"); + create_stmt.bind_string(0, basename); + create_stmt.bind_rowid(1, parent_id); + + id = create_stmt.exec_insert(cancellable); + } + + // watch for path loops, real bad if it happens ... could be more thorough here, but at + // least one level of checking is better than none + if (id == parent_id) { + warning("Loop found in database: parent of %lld is %lld in FolderTable", + parent_id, id); + + return false; + } + + parent_id = id; + } + + // parent_id is now the folder being searched for + folder_id = parent_id; + + return (folder_id != Db.INVALID_ROWID); + } + + private bool do_fetch_parent_id(Db.Connection cx, Geary.FolderPath path, bool create, out int64 parent_id, + Cancellable? cancellable = null) throws Error { + if (path.is_root()) { + parent_id = Db.INVALID_ROWID; + + return false; + } + + return do_fetch_folder_id(cx, path.get_parent(), create, out parent_id, cancellable); + } +} + diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala new file mode 100644 index 00000000..b63050f5 --- /dev/null +++ b/src/engine/imap-db/imap-db-database.vala @@ -0,0 +1,14 @@ +/* Copyright 2012 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. + */ + +private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase { + private const string DB_FILENAME = "geary.db"; + + public Database(File db_dir, File schema_dir) { + base (db_dir.get_child(DB_FILENAME), schema_dir); + } +} + diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala new file mode 100644 index 00000000..3b13eece --- /dev/null +++ b/src/engine/imap-db/imap-db-folder.vala @@ -0,0 +1,1203 @@ +/* Copyright 2011-2012 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. + */ + +private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics { + public const Geary.Email.Field REQUIRED_FOR_DUPLICATE_DETECTION = Geary.Email.Field.PROPERTIES; + + [Flags] + public enum ListFlags { + NONE = 0, + PARTIAL_OK, + INCLUDE_MARKED_FOR_REMOVE, + EXCLUDING_ID; + + public bool is_all_set(ListFlags flags) { + return (this & flags) == flags; + } + + public bool is_any_set(ListFlags flags) { + return (this & flags) != 0; + } + + public bool include_marked_for_remove() { + return is_all_set(INCLUDE_MARKED_FOR_REMOVE); + } + } + + private class LocationIdentifier { + public int64 message_id; + public int position; + public int64 ordering; + public Geary.EmailIdentifier email_id; + + // If EmailIdentifier has already been built, it can be supplied rather then auto-created + // by LocationIdentifier + public LocationIdentifier(int64 message_id, int position, int64 ordering, + Geary.EmailIdentifier? email_id) { + assert(position >= 1); + + this.message_id = message_id; + this.position = position; + this.ordering = ordering; + this.email_id = email_id ?? new Imap.EmailIdentifier(new Imap.UID(ordering)); + + // verify that the EmailIdentifier and ordering are pointing to the same thing + assert(this.email_id.ordering == this.ordering); + } + } + + public bool opened { get; private set; default = false; } + + protected int manual_ref_count { get; protected set; } + + private ImapDB.Database db; + private Geary.FolderPath path; + private int64 folder_id; + private Geary.Imap.FolderProperties? properties; + private Gee.HashSet marked_removed = new Gee.HashSet( + Hashable.hash_func, Equalable.equal_func); + + internal Folder(ImapDB.Database db, Geary.FolderPath path, int64 folder_id, + Geary.Imap.FolderProperties? properties) { + this.db = db; + this.path = path; + this.folder_id = folder_id; + this.properties = properties; + } + + private void check_open() throws Error { + if (!opened) + throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); + } + + public Geary.FolderPath get_path() { + return path; + } + + public Geary.Imap.FolderProperties? get_properties() { + // TODO: TBD: alteration/updated signals for folders + return properties; + } + + internal void set_properties(Geary.Imap.FolderProperties? properties) { + this.properties = properties; + } + + 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; + + lock (marked_removed) + marked_removed.clear(); + } + + public async void close_async(Cancellable? cancellable = null) throws Error { + opened = false; + + // anything marked as removed is dropped rather than actually deleted from the database; + // folder synchronization the next time the folder is opened will take care of anything + // not properly sync'd + lock (marked_removed) { + marked_removed.clear(); + } + + // TODO: Wait for all I/O to complete before exiting + } + + // Returns true if the EmailIdentifier was marked before being removed + private bool unmark_removed(Geary.EmailIdentifier id) { + lock (marked_removed) { + return marked_removed.remove(id); + } + } + + private void mark_unmark_removed(Gee.Collection ids, bool mark) { + lock (marked_removed) { + if (mark) + marked_removed.add_all(ids); + else + marked_removed.remove_all(ids); + } + } + + private bool is_marked_removed(Geary.EmailIdentifier id) { + lock (marked_removed) { + return marked_removed.contains(id); + } + } + + private int get_marked_removed_count() { + lock (marked_removed) { + return marked_removed.size; + } + } + + private int get_marked_removed_count_lte(Geary.EmailIdentifier id) { + int count = 0; + lock (marked_removed) { + foreach (Geary.EmailIdentifier marked in marked_removed) { + if (marked.ordering <= id.ordering) + count++; + } + } + + return count; + } + + public async int get_email_count_async(ListFlags flags, Cancellable? cancellable) throws Error { + check_open(); + + 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; + } + + public async int get_id_position_async(Geary.EmailIdentifier id, ListFlags flags, + Cancellable? cancellable) throws Error { + check_open(); + + int position = -1; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + position = do_get_message_position(cx, id, flags, cancellable); + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return position; + } + + // Returns true if created, false if merged. + public async bool create_or_merge_email_async(Geary.Email email, Cancellable? cancellable = null) + throws Error { + check_open(); + + bool created = false; + yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => { + created = do_create_or_merge_email(cx, email, cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + + return created; + } + + public async Gee.List? list_email_async(int low, int count, + Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error { + check_open(); + + Gee.List? list = null; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => { + Geary.Folder.normalize_span_specifiers(ref low, ref count, + do_get_email_count(cx, flags, cancellable)); + if (count == 0) + return Db.TransactionOutcome.SUCCESS; + + Db.Statement stmt = cx.prepare( + "SELECT message_id, ordering FROM MessageLocationTable WHERE folder_id=? " + + "ORDER BY ordering LIMIT ? OFFSET ?"); + stmt.bind_rowid(0, folder_id); + stmt.bind_int(1, count); + stmt.bind_int(2, low - 1); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return Db.TransactionOutcome.SUCCESS; + + Gee.List ids = new Gee.ArrayList(); + int position = low; + do { + LocationIdentifier location = new LocationIdentifier(results.rowid_at(0), position++, + results.int64_at(1), null); + if (!flags.include_marked_for_remove() && is_marked_removed(location.email_id)) + continue; + + ids.add(location); + } while (results.next(cancellable)); + + list = do_list_email(cx, ids, required_fields, flags, cancellable); + + return Db.TransactionOutcome.SUCCESS; + }, cancellable); + + return list; + } + + public async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, + int count, Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable = null) + throws Error { + check_open(); + + if (count == 0 || count == 1) { + try { + Geary.Email email = yield fetch_email_async(initial_id, required_fields, flags, + cancellable); + + Gee.List singleton = new Gee.ArrayList(); + singleton.add(email); + + return singleton; + } catch (EngineError engine_err) { + // list_email variants don't return NOT_FOUND or INCOMPLETE_MESSAGE + if ((engine_err is EngineError.NOT_FOUND) || (engine_err is EngineError.INCOMPLETE_MESSAGE)) + return null; + + throw engine_err; + } + } + + Geary.Imap.UID uid = ((Geary.Imap.EmailIdentifier) initial_id).uid; + bool excluding_id = flags.is_all_set(ListFlags.EXCLUDING_ID); + + int64 low, high; + if (count < 0) { + high = excluding_id ? uid.value - 1 : uid.value; + low = (count != int.MIN) ? (high + count).clamp(1, uint32.MAX) : -1; + } else { + // count > 1 + low = excluding_id ? uid.value + 1 : uid.value; + high = (count != int.MAX) ? (low + count).clamp(1, uint32.MAX) : -1; + } + + Gee.List? list = null; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + Db.Statement stmt; + if (high != -1 && low != -1) { + stmt = cx.prepare( + "SELECT message_id, ordering FROM MessageLocationTable WHERE folder_id=? " + + "AND ordering >= ? AND ordering <= ? ORDER BY ordering ASC"); + stmt.bind_int64(0, folder_id); + stmt.bind_int64(1, low); + stmt.bind_int64(2, high); + } else if (high == -1) { + stmt = cx.prepare( + "SELECT message_id, ordering FROM MessageLocationTable WHERE folder_id=? " + + "AND ordering >= ? ORDER BY ordering ASC"); + stmt.bind_int64(0, folder_id); + stmt.bind_int64(1, low); + } else { + assert(low == -1); + + stmt = cx.prepare( + "SELECT message_id, ordering FROM MessageLocationTable WHERE folder_id=? " + + "AND ordering <= ? ORDER BY ordering ASC"); + stmt.bind_int64(0, folder_id); + stmt.bind_int64(1, high); + } + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return Db.TransactionOutcome.SUCCESS; + + Gee.List ids = new Gee.ArrayList(); + int position = -1; + do { + int64 ordering = results.int64_at(1); + Geary.EmailIdentifier email_id = new Imap.EmailIdentifier(new Imap.UID(ordering)); + + // get position of first message and roll from there + if (position == -1) { + position = do_get_message_position(cx, email_id, flags, cancellable); + assert(position >= 1); + } + + LocationIdentifier location = new LocationIdentifier(results.rowid_at(0), position++, + ordering, email_id); + if (!flags.include_marked_for_remove() && is_marked_removed(location.email_id)) { + // don't count this in the positional addressing + position--; + + continue; + } + + ids.add(location); + } while (results.next(cancellable)); + + list = do_list_email(cx, ids, required_fields, flags, cancellable); + + return Db.TransactionOutcome.SUCCESS; + }, cancellable); + + return list; + } + + public async Geary.Email fetch_email_async(Geary.EmailIdentifier id, + Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error { + check_open(); + + Geary.Email? email = null; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + // get the message and its position + int64 message_id = do_find_message(cx, id, flags, cancellable); + if (message_id == Db.INVALID_ROWID) + return Db.TransactionOutcome.DONE; + + int position = do_get_message_position(cx, id, flags, cancellable); + if (position < 1) + return Db.TransactionOutcome.DONE; + + LocationIdentifier location = new LocationIdentifier(message_id, position, id.ordering, + null); + if (!flags.include_marked_for_remove() && is_marked_removed(location.email_id)) + 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; + } + + public async Geary.Imap.UID? get_earliest_uid_async(Cancellable? cancellable = null) throws Error { + return yield get_uid_extremes_async(true, cancellable); + } + + public async Geary.Imap.UID? get_latest_uid_async(Cancellable? cancellable = null) throws Error { + return yield get_uid_extremes_async(false, cancellable); + } + + private async Geary.Imap.UID? get_uid_extremes_async(bool earliest, Cancellable? cancellable) + throws Error { + check_open(); + + int64 ordering = Imap.UID.INVALID; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + Db.Statement stmt; + if (earliest) + stmt = cx.prepare("SELECT MIN(ordering) FROM MessageLocationTable WHERE folder_id=?"); + else + stmt = cx.prepare("SELECT MAX(ordering) FROM MessageLocationTable WHERE folder_id=?"); + stmt.bind_rowid(0, folder_id); + + Db.Result results = stmt.exec(cancellable); + if (!results.finished) + ordering = results.int64_at(0); + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return Imap.UID.is_value_valid(ordering) ? new Imap.UID(ordering) : null; + } + + public async void remove_email_async(Gee.Collection ids, + Cancellable? cancellable = null) throws Error { + check_open(); + + // 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.WO, (cx) => { + // prepare Statement and invariants + Db.Statement stmt = cx.prepare( + "DELETE FROM MessageLocationTable WHERE folder_id=? AND ordering=?"); + stmt.bind_rowid(0, folder_id); + + foreach (Geary.EmailIdentifier id in ids) { + stmt.reset(Db.ResetScope.SAVE_BINDINGS); + stmt.bind_int64(1, id.ordering); + + stmt.exec(cancellable); + } + + // Remove any that may have been marked removed + mark_unmark_removed(ids, false); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + } + + public async void mark_email_async(Gee.Collection to_mark, + Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, Cancellable? cancellable) + throws Error { + check_open(); + + yield db.exec_transaction_async(Db.TransactionType.RW, (cx, cancellable) => { + // fetch flags for each email + Gee.Map? map = do_get_email_flags(cx, to_mark, + cancellable); + if (map == null) + return Db.TransactionOutcome.COMMIT; + + // update flags according to arguments + foreach (Geary.EmailIdentifier id in map.keys) { + Geary.Imap.EmailFlags flags = ((Geary.Imap.EmailFlags) map.get(id)); + + if (flags_to_add != null) { + foreach (Geary.EmailFlag flag in flags_to_add.get_all()) + flags.add(flag); + } + + if (flags_to_remove != null) { + foreach (Geary.EmailFlag flag in flags_to_remove.get_all()) + flags.remove(flag); + } + } + + // write them all back out + do_set_email_flags(cx, map, cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + } + + public async Gee.Map? get_email_flags_async( + Gee.Collection ids, Cancellable? cancellable) throws Error { + check_open(); + + Gee.Map? 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 map, + Cancellable? cancellable) throws Error { + check_open(); + + yield db.exec_transaction_async(Db.TransactionType.RW, (cx, cancellable) => { + do_set_email_flags(cx, map, cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + } + + public async bool is_email_present_async(Geary.EmailIdentifier id, out Geary.Email.Field available_fields, + Cancellable? cancellable = null) throws Error { + check_open(); + + Geary.Email.Field internal_available_fields = Geary.Email.Field.NONE; + bool is_present = false; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => { + int64 message_id = do_find_message(cx, id, ListFlags.NONE, cancellable); + if (message_id == Db.INVALID_ROWID) + return Db.TransactionOutcome.DONE; + + is_present = do_fetch_email_fields(cx, message_id, out internal_available_fields, + cancellable); + + return Db.TransactionOutcome.DONE; + }, cancellable); + + available_fields = internal_available_fields; + + return is_present; + } + + public async void remove_marked_email_async(Geary.EmailIdentifier id, out bool is_marked, + Cancellable? cancellable) throws Error { + check_open(); + + bool internal_is_marked = false; + yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => { + internal_is_marked = unmark_removed(id); + + do_remove_association_with_folder(cx, id, cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + + is_marked = internal_is_marked; + } + + // 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 remove_marked_email_async() + // to formally remove the messages from the folder. + public async void mark_removed_async(Gee.Collection ids, bool mark_removed, + Cancellable? cancellable) throws Error { + check_open(); + + mark_unmark_removed(ids, mark_removed); + } + + public async Gee.Map? list_email_fields_by_id_async( + Gee.Collection ids, Cancellable? cancellable) throws Error { + check_open(); + + if (ids.size == 0) + return null; + + Gee.HashMap map = new Gee.HashMap< + Geary.EmailIdentifier, Geary.Email.Field>(Hashable.hash_func, Equalable.equal_func); + + yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => { + Db.Statement fetch_stmt = cx.prepare( + "SELECT fields FROM MessageTable WHERE id=?"); + + foreach (Geary.EmailIdentifier id in ids) { + int64 message_id = do_find_message(cx, id, ListFlags.NONE, cancellable); + if (message_id == Db.INVALID_ROWID) + continue; + + fetch_stmt.reset(Db.ResetScope.CLEAR_BINDINGS); + fetch_stmt.bind_rowid(0, message_id); + + Db.Result results = fetch_stmt.exec(cancellable); + if (!results.finished) + map.set(id, (Geary.Email.Field) results.int_at(0)); + } + + return Db.TransactionOutcome.SUCCESS; + }, cancellable); + + 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_int64(0, folder_id); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return 0; + + int marked = !flags.include_marked_for_remove() ? get_marked_removed_count() : 0; + + return Numeric.int_floor(results.int_at(0) - marked, 0); + } + + private int64 do_find_message(Db.Connection cx, Geary.EmailIdentifier id, ListFlags flags, + Cancellable? cancellable) throws Error { + if (!flags.include_marked_for_remove() && is_marked_removed(id)) + return Db.INVALID_ROWID; + + Db.Statement stmt = cx.prepare( + "SELECT message_id FROM MessageLocationTable WHERE folder_id=? AND ordering=?"); + stmt.bind_rowid(0, folder_id); + stmt.bind_int64(1, id.ordering); + + Db.Result results = stmt.exec(cancellable); + + return (!results.finished) ? results.rowid_at(0) : Db.INVALID_ROWID; + } + + // Returns -1 if not found + private int do_get_message_position(Db.Connection cx, Geary.EmailIdentifier id, ListFlags flags, + Cancellable? cancellable) throws Error { + if (!flags.include_marked_for_remove() && is_marked_removed(id)) + return -1; + + Db.Statement stmt = cx.prepare( + "SELECT COUNT(*), MAX(ordering) FROM MessageLocationTable WHERE folder_id=? " + + "AND ordering <= ? ORDER BY ordering ASC"); + stmt.bind_rowid(0, folder_id); + stmt.bind_int64(1, id.ordering); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return -1; + + // without the MAX it's possible to overshoot, so the MAX(ordering) *must* match the argument + if (results.int64_at(1) != id.ordering) + return -1; + + // the COUNT represents the 1-based number of rows from the first ordering to this one + if (flags.include_marked_for_remove()) + return results.int_at(0); + + int adjusted = results.int_at(0) - get_marked_removed_count_lte(id); + + return (adjusted >= 1) ? adjusted : -1; + } + + // Returns message_id if duplicate found, associated set to true if message is already associated + // with this folder + private int64 do_search_for_duplicates(Db.Connection cx, Geary.Email email, out bool associated, + Cancellable? cancellable) throws Error { + associated = false; + + // See if it already exists; first by UID (which is only guaranteed to be unique in a folder, + // not account-wide) + int64 message_id = do_find_message(cx, email.id, ListFlags.NONE, cancellable); + if (message_id != Db.INVALID_ROWID) { + associated = true; + + return message_id; + } + + // if fields not present, then no duplicate can reliably be found + if (!email.fields.is_all_set(REQUIRED_FOR_DUPLICATE_DETECTION)) { + debug("Unable to detect duplicates for %s", email.id.to_string()); + return Db.INVALID_ROWID; + } + + // 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.original : null; + long rfc822_size = (imap_properties != null) ? imap_properties.rfc822_size.value : -1; + + if (String.is_empty(internaldate) || rfc822_size < 0) + return Db.INVALID_ROWID; + + // look for duplicate in IMAP message properties + Db.Statement stmt = cx.prepare( + "SELECT message_id FROM MessageTable WHERE internaldate=? AND rfc822_size=?"); + stmt.bind_string(0, internaldate); + stmt.bind_int64(1, rfc822_size); + + Db.Result results = stmt.exec(cancellable); + if (!results.finished) { + message_id = results.rowid_at(0); + if (results.next(cancellable)) { + debug("Warning: multiple messages with the same internaldate (%s) and size (%lu) in %s", + internaldate, rfc822_size, to_string()); + } + + associated = true; + + return message_id; + } + + // no duplicates found + return Db.INVALID_ROWID; + } + + // Note: does NOT check if message is already associated with thie folder + private void do_associate_with_folder(Db.Connection cx, int64 message_id, Geary.Email email, + Cancellable? cancellable) throws Error { + assert(message_id != Db.INVALID_ROWID); + + // insert email at supplied position + Db.Statement stmt = cx.prepare( + "INSERT INTO MessageLocationTable (message_id, folder_id, ordering) VALUES (?, ?, ?)"); + stmt.bind_rowid(0, message_id); + stmt.bind_rowid(1, folder_id); + stmt.bind_int64(2, email.id.ordering); + + stmt.exec(cancellable); + } + + private void do_remove_association_with_folder(Db.Connection cx, Geary.EmailIdentifier id, + Cancellable? cancellable) throws Error { + Db.Statement stmt = cx.prepare( + "DELETE FROM MessageLocationTable WHERE folder_id=? AND ordering=?"); + stmt.bind_rowid(0, folder_id); + stmt.bind_int64(1, id.ordering); + + stmt.exec(cancellable); + } + + private bool do_create_or_merge_email(Db.Connection cx, Geary.Email email, + Cancellable? cancellable) throws Error { + // see if message already present in current folder, if not, search for duplicate throughout + // mailbox + bool associated; + int64 message_id = do_search_for_duplicates(cx, email, out associated, cancellable); + + // if found, merge, and associate if necessary + if (message_id != Db.INVALID_ROWID) { + if (!associated) + do_associate_with_folder(cx, message_id, email, cancellable); + + do_merge_email(cx, message_id, email, cancellable); + + // return false to indicate a merge + return false; + } + + // not found, so create and associate with this folder + MessageRow row = new MessageRow.from_email(email); + + 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, 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(13, row.header); + stmt.bind_string(14, row.body); + stmt.bind_string(15, row.preview); + stmt.bind_string(16, row.email_flags); + stmt.bind_string(17, row.internaldate); + stmt.bind_long(18, row.rfc822_size); + + message_id = stmt.exec_insert(cancellable); + do_associate_with_folder(cx, message_id, email, 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)) + do_save_attachments(cx, message_id, email.get_message().get_attachments(), cancellable); + + return true; + } + + private Gee.List? do_list_email(Db.Connection cx, Gee.List locations, + Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error { + Gee.List emails = new Gee.ArrayList(); + 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. + private static MessageRow do_fetch_message_row(Db.Connection cx, int64 message_id, + Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { + Db.Statement stmt = cx.prepare( + "SELECT %s FROM MessageTable WHERE id=?".printf(fields_to_columns(required_fields))); + stmt.bind_rowid(0, message_id); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + throw new EngineError.NOT_FOUND("No message ID %lld found in database", message_id); + + return new MessageRow.from_result(required_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() && is_marked_removed(location.email_id)) { + 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.position, location.email_id); + + MessageRow row = do_fetch_message_row(cx, location.message_id, required_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.position, location.email_id); + + // Add attachments if available + if (email.fields.fulfills(Geary.Attachment.REQUIRED_FIELDS)) { + Gee.List? attachments = do_list_attachments(cx, location.message_id, + cancellable); + if (attachments != null) + email.add_attachments(attachments); + } + + 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, rfc822_size"; + break; + } + } + + if (append != null) { + builder.append(", "); + builder.append(append); + } + } + + return builder.str; + } + + private Gee.Map? do_get_email_flags(Db.Connection cx, + Gee.Collection ids, Cancellable? cancellable) throws Error { + // prepare Statement for reuse + Db.Statement fetch_stmt = cx.prepare("SELECT flags FROM MessageTable WHERE id=?"); + + Gee.Map map = new Gee.HashMap< + Geary.EmailIdentifier, Geary.EmailFlags>(Hashable.hash_func, Equalable.equal_func); + + foreach (Geary.EmailIdentifier id in ids) { + int64 message_id = do_find_message(cx, id, ListFlags.NONE, cancellable); + if (message_id == Db.INVALID_ROWID) + continue; + + fetch_stmt.reset(Db.ResetScope.CLEAR_BINDINGS); + fetch_stmt.bind_rowid(0, message_id); + + Db.Result results = fetch_stmt.exec(cancellable); + if (results.finished) + continue; + + map.set(id, new Geary.Imap.EmailFlags(Geary.Imap.MessageFlags.deserialize(results.string_at(0)))); + } + + return (map.size > 0) ? map : null; + } + + private void do_set_email_flags(Db.Connection cx, Gee.Map map, + Cancellable? cancellable) throws Error { + Db.Statement update_stmt = cx.prepare( + "UPDATE MessageTable SET flags=? WHERE id=?"); + + foreach (Geary.EmailIdentifier id in map.keys) { + int64 message_id = do_find_message(cx, id, ListFlags.NONE, cancellable); + if (message_id == Db.INVALID_ROWID) + continue; + + Geary.Imap.MessageFlags flags = ((Geary.Imap.EmailFlags) map.get(id)).message_flags; + + update_stmt.reset(Db.ResetScope.CLEAR_BINDINGS); + update_stmt.bind_string(0, flags.serialize()); + update_stmt.bind_rowid(1, 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, Cancellable? cancellable) + throws 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 %lld found in database", row.id); + + // This calculates the fields in the row that are not in the database already + Geary.Email.Field new_fields = (row.fields ^ available_fields) & row.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(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(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)) { + 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=?, rfc822_size=? WHERE id=?"); + stmt.bind_string(0, row.internaldate); + stmt.bind_long(1, row.rfc822_size); + stmt.bind_rowid(2, 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(Db.Connection cx, int64 message_id, Geary.Email email, + Cancellable? cancellable) throws Error { + assert(message_id != Db.INVALID_ROWID); + + // nothing to merge, nothing to do + if (email.fields == Geary.Email.Field.NONE) + return; + + // fetch message from database and merge in this email + MessageRow row = do_fetch_message_row(cx, message_id, email.fields | Attachment.REQUIRED_FIELDS, + cancellable); + Geary.Email.Field db_fields = row.fields; + row.merge_from_remote(email); + + // Build the combined email from the merge, which will be used to save the attachments + Geary.Email combined_email = row.to_email(email.position, email.id); + + // Merge in any fields in the submitted email that aren't already in the database + if ((db_fields & email.fields) != email.fields) { + do_merge_message_row(cx, row, cancellable); + + // Update attachments if not already in the database + if (!db_fields.fulfills(Attachment.REQUIRED_FIELDS) + && combined_email.fields.fulfills(Attachment.REQUIRED_FIELDS)) { + do_save_attachments(cx, message_id, combined_email.get_message().get_attachments(), + cancellable); + } + } + } + + private Gee.List? do_list_attachments(Db.Connection cx, int64 message_id, + Cancellable? cancellable) throws Error { + Db.Statement stmt = cx.prepare( + "SELECT id, filename, mime_type, filesize FROM MessageAttachmentTable WHERE message_id=? " + + "ORDER BY id"); + stmt.bind_rowid(0, message_id); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return null; + + Gee.List list = new Gee.ArrayList(); + do { + list.add(new Geary.Attachment(cx.database.db_file.get_parent(), results.string_at(1), + results.string_at(2), results.int64_at(3), message_id, results.rowid_at(0))); + } while (results.next(cancellable)); + + return list; + } + + private void do_save_attachments(Db.Connection cx, int64 message_id, + Gee.List? attachments, Cancellable? cancellable) throws Error { + // nothing to do if no attachments + if (attachments == null || attachments.size == 0) + return; + + foreach (GMime.Part attachment in attachments) { + string mime_type = attachment.get_content_type().to_string(); + string? filename = attachment.get_filename(); + if (String.is_empty(filename)) { + /// Placeholder filename for attachments with no filename. + filename = _("none"); + } + + // Convert the attachment content into a usable ByteArray. + GMime.DataWrapper attachment_data = attachment.get_content_object(); + ByteArray byte_array = new ByteArray(); + GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array); + stream.set_owner(false); + attachment_data.write_to_stream(stream); + uint filesize = byte_array.len; + + // Insert it into the database. + Db.Statement stmt = cx.prepare( + "INSERT INTO MessageAttachmentTable (message_id, filename, mime_type, filesize) " + + "VALUES (?, ?, ?, ?)"); + stmt.bind_rowid(0, message_id); + stmt.bind_string(1, filename); + stmt.bind_string(2, mime_type); + stmt.bind_uint(3, filesize); + + int64 attachment_id = stmt.exec_insert(cancellable); + + File saved_file = File.new_for_path(Attachment.get_path(db.db_file.get_parent(), message_id, + attachment_id, filename)); + debug("Saving attachment to %s", saved_file.get_path()); + try { + // Create the file where the attachment will be saved and get the output stream. + saved_file.get_parent().make_directory_with_parents(); + FileOutputStream saved_stream = saved_file.create(FileCreateFlags.REPLACE_DESTINATION, + cancellable); + + // Save the data to disk and flush it. + size_t written; + saved_stream.write_all(byte_array.data[0:filesize], out written, cancellable); + saved_stream.flush(cancellable); + } catch (Error error) { + // An error occurred while saving the attachment, so lets remove the attachment from + // the database and delete the file (in case it's partially written) + debug("Failed to save attachment %s: %s", saved_file.get_path(), error.message); + + try { + saved_file.delete(); + } catch (Error delete_error) { + debug("Error attempting to delete partial attachment %s: %s", saved_file.get_path(), + delete_error.message); + } + + try { + Db.Statement remove_stmt = cx.prepare( + "DELETE FROM MessageAttachmentTable WHERE id=?"); + remove_stmt.bind_rowid(0, attachment_id); + + remove_stmt.exec(); + } catch (Error remove_error) { + debug("Error attempting to remove added attachment row for %s: %s", + saved_file.get_path(), remove_error.message); + } + + throw error; + } + } + } + +} + diff --git a/src/engine/imap-db/imap-db-message-row.vala b/src/engine/imap-db/imap-db-message-row.vala new file mode 100644 index 00000000..b8862d28 --- /dev/null +++ b/src/engine/imap-db/imap-db-message-row.vala @@ -0,0 +1,276 @@ +/* Copyright 2011-2012 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. + */ + +public class Geary.ImapDB.MessageRow { + public int64 id { get; set; default = Db.INVALID_ROWID; } + public Geary.Email.Field fields { get; set; default = Geary.Email.Field.NONE; } + + public string? date { get; set; default = null; } + public time_t date_time_t { get; set; default = -1; } + + public string? from { get; set; default = null; } + public string? sender { get; set; default = null; } + public string? reply_to { get; set; default = null; } + + public string? to { get; set; default = null; } + public string? cc { get; set; default = null; } + public string? bcc { get; set; default = null; } + + public string? message_id { get; set; default = null; } + public string? in_reply_to { get; set; default = null; } + public string? references { get; set; default = null; } + + public string? subject { get; set; default = null; } + + public string? header { get; set; default = null; } + + public string? body { get; set; default = null; } + + public string? preview { get; set; default = null; } + + public string? email_flags { get; set; default = null; } + public string? internaldate { get; set; default = null; } + public long rfc822_size { get; set; default = -1; } + + public MessageRow() { + } + + public MessageRow.from_email(Geary.Email email) { + set_from_email(email); + } + + // Converts the current row of the Result object into fields. It's vitally important that + // the columns specified in requested_fields be present in Result. + public MessageRow.from_result(Geary.Email.Field requested_fields, Db.Result results) throws Error { + id = results.int64_for("id"); + + // the available fields are an intersection of what's available in the database and + // what was requested + fields = requested_fields & results.int_for("fields"); + + if (fields.is_all_set(Geary.Email.Field.DATE)) { + date = results.string_for("date_field"); + date_time_t = (time_t) results.int64_for("date_time_t"); + } + + if (fields.is_all_set(Geary.Email.Field.ORIGINATORS)) { + from = results.string_for("from_field"); + sender = results.string_for("sender"); + reply_to = results.string_for("reply_to"); + } + + if (fields.is_all_set(Geary.Email.Field.RECEIVERS)) { + to = results.string_for("to_field"); + cc = results.string_for("cc"); + bcc = results.string_for("bcc"); + } + + if (fields.is_all_set(Geary.Email.Field.REFERENCES)) { + message_id = results.string_for("message_id"); + in_reply_to = results.string_for("in_reply_to"); + references = results.string_for("reference_ids"); + } + + if (fields.is_all_set(Geary.Email.Field.SUBJECT)) + subject = results.string_for("subject"); + + if (fields.is_all_set(Geary.Email.Field.HEADER)) + header = results.string_for("header"); + + if (fields.is_all_set(Geary.Email.Field.BODY)) + body = results.string_for("body"); + + if (fields.is_all_set(Geary.Email.Field.PREVIEW)) + preview = results.string_for("preview"); + + if (fields.is_all_set(Geary.Email.Field.FLAGS)) + email_flags = results.string_for("flags"); + + if (fields.is_all_set(Geary.Email.Field.PROPERTIES)) { + internaldate = results.string_for("internaldate"); + rfc822_size = results.long_for("rfc822_size"); + } + } + + public Geary.Email to_email(int position, Geary.EmailIdentifier id) throws Error { + // Important to set something in the Email object if the field bit is set ... for example, + // if the caller expects to see a DATE field, that field is set in the Email's bitmask, + // even if the Date object is null + Geary.Email email = new Geary.Email(position, id); + + if (fields.is_all_set(Geary.Email.Field.DATE)) + email.set_send_date(!String.is_empty(date) ? new RFC822.Date(date) : null); + + if (fields.is_all_set(Geary.Email.Field.ORIGINATORS)) { + email.set_originators(unflatten_addresses(from), unflatten_addresses(sender), + unflatten_addresses(reply_to)); + } + + if (fields.is_all_set(Geary.Email.Field.RECEIVERS)) { + email.set_receivers(unflatten_addresses(to), unflatten_addresses(cc), + unflatten_addresses(bcc)); + } + + if (fields.is_all_set(Geary.Email.Field.REFERENCES)) { + email.set_full_references( + (message_id != null) ? new RFC822.MessageID(message_id) : null, + (in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null, + (references != null) ? new RFC822.MessageIDList.from_rfc822_string(references) : null); + } + + if (fields.is_all_set(Geary.Email.Field.SUBJECT)) + email.set_message_subject(new RFC822.Subject.decode(subject ?? "")); + + if (fields.is_all_set(Geary.Email.Field.HEADER)) + email.set_message_header(new RFC822.Header(new Geary.Memory.StringBuffer(header ?? ""))); + + if (fields.is_all_set(Geary.Email.Field.BODY)) + email.set_message_body(new RFC822.Text(new Geary.Memory.StringBuffer(body ?? ""))); + + if (fields.is_all_set(Geary.Email.Field.PREVIEW)) + email.set_message_preview(new RFC822.PreviewText(new Geary.Memory.StringBuffer(preview ?? ""))); + + if (fields.is_all_set(Geary.Email.Field.FLAGS)) + email.set_flags(get_generic_email_flags()); + + if (fields.is_all_set(Geary.Email.Field.PROPERTIES)) + email.set_email_properties(get_imap_email_properties()); + + return email; + } + + + public Geary.Imap.EmailProperties? get_imap_email_properties() { + if (internaldate == null || rfc822_size < 0) + return null; + + Imap.InternalDate? constructed = null; + try { + constructed = new Imap.InternalDate(internaldate); + } catch (Error err) { + debug("Unable to construct internaldate object from \"%s\": %s", internaldate, + err.message); + + return null; + } + + return new Geary.Imap.EmailProperties(constructed, new RFC822.Size(rfc822_size)); + } + + public Geary.EmailFlags? get_generic_email_flags() { + return (email_flags != null) + ? new Geary.Imap.EmailFlags(Geary.Imap.MessageFlags.deserialize(email_flags)) + : null; + } + + public void merge_from_remote(Geary.Email email) { + set_from_email(email); + } + + private void set_from_email(Geary.Email email) { + // Although the fields bitmask might indicate various fields are set, they may still be + // null if empty + + if (email.fields.is_all_set(Geary.Email.Field.DATE)) { + date = (email.date != null) ? email.date.original : null; + date_time_t = (email.date != null) ? email.date.as_time_t : -1; + + fields = fields.set(Geary.Email.Field.DATE); + } + + if (email.fields.is_all_set(Geary.Email.Field.ORIGINATORS)) { + from = flatten_addresses(email.from); + sender = flatten_addresses(email.sender); + reply_to = flatten_addresses(email.reply_to); + + fields = fields.set(Geary.Email.Field.ORIGINATORS); + } + + if (email.fields.is_all_set(Geary.Email.Field.RECEIVERS)) { + to = flatten_addresses(email.to); + cc = flatten_addresses(email.cc); + bcc = flatten_addresses(email.bcc); + + fields = fields.set(Geary.Email.Field.RECEIVERS); + } + + if (email.fields.is_all_set(Geary.Email.Field.REFERENCES)) { + message_id = (email.message_id != null) ? email.message_id.value : null; + in_reply_to = (email.in_reply_to != null) ? email.in_reply_to.value : null; + references = (email.references != null) ? email.references.to_rfc822_string() : null; + + fields = fields.set(Geary.Email.Field.REFERENCES); + } + + if (email.fields.is_all_set(Geary.Email.Field.SUBJECT)) { + subject = (email.subject != null) ? email.subject.original : null; + + fields = fields.set(Geary.Email.Field.SUBJECT); + } + + if (email.fields.is_all_set(Geary.Email.Field.HEADER)) { + header = (email.header != null) ? email.header.buffer.to_string() : null; + + fields = fields.set(Geary.Email.Field.HEADER); + } + + if (email.fields.is_all_set(Geary.Email.Field.BODY)) { + body = (email.body != null) ? email.body.buffer.to_string() : null; + + fields = fields.set(Geary.Email.Field.BODY); + } + + if (email.fields.is_all_set(Geary.Email.Field.PREVIEW)) { + preview = (email.preview != null) ? email.preview.buffer.to_string() : null; + + fields = fields.set(Geary.Email.Field.PREVIEW); + } + + if (email.fields.is_all_set(Geary.Email.Field.FLAGS)) { + Geary.Imap.EmailFlags? imap_flags = (Geary.Imap.EmailFlags) email.email_flags; + email_flags = (imap_flags != null) ? imap_flags.message_flags.serialize() : null; + + fields = fields.set(Geary.Email.Field.FLAGS); + } + + if (email.fields.is_all_set(Geary.Email.Field.PROPERTIES)) { + Geary.Imap.EmailProperties? imap_properties = (Geary.Imap.EmailProperties) email.properties; + internaldate = (imap_properties != null) ? imap_properties.internaldate.original : null; + rfc822_size = (imap_properties != null) ? imap_properties.rfc822_size.value : -1; + + fields = fields.set(Geary.Email.Field.PROPERTIES); + } + } + + private static string? flatten_addresses(RFC822.MailboxAddresses? addrs) { + if (addrs == null) + return null; + + switch (addrs.size) { + case 0: + return null; + + case 1: + return addrs[0].to_rfc822_string(); + + default: + StringBuilder builder = new StringBuilder(); + foreach (RFC822.MailboxAddress addr in addrs) { + if (!String.is_empty(builder.str)) + builder.append(", "); + + builder.append(addr.to_rfc822_string()); + } + + return builder.str; + } + } + + private RFC822.MailboxAddresses? unflatten_addresses(string? str) { + return String.is_empty(str) ? null : new RFC822.MailboxAddresses.from_rfc822_string(str); + } +} + diff --git a/src/engine/impl/outbox/smtp-outbox-email-identifier.vala b/src/engine/imap-db/outbox/smtp-outbox-email-identifier.vala similarity index 69% rename from src/engine/impl/outbox/smtp-outbox-email-identifier.vala rename to src/engine/imap-db/outbox/smtp-outbox-email-identifier.vala index 95414292..67c01b4f 100644 --- a/src/engine/impl/outbox/smtp-outbox-email-identifier.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-email-identifier.vala @@ -4,13 +4,13 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -private class Geary.OutboxEmailIdentifier : Geary.EmailIdentifier { - public OutboxEmailIdentifier(int64 ordering) { +private class Geary.SmtpOutboxEmailIdentifier : Geary.EmailIdentifier { + public SmtpOutboxEmailIdentifier(int64 ordering) { base (ordering); } public override bool equals(Geary.Equalable o) { - EmailIdentifier? other = o as EmailIdentifier; + SmtpOutboxEmailIdentifier? other = o as SmtpOutboxEmailIdentifier; if (other == null) return false; diff --git a/src/engine/impl/outbox/smtp-outbox-email-properties.vala b/src/engine/imap-db/outbox/smtp-outbox-email-properties.vala similarity index 62% rename from src/engine/impl/outbox/smtp-outbox-email-properties.vala rename to src/engine/imap-db/outbox/smtp-outbox-email-properties.vala index c50d8aa5..76ff7d61 100644 --- a/src/engine/impl/outbox/smtp-outbox-email-properties.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-email-properties.vala @@ -4,12 +4,12 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -private class Geary.OutboxEmailProperties : Geary.EmailProperties { - public OutboxEmailProperties() { +private class Geary.SmtpOutboxEmailProperties : Geary.EmailProperties { + public SmtpOutboxEmailProperties() { } public override string to_string() { - return "OutboxProperties"; + return "SmtpOutboxProperties"; } } diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder-root.vala b/src/engine/imap-db/outbox/smtp-outbox-folder-root.vala new file mode 100644 index 00000000..d109a2bb --- /dev/null +++ b/src/engine/imap-db/outbox/smtp-outbox-folder-root.vala @@ -0,0 +1,14 @@ +/* Copyright 2012 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. + */ + +private class Geary.SmtpOutboxFolderRoot : Geary.FolderRoot { + public const string MAGIC_BASENAME = "$GearyOutbox$"; + + public SmtpOutboxFolderRoot() { + base(MAGIC_BASENAME, null, false); + } +} + diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala b/src/engine/imap-db/outbox/smtp-outbox-folder.vala new file mode 100644 index 00000000..9825cc19 --- /dev/null +++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala @@ -0,0 +1,547 @@ +/* Copyright 2011-2012 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. + */ + +// Special type of folder that runs an asynchronous send queue. Messages are +// saved to the database, then queued up for sending. +// +// The Outbox table is not currently maintained in its own database, so it must piggy-back +// on the ImapDB.Database. SmtpOutboxFolder assumes the database is opened before it's passed in +// to the constructor -- it does not open or close the database itself and will start using it +// immediately. +private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupportsRemove, + Geary.FolderSupportsCreate { + private class OutboxRow { + public int64 id; + public int position; + public int64 ordering; + public string? message; + public SmtpOutboxEmailIdentifier outbox_id; + + public OutboxRow(int64 id, int position, int64 ordering, string? message) { + assert(position >= 1); + + this.id = id; + this.position = position; + this.ordering = ordering; + this.message = message; + + outbox_id = new SmtpOutboxEmailIdentifier(ordering); + } + } + + private static FolderRoot? path = null; + + private ImapDB.Database db; + private AccountSettings settings; + private Geary.Smtp.ClientSession smtp; + private bool opened = false; + private NonblockingMailbox outbox_queue = new NonblockingMailbox(); + + // Requires the Database from the get-go because it runs a background task that access it + // whether open or not + public SmtpOutboxFolder(ImapDB.Database db, AccountSettings settings) { + this.db = db; + this.settings = settings; + + smtp = new Geary.Smtp.ClientSession(settings.smtp_endpoint); + + do_postman_async.begin(); + } + + // Used solely for debugging, hence "(no subject)" not marked for translation + private static string message_subject(RFC822.Message message) { + return (message.subject != null && !String.is_empty(message.subject.to_string())) + ? message.subject.to_string() : "(no subject)"; + } + + // TODO: Use Cancellable to shut down outbox processor when closing account + private async void do_postman_async() { + debug("Starting outbox postman"); + + // Fill the send queue with existing mail (if any) + try { + Gee.ArrayList list = new Gee.ArrayList(); + yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => { + Db.Statement stmt = cx.prepare( + "SELECT id, ordering, message FROM SmtpOutboxTable ORDER BY ordering"); + + Db.Result results = stmt.exec(cancellable); + int position = 1; + while (!results.finished) { + list.add(new OutboxRow(results.rowid_at(0), position++, results.int64_at(1), + results.string_at(2))); + results.next(cancellable); + } + + return Db.TransactionOutcome.DONE; + }, null); + + if (list.size > 0) { + debug("Priming outbox postman with %d stored messages", list.size); + foreach (OutboxRow row in list) + outbox_queue.send(row); + } + } catch (Error prime_err) { + warning("Error priming outbox: %s", prime_err.message); + } + + // Start the send queue. + for (;;) { + // yield until a message is ready + OutboxRow row; + try { + row = yield outbox_queue.recv_async(); + } catch (Error wait_err) { + debug("Outbox postman queue error: %s", wait_err.message); + + break; + } + + // Convert row into RFC822 message suitable for sending or framing + RFC822.Message message; + try { + message = new RFC822.Message.from_string(row.message); + } catch (RFC822Error msg_err) { + // TODO: This needs to be reported to the user + debug("Outbox postman message error: %s", msg_err.message); + + continue; + } + + // Send the message, but only remove from database once sent + try { + debug("Outbox postman: Sending \"%s\" (ID:%s)...", message_subject(message), + row.outbox_id.to_string()); + yield send_email_async(message, null); + } catch (Error send_err) { + debug("Outbox postman send error, retrying: %s", send_err.message); + + try { + outbox_queue.send(row); + } catch (Error send_err) { + debug("Outbox postman: Unable to re-send row to outbox, dropping on floor: %s", send_err.message); + } + + continue; + } + + // Remove from database ... can't use remove_email_async() because this runs even if + // the outbox is closed as a Geary.Folder. + try { + debug("Outbox postman: Removing \"%s\" (ID:%s) from database", message_subject(message), + row.outbox_id.to_string()); + Gee.ArrayList list = new Gee.ArrayList(); + list.add(row.outbox_id); + yield internal_remove_email_async(list, null); + } catch (Error rm_err) { + debug("Outbox postman: Unable to remove row from database: %s", rm_err.message); + } + } + + debug("Exiting outbox postman"); + } + + public override Geary.FolderPath get_path() { + if (path == null) + path = new SmtpOutboxFolderRoot(); + + return path; + } + + public override Geary.Trillian has_children() { + return Geary.Trillian.FALSE; + } + + public override Geary.SpecialFolderType get_special_folder_type() { + return Geary.SpecialFolderType.OUTBOX; + } + + public override Geary.Folder.OpenState get_open_state() { + return opened ? Geary.Folder.OpenState.LOCAL : Geary.Folder.OpenState.CLOSED; + } + + private void check_open() throws EngineError { + if (!opened) + throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); + } + + public override async void open_async(bool readonly, Cancellable? cancellable = null) + throws Error { + if (opened) + throw new EngineError.ALREADY_OPEN("Folder %s already open", to_string()); + + opened = true; + notify_opened(Geary.Folder.OpenState.LOCAL, yield get_email_count_async(cancellable)); + } + + public override async void close_async(Cancellable? cancellable = null) throws Error { + if (!opened) + return; + + opened = false; + + notify_closed(Geary.Folder.CloseReason.LOCAL_CLOSE); + notify_closed(Geary.Folder.CloseReason.FOLDER_CLOSED); + } + + public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { + check_open(); + + int count = 0; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + count = do_get_email_count(cx, cancellable); + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return count; + } + + // create_email_async() requires the Outbox be open according to contract, but enqueuing emails + // for background delivery can happen at any time, so this is the mechanism to do so. + // email_count is the number of emails in the Outbox after enqueueing the message. + public async SmtpOutboxEmailIdentifier enqueue_email_async(Geary.RFC822.Message rfc822, + Cancellable? cancellable) throws Error { + int email_count = 0; + OutboxRow? row = null; + yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => { + Db.Statement stmt = cx.prepare( + "INSERT INTO SmtpOutboxTable (message, ordering)" + + "VALUES (?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM SmtpOutboxTable))"); + stmt.bind_string(0, rfc822.get_body_rfc822_buffer().to_string()); + + int64 id = stmt.exec_insert(cancellable); + + stmt = cx.prepare("SELECT ordering, message FROM SmtpOutboxTable WHERE id=?"); + stmt.bind_rowid(0, id); + + // This has got to work; Db should throw an exception if the INSERT failed + Db.Result results = stmt.exec(cancellable); + assert(!results.finished); + + int64 ordering = results.int64_at(0); + string message = results.string_at(1); + + int position = do_get_position_by_ordering(cx, ordering, cancellable); + + row = new OutboxRow(id, position, ordering, message); + email_count = do_get_email_count(cx, cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + + // should have thrown an error if this failed + assert(row != null); + + // immediately add to outbox queue for delivery + outbox_queue.send(row); + + // notify only if opened + if (opened) { + Gee.List list = new Gee.ArrayList(); + list.add(row.outbox_id); + + notify_email_appended(list); + notify_email_count_changed(email_count, CountChangeReason.ADDED); + } + + return row.outbox_id; + } + + public virtual async Geary.FolderSupportsCreate.Result create_email_async(Geary.RFC822.Message rfc822, + Cancellable? cancellable = null) throws Error { + check_open(); + + yield enqueue_email_async(rfc822, cancellable); + + return FolderSupportsCreate.Result.CREATED; + } + + public override async Gee.List? list_email_async(int low, int count, + Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null) + throws Error { + check_open(); + + Gee.List? list = null; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + Geary.Folder.normalize_span_specifiers(ref low, ref count, + do_get_email_count(cx, cancellable)); + + if (count == 0) + return Db.TransactionOutcome.DONE; + + Db.Statement stmt = cx.prepare( + "SELECT id, ordering, message FROM SmtpOutboxTable ORDER BY ordering LIMIT ? OFFSET ?"); + stmt.bind_int(0, count); + stmt.bind_int(1, low - 1); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return Db.TransactionOutcome.DONE; + + list = new Gee.ArrayList(); + int position = low; + do { + list.add(row_to_email(new OutboxRow(results.rowid_at(0), position++, results.int64_at(1), + results.string_at(2)))); + } while (results.next()); + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return list; + } + + public override async Gee.List? list_email_by_id_async( + Geary.EmailIdentifier initial_id, int count, Geary.Email.Field required_fields, + Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error { + check_open(); + + SmtpOutboxEmailIdentifier? id = initial_id as SmtpOutboxEmailIdentifier; + if (id == null) { + throw new EngineError.BAD_PARAMETERS("EmailIdentifier %s not for Outbox", + initial_id.to_string()); + } + + Gee.List? list = null; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + count = int.min(count, do_get_email_count(cx, cancellable)); + + Db.Statement stmt = cx.prepare( + "SELECT id, ordering, message FROM SmtpOutboxTable WHERE ordering >= ? " + + "ORDER BY ordering LIMIT ?"); + stmt.bind_int64(0, + flags.is_all_set(Folder.ListFlags.EXCLUDING_ID) ? id.ordering + 1 : id.ordering); + stmt.bind_int(1, count); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return Db.TransactionOutcome.DONE; + + list = new Gee.ArrayList(); + int position = -1; + do { + int64 ordering = results.int64_at(1); + if (position == -1) { + position = do_get_position_by_ordering(cx, ordering, cancellable); + assert(position >= 1); + } + + list.add(row_to_email(new OutboxRow(results.rowid_at(0), position++, ordering, + results.string_at(2)))); + } while (results.next()); + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return list; + } + + public override async Gee.List? list_email_by_sparse_id_async( + Gee.Collection ids, Geary.Email.Field required_fields, + Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error { + check_open(); + + Gee.List list = new Gee.ArrayList(); + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + foreach (Geary.EmailIdentifier id in ids) { + SmtpOutboxEmailIdentifier? outbox_id = id as SmtpOutboxEmailIdentifier; + if (outbox_id == null) + throw new EngineError.BAD_PARAMETERS("%s is not outbox EmailIdentifier", id.to_string()); + + OutboxRow? row = do_fetch_row_by_ordering(cx, outbox_id.ordering, cancellable); + if (row == null) + continue; + + list.add(row_to_email(row)); + } + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return (list.size > 0) ? list : null; + } + + public override async Gee.Map? + list_local_email_fields_async(Gee.Collection ids, + Cancellable? cancellable = null) throws Error { + check_open(); + + Gee.Map map = new Gee.HashMap< + Geary.EmailIdentifier, Geary.Email.Field>(Hashable.hash_func, Equalable.equal_func); + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + Db.Statement stmt = cx.prepare( + "SELECT id FROM SmtpOutboxTable WHERE ordering=?"); + foreach (Geary.EmailIdentifier id in ids) { + SmtpOutboxEmailIdentifier? outbox_id = id as SmtpOutboxEmailIdentifier; + if (outbox_id == null) + throw new EngineError.BAD_PARAMETERS("%s is not outbox EmailIdentifier", id.to_string()); + + stmt.reset(Db.ResetScope.CLEAR_BINDINGS); + stmt.bind_int64(0, outbox_id.ordering); + + // merely checking for presence, all emails in outbox have same fields + Db.Result results = stmt.exec(cancellable); + if (!results.finished) + map.set(outbox_id, Geary.Email.Field.ALL); + } + + return Db.TransactionOutcome.DONE; + }, cancellable); + + return (map.size > 0) ? map : null; + } + + public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id, + Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, + Cancellable? cancellable = null) throws Error { + check_open(); + + SmtpOutboxEmailIdentifier? outbox_id = id as SmtpOutboxEmailIdentifier; + if (outbox_id == null) + throw new EngineError.BAD_PARAMETERS("%s is not outbox EmailIdentifier", id.to_string()); + + OutboxRow? row = null; + yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + row = do_fetch_row_by_ordering(cx, outbox_id.ordering, cancellable); + + return Db.TransactionOutcome.DONE; + }, cancellable); + + if (row == null) + throw new EngineError.NOT_FOUND("No message with ID %s in outbox", id.to_string()); + + return row_to_email(row); + } + + public virtual async void remove_email_async(Gee.List email_ids, + Cancellable? cancellable = null) throws Error { + check_open(); + + yield internal_remove_email_async(email_ids, cancellable); + } + + // Like remove_email_async(), but can be called even when the folder isn't open + private async bool internal_remove_email_async(Gee.List email_ids, + Cancellable? cancellable) throws Error { + Gee.List removed = new Gee.ArrayList(); + int final_count = 0; + yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => { + foreach (Geary.EmailIdentifier id in email_ids) { + SmtpOutboxEmailIdentifier? outbox_id = id as SmtpOutboxEmailIdentifier; + if (outbox_id == null) + throw new EngineError.BAD_PARAMETERS("%s is not outbox EmailIdentifier", id.to_string()); + + if (do_remove_email(cx, outbox_id, cancellable)) + removed.add(outbox_id); + } + + final_count = do_get_email_count(cx, cancellable); + + return Db.TransactionOutcome.COMMIT; + }, cancellable); + + if (removed.size == 0) + return false; + + // notify only if opened + if (opened) { + notify_email_removed(removed); + notify_email_count_changed(final_count, CountChangeReason.REMOVED); + } + + return true; + } + + public virtual async void remove_single_email_async(Geary.EmailIdentifier id, + Cancellable? cancellable = null) throws Error { + Gee.List list = new Gee.ArrayList(); + list.add(id); + + yield remove_email_async(list, cancellable); + } + + // Utility for getting an email object back from an outbox row. + private Geary.Email row_to_email(OutboxRow row) throws Error { + RFC822.Message message = new RFC822.Message.from_string(row.message); + + Geary.Email email = message.get_email(row.position, row.outbox_id); + email.set_email_properties(new SmtpOutboxEmailProperties()); + email.set_flags(new Geary.EmailFlags()); + + return email; + } + + private async void send_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable) + throws Error { + yield smtp.login_async(settings.credentials, cancellable); + try { + yield smtp.send_email_async(rfc822, cancellable); + } finally { + // always logout + try { + yield smtp.logout_async(cancellable); + } catch (Error err) { + message("Unable to disconnect from SMTP server %s: %s", smtp.to_string(), err.message); + } + } + } + + // + // Transaction helper methods + // + + private int do_get_email_count(Db.Connection cx, Cancellable? cancellable) throws Error { + Db.Statement stmt = cx.prepare("SELECT COUNT(*) FROM SmtpOutboxTable"); + + Db.Result results = stmt.exec(cancellable); + + return (!results.finished) ? results.int_at(0) : 0; + } + + private int do_get_position_by_ordering(Db.Connection cx, int64 ordering, Cancellable? cancellable) + throws Error { + Db.Statement stmt = cx.prepare( + "SELECT COUNT(*), MAX(ordering) FROM SmtpOutboxTable WHERE ordering <= ? ORDER BY ordering ASC"); + stmt.bind_int64(0, ordering); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return -1; + + // without the MAX it's possible to overshoot, so the MAX(ordering) *must* match the argument + if (results.int64_at(1) != ordering) + return -1; + + return results.int_at(0) + 1; + } + + private OutboxRow? do_fetch_row_by_ordering(Db.Connection cx, int64 ordering, Cancellable? cancellable) + throws Error { + Db.Statement stmt = cx.prepare( + "SELECT id, message FROM SmtpOutboxTable WHERE ordering=?"); + stmt.bind_int64(0, ordering); + + Db.Result results = stmt.exec(cancellable); + if (results.finished) + return null; + + int position = do_get_position_by_ordering(cx, ordering, cancellable); + if (position < 1) + return null; + + return new OutboxRow(results.rowid_at(0), position, ordering, results.string_at(1)); + } + + private bool do_remove_email(Db.Connection cx, SmtpOutboxEmailIdentifier id, Cancellable? cancellable) + throws Error { + Db.Statement stmt = cx.prepare("DELETE FROM SmtpOutboxTable WHERE ordering=?"); + stmt.bind_int64(0, id.ordering); + + return stmt.exec_get_modified(cancellable) > 0; + } +} + diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala index 89ac2d48..9bfc6eba 100644 --- a/src/engine/imap/api/imap-account.vala +++ b/src/engine/imap/api/imap-account.vala @@ -30,21 +30,18 @@ private class Geary.Imap.Account : Object { } private string name; - private Geary.Credentials cred; + private AccountSettings settings; private ClientSessionManager session_mgr; - private Geary.Smtp.ClientSession smtp; private Gee.HashMap delims = new Gee.HashMap(); public signal void login_failed(Geary.Credentials cred); - public Account(Geary.Endpoint imap_endpoint, Geary.Endpoint smtp_endpoint, Geary.Credentials cred, - Geary.AccountInformation account_info) { - name = "IMAP Account for %s".printf(cred.to_string()); - this.cred = cred; + public Account(Geary.AccountSettings settings) { + name = "IMAP Account for %s".printf(settings.credentials.to_string()); + this.settings = settings; - session_mgr = new ClientSessionManager(imap_endpoint, cred, account_info); + session_mgr = new ClientSessionManager(settings); session_mgr.login_failed.connect(on_login_failed); - smtp = new Geary.Smtp.ClientSession(smtp_endpoint); } public async void open_async(Cancellable? cancellable) throws Error { @@ -187,24 +184,8 @@ private class Geary.Imap.Account : Object { return parent; } - public async void send_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable = null) - throws Error { - yield smtp.login_async(cred, cancellable); - try { - yield smtp.send_email_async(rfc822, cancellable); - email_sent(rfc822); - } finally { - // always logout - try { - yield smtp.logout_async(cancellable); - } catch (Error err) { - message("Unable to disconnect from SMTP server %s: %s", smtp.to_string(), err.message); - } - } - } - private void on_login_failed() { - login_failed(cred); + login_failed(settings.credentials); } public string to_string() { diff --git a/src/engine/imap/message/imap-message-data.vala b/src/engine/imap/message/imap-message-data.vala index 60ce8928..41596165 100644 --- a/src/engine/imap/message/imap-message-data.vala +++ b/src/engine/imap/message/imap-message-data.vala @@ -23,13 +23,18 @@ public class Geary.Imap.UID : Geary.Common.Int64MessageData, Geary.Imap.MessageD // Using statics because int32.MAX is static, not const (??) public static int64 MIN = 1; public static int64 MAX = int32.MAX; + public static int64 INVALID = -1; public UID(int64 value) { base (value); } public bool is_valid() { - return Numeric.int64_in_range_inclusive(value, MIN, MAX); + return is_value_valid(value); + } + + public static bool is_value_valid(int64 val) { + return Numeric.int64_in_range_inclusive(val, MIN, MAX); } /** diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index d317012c..6cb51d2f 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -7,9 +7,7 @@ public class Geary.Imap.ClientSessionManager { public const int DEFAULT_MIN_POOL_SIZE = 2; - private Endpoint endpoint; - private Credentials credentials; - private AccountInformation account_info; + private AccountSettings settings; private int min_pool_size; private Gee.HashSet sessions = new Gee.HashSet(); private Geary.NonblockingMutex sessions_mutex = new Geary.NonblockingMutex(); @@ -21,11 +19,8 @@ public class Geary.Imap.ClientSessionManager { public signal void login_failed(); - public ClientSessionManager(Endpoint endpoint, Credentials credentials, - AccountInformation account_info, int min_pool_size = DEFAULT_MIN_POOL_SIZE) { - this.endpoint = endpoint; - this.credentials = credentials; - this.account_info = account_info; + public ClientSessionManager(AccountSettings settings, int min_pool_size = DEFAULT_MIN_POOL_SIZE) { + this.settings = settings; this.min_pool_size = min_pool_size; adjust_session_pool.begin(); @@ -47,7 +42,7 @@ public class Geary.Imap.ClientSessionManager { try { yield create_new_authorized_session(null); } catch (Error err) { - debug("Unable to create authorized session to %s: %s", endpoint.to_string(), err.message); + debug("Unable to create authorized session to %s: %s", settings.imap_endpoint.to_string(), err.message); break; } @@ -223,7 +218,7 @@ public class Geary.Imap.ClientSessionManager { // This should only be called when sessions_mutex is locked. private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error { - ClientSession new_session = new ClientSession(endpoint, account_info); + ClientSession new_session = new ClientSession(settings.imap_endpoint, settings.imap_server_pipeline); // add session to pool before launching all the connect activity so error cases can properly // back it out @@ -231,7 +226,7 @@ public class Geary.Imap.ClientSessionManager { try { yield new_session.connect_async(cancellable); - yield new_session.login_async(credentials, cancellable); + yield new_session.login_async(settings.credentials, cancellable); // If no capabilities were returned at login, ask for them now if (new_session.get_capabilities().is_empty()) @@ -365,7 +360,7 @@ public class Geary.Imap.ClientSessionManager { * Use only for debugging and logging. */ public string to_string() { - return endpoint.to_string(); + return settings.imap_endpoint.to_string(); } } diff --git a/src/engine/imap/transport/imap-client-session.vala b/src/engine/imap/transport/imap-client-session.vala index 9d03219e..7ef64a8d 100644 --- a/src/engine/imap/transport/imap-client-session.vala +++ b/src/engine/imap/transport/imap-client-session.vala @@ -170,8 +170,8 @@ public class Geary.Imap.ClientSession { "Geary.Imap.ClientSession", State.DISCONNECTED, State.COUNT, Event.COUNT, state_to_string, event_to_string); - private Geary.Endpoint endpoint; - private Geary.AccountInformation account_info; + private Endpoint imap_endpoint; + private bool imap_server_pipeline; private Geary.State.Machine fsm; private ImapError not_connected_err; private ClientConnection? cx = null; @@ -235,11 +235,11 @@ public class Geary.Imap.ClientSession { public virtual signal void unsolicited_flags(MailboxAttributes attrs) { } - public ClientSession(Geary.Endpoint endpoint, Geary.AccountInformation account_info) { - this.endpoint = endpoint; - this.account_info = account_info; + public ClientSession(Endpoint imap_endpoint, bool imap_server_pipeline) { + this.imap_endpoint = imap_endpoint; + this.imap_server_pipeline = imap_server_pipeline; - not_connected_err = new ImapError.NOT_CONNECTED("Not connected to %s", endpoint.to_string()); + not_connected_err = new ImapError.NOT_CONNECTED("Not connected to %s", imap_endpoint.to_string()); Geary.State.Mapping[] mappings = { new Geary.State.Mapping(State.DISCONNECTED, Event.CONNECT, on_connect), @@ -439,7 +439,7 @@ public class Geary.Imap.ClientSession { connect_params = (AsyncParams) object; assert(cx == null); - cx = new ClientConnection(endpoint); + cx = new ClientConnection(imap_endpoint); cx.connected.connect(on_network_connected); cx.disconnected.connect(on_network_disconnected); cx.sent_command.connect(on_network_sent_command); @@ -1143,7 +1143,7 @@ public class Geary.Imap.ClientSession { return new AsyncCommandResponse(null, user, not_connected_err); int claim_stub = NonblockingMutex.INVALID_TOKEN; - if (!account_info.imap_server_pipeline) { + if (!imap_server_pipeline) { try { debug("[%s] Waiting to send cmd %s: %d", to_full_string(), cmd.to_string(), ++waiting_to_send); claim_stub = yield serialized_cmds_mutex.claim_async(cancellable); @@ -1161,7 +1161,7 @@ public class Geary.Imap.ClientSession { yield cx.send_async(cmd, cancellable); } catch (Error send_err) { try { - if (!account_info.imap_server_pipeline && claim_stub != NonblockingMutex.INVALID_TOKEN) + if (!imap_server_pipeline && claim_stub != NonblockingMutex.INVALID_TOKEN) serialized_cmds_mutex.release(ref claim_stub); } catch (Error abort_err) { debug("Error attempting to abort from send operation: %s", abort_err.message); @@ -1188,7 +1188,7 @@ public class Geary.Imap.ClientSession { assert(cmd_response.is_sealed()); assert(cmd_response.status_response.tag.equals(cmd.tag)); - if (!account_info.imap_server_pipeline && claim_stub != NonblockingMutex.INVALID_TOKEN) { + if (!imap_server_pipeline && claim_stub != NonblockingMutex.INVALID_TOKEN) { try { serialized_cmds_mutex.release(ref claim_stub); } catch (Error notify_err) { @@ -1263,7 +1263,7 @@ public class Geary.Imap.ClientSession { // private void on_network_connected() { - debug("[%s] Connected to %s", to_full_string(), endpoint.to_string()); + debug("[%s] Connected to %s", to_full_string(), imap_endpoint.to_string()); // the first ServerData from the server is a greeting; this flag indicates to treat it // differently than the other data thereafter @@ -1271,7 +1271,7 @@ public class Geary.Imap.ClientSession { } private void on_network_disconnected() { - debug("[%s] Disconnected from %s", to_full_string(), endpoint.to_string()); + debug("[%s] Disconnected from %s", to_full_string(), imap_endpoint.to_string()); } private void on_network_sent_command(Command cmd) { @@ -1367,7 +1367,7 @@ public class Geary.Imap.ClientSession { } public string to_string() { - return "ClientSession:%s".printf((cx == null) ? endpoint.to_string() : cx.to_string()); + return "ClientSession:%s".printf((cx == null) ? imap_endpoint.to_string() : cx.to_string()); } public string to_full_string() { diff --git a/src/engine/impl/geary-generic-imap-account.vala b/src/engine/impl/geary-generic-imap-account.vala index e4339caf..180ad9b0 100644 --- a/src/engine/impl/geary-generic-imap-account.vala +++ b/src/engine/impl/geary-generic-imap-account.vala @@ -9,18 +9,18 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { private static Geary.FolderPath? outbox_path = null; private Imap.Account remote; - private Sqlite.Account local; + private ImapDB.Account local; + private bool open = false; private Gee.HashMap properties_map = new Gee.HashMap< FolderPath, Imap.FolderProperties>(Hashable.hash_func, Equalable.equal_func); - private SmtpOutboxFolder? outbox = null; private Gee.HashMap existing_folders = new Gee.HashMap< FolderPath, GenericImapFolder>(Hashable.hash_func, Equalable.equal_func); private Gee.HashSet local_only = new Gee.HashSet( Hashable.hash_func, Equalable.equal_func); - public GenericImapAccount(string name, string username, AccountInformation? account_info, - File user_data_dir, Imap.Account remote, Sqlite.Account local) { - base (name, username, account_info, user_data_dir); + public GenericImapAccount(string name, Geary.AccountSettings settings, Imap.Account remote, + ImapDB.Account local) { + base (name, settings); this.remote = remote; this.local = local; @@ -43,9 +43,17 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { return properties_map.get(path); } + private void check_open() throws EngineError { + if (!open) + throw new EngineError.OPEN_REQUIRED("Account %s not opened", to_string()); + } + public override async void open_async(Cancellable? cancellable = null) throws Error { - yield local.open_async(get_account_information().credentials, Engine.user_data_dir, Engine.resource_dir, - cancellable); + if (open) + throw new EngineError.ALREADY_OPEN("Account %s already opened", to_string()); + + yield local.open_async(Engine.user_data_dir.get_child(settings.credentials.user), + Engine.resource_dir.get_child("sql"), cancellable); // need to back out local.open_async() if remote fails try { @@ -61,12 +69,15 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { throw err; } - outbox = new SmtpOutboxFolder(remote, local.get_outbox()); + open = true; notify_opened(); } public override async void close_async(Cancellable? cancellable = null) throws Error { + if (!open) + return; + // attempt to close both regardless of errors Error? local_err = null; try { @@ -82,8 +93,6 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { remote_err = rclose_err; } - outbox = null; - if (local_err != null) throw local_err; @@ -97,9 +106,9 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { // // This won't be called to build the Outbox, but for all others (including Inbox) it will. protected abstract GenericImapFolder new_folder(Geary.FolderPath path, Imap.Account remote_account, - Sqlite.Account local_account, Sqlite.Folder local_folder); + ImapDB.Account local_account, ImapDB.Folder local_folder); - private GenericImapFolder build_folder(Sqlite.Folder local_folder) { + private GenericImapFolder build_folder(ImapDB.Folder local_folder) { GenericImapFolder? folder = existing_folders.get(local_folder.get_path()); if (folder != null) return folder; @@ -112,7 +121,9 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { public override async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error { - Gee.Collection? local_list = null; + check_open(); + + Gee.Collection? local_list = null; try { local_list = yield local.list_folders_async(parent, cancellable); } catch (EngineError err) { @@ -123,13 +134,13 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { Gee.Collection engine_list = new Gee.ArrayList(); if (local_list != null && local_list.size > 0) { - foreach (Geary.Sqlite.Folder local_folder in local_list) + foreach (ImapDB.Folder local_folder in local_list) engine_list.add(build_folder(local_folder)); } // Add Outbox to root if (parent == null) - engine_list.add(outbox); + engine_list.add(local.outbox); background_update_folders.begin(parent, engine_list, cancellable); @@ -138,6 +149,8 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { public override async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { + check_open(); + if (yield local.folder_exists_async(path, cancellable)) return true; @@ -147,12 +160,13 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { // TODO: This needs to be made into a single transaction public override async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { + check_open(); - if (path.equals(outbox.get_path())) - return outbox; + if (path.equals(local.outbox.get_path())) + return local.outbox; try { - return build_folder((Sqlite.Folder) yield local.fetch_folder_async(path, cancellable)); + return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable)); } catch (EngineError err) { // don't thrown NOT_FOUND's, that means we need to fall through and clone from the // server @@ -175,7 +189,7 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { } // Fetch the local account's version of the folder for the GenericImapFolder - return build_folder((Sqlite.Folder) yield local.fetch_folder_async(path, cancellable)); + return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable)); } private async void background_update_folders(Geary.FolderPath? parent, @@ -259,7 +273,7 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { engine_added = new Gee.ArrayList(); foreach (Geary.Imap.Folder remote_folder in to_add) { try { - engine_added.add(build_folder((Sqlite.Folder) yield local.fetch_folder_async( + engine_added.add(build_folder((ImapDB.Folder) yield local.fetch_folder_async( remote_folder.get_path(), cancellable))); } catch (Error convert_err) { error("Unable to fetch local folder: %s", convert_err.message); @@ -278,14 +292,14 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { notify_folders_added_removed(engine_added, null); } - public override bool delete_is_archive() { - return false; - } - public override async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) throws Error { + check_open(); + Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(composed); - yield outbox.create_email_async(rfc822, cancellable); + + // don't use create_email_async() as that requires the folder be open to use + yield local.outbox.enqueue_email_async(rfc822, cancellable); } private void on_email_sent(Geary.RFC822.Message rfc822) { diff --git a/src/engine/impl/geary-generic-imap-folder.vala b/src/engine/impl/geary-generic-imap-folder.vala index 3dac78f6..9dabe4c7 100644 --- a/src/engine/impl/geary-generic-imap-folder.vala +++ b/src/engine/impl/geary-generic-imap-folder.vala @@ -8,16 +8,16 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor Geary.FolderSupportsMark, Geary.FolderSupportsMove { internal const int REMOTE_FETCH_CHUNK_COUNT = 50; - private const Geary.Email.Field NORMALIZATION_FIELDS = Geary.Email.Field.PROPERTIES - | Geary.Email.Field.FLAGS; + private const Geary.Email.Field NORMALIZATION_FIELDS = + Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS | ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION; - internal Sqlite.Folder local_folder { get; protected set; } + internal ImapDB.Folder local_folder { get; protected set; } internal Imap.Folder? remote_folder { get; protected set; default = null; } internal int remote_count { get; private set; default = -1; } private weak GenericImapAccount account; private Imap.Account remote; - private Sqlite.Account local; + private ImapDB.Account local; private EmailFlagWatcher email_flag_watcher; private EmailPrefetcher email_prefetcher; private SpecialFolderType special_folder_type; @@ -26,8 +26,8 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor private ReplayQueue? replay_queue = null; private NonblockingMutex normalize_email_positions_mutex = new NonblockingMutex(); - public GenericImapFolder(GenericImapAccount account, Imap.Account remote, Sqlite.Account local, - Sqlite.Folder local_folder, SpecialFolderType special_folder_type) { + public GenericImapFolder(GenericImapAccount account, Imap.Account remote, ImapDB.Account local, + ImapDB.Folder local_folder, SpecialFolderType special_folder_type) { this.account = account; this.remote = remote; this.local = local; @@ -165,7 +165,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor // Get the local emails in the range ... use PARTIAL_OK to ensure all emails are normalized Gee.List? old_local = yield local_folder.list_email_by_id_async( - earliest_id, int.MAX, NORMALIZATION_FIELDS, Sqlite.Folder.ListFlags.PARTIAL_OK, cancellable); + earliest_id, int.MAX, NORMALIZATION_FIELDS, ImapDB.Folder.ListFlags.PARTIAL_OK, cancellable); // be sure they're sorted from earliest to latest if (old_local != null) @@ -387,7 +387,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor try { count = (remote_folder != null) ? remote_count - : yield local_folder.get_email_count_async(cancellable); + : yield local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, cancellable); } catch (Error count_err) { debug("Unable to fetch count from local folder: %s", count_err.message); @@ -424,6 +424,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor if (remote_folder != null) return true; + debug("waiting for remote to open, replay queue blocked..."); yield remote_semaphore.wait_async(cancellable); return (remote_folder != null); @@ -535,7 +536,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor // which has now changed Imap.MessageSet msg_set = new Imap.MessageSet.range_to_highest(remote_count + 1); Gee.List? list = yield remote_folder.list_email_async( - msg_set, Geary.Sqlite.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, null); + msg_set, ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, null); if (list != null && list.size > 0) { debug("do_replay_appended_messages: %d new messages from %s in %s", list.size, msg_set.to_string(), to_string()); @@ -547,7 +548,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor // need to report both if it was created (not known before) and appended (which // could mean created or simply a known email associated with this folder) - if (yield local_folder.create_email_async(email, null)) { + if (yield local_folder.create_or_merge_email_async(email, null)) { created.add(email.id); } else { debug("do_replay_appended_messages: appended email ID %s already known in account, now associated with %s...", @@ -599,7 +600,8 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor Geary.EmailIdentifier? owned_id = null; try { - local_count = yield local_folder.get_email_count_including_marked_async(); + local_count = yield local_folder.get_email_count_async(ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, + null); // can't use remote_position_to_local_position() because local_count includes messages // marked for removal, which that helper function doesn't like local_position = remote_position - (remote_count - local_count); @@ -607,7 +609,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor debug("do_replay_remove_message: local_count=%d local_position=%d", local_count, local_position); Gee.List? list = yield local_folder.list_email_async(local_position, - 1, Geary.Email.Field.NONE, Sqlite.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null); + 1, Geary.Email.Field.NONE, ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null); if (list != null && list.size > 0) owned_id = list[0].id; } catch (Error err) { @@ -634,7 +636,8 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor // for debugging int new_local_count = -1; try { - new_local_count = yield local_folder.get_email_count_including_marked_async(); + new_local_count = yield local_folder.get_email_count_async( + ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null); } catch (Error new_count_err) { debug("Error fetching new local count for %s: %s", to_string(), new_count_err.message); } @@ -686,7 +689,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor return remote_count; } - return yield local_folder.get_email_count_async(cancellable); + return yield local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, cancellable); } // @@ -776,6 +779,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor EmailCallback? cb, Cancellable? cancellable) throws Error { check_open(method); check_flags(method, flags); + check_id(method, initial_id); // listing by ID requires the remote to be open and fully synchronized, as there's no // reliable way to determine certain counts and positions without it @@ -827,6 +831,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor Gee.List? accumulator, EmailCallback? cb, Cancellable? cancellable = null) throws Error { check_open(method); check_flags(method, flags); + check_ids(method, ids); // Unlike list_email_by_id, don't need to wait for remote to open because not dealing with // a range of emails, but specific ones by ID @@ -840,6 +845,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor public override async Gee.Map? list_local_email_fields_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error { check_open("list_local_email_fields_async"); + check_ids("list_local_email_fields_async", ids); return yield local_folder.list_email_fields_by_id_async(ids, cancellable); } @@ -849,6 +855,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor throws Error { check_open("fetch_email_async"); check_flags("fetch_email_async", flags); + check_id("fetch_email_async", id); FetchEmail op = new FetchEmail(this, id, required_fields, flags, cancellable); replay_queue.schedule(op); @@ -870,6 +877,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor protected async void expunge_email_async(Gee.List email_ids, Cancellable? cancellable = null) throws Error { check_open("expunge_email_async"); + check_ids("expunge_email_async", email_ids); replay_queue.schedule(new ExpungeEmail(this, email_ids, cancellable)); } @@ -886,6 +894,16 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor } } + private void check_id(string method, EmailIdentifier id) throws EngineError { + if (!(id is Imap.EmailIdentifier)) + throw new EngineError.BAD_PARAMETERS("Email ID %s is not IMAP Email ID", id.to_string()); + } + + private void check_ids(string method, Gee.Collection ids) throws EngineError { + foreach (EmailIdentifier id in ids) + check_id(method, id); + } + // Converts a remote position to a local position, assuming that the remote has been completely // opened. local_count must be supplied because that's not held by EngineFolder (unlike // remote_count). remote_pos is 1-based. @@ -938,7 +956,8 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor Error? error = null; try { - local_count = yield local_folder.get_email_count_async(cancellable); + local_count = yield local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, + cancellable); // fixup span specifier normalize_span_specifiers(ref low, ref count, remote_count); @@ -963,7 +982,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor // as fields for duplicate detection Gee.List? list = yield remote_folder.list_email_async( new Imap.MessageSet.range(high, prefetch_count), - Geary.Sqlite.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, cancellable); + ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, cancellable); if (list == null || list.size != prefetch_count) { throw new EngineError.BAD_PARAMETERS("Unable to prefetch %d email starting at %d in %s", count, low, to_string()); @@ -973,7 +992,7 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder, Geary.FolderSuppor foreach (Geary.Email email in list) { batch.add(new CreateLocalEmailOperation(local_folder, email, - Geary.Sqlite.Folder.REQUIRED_FOR_DUPLICATE_DETECTION)); + ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION)); } yield batch.execute_all_async(cancellable); diff --git a/src/engine/impl/geary-gmail-account.vala b/src/engine/impl/geary-gmail-account.vala index 7d419e3a..85dca091 100644 --- a/src/engine/impl/geary-gmail-account.vala +++ b/src/engine/impl/geary-gmail-account.vala @@ -36,9 +36,9 @@ private class Geary.GmailAccount : Geary.GenericImapAccount { private static Gee.HashMap? path_type_map = null; - public GmailAccount(string name, string username, AccountInformation account_info, - File user_data_dir, Imap.Account remote, Sqlite.Account local) { - base (name, username, account_info, user_data_dir, remote, local); + public GmailAccount(string name, Geary.AccountSettings settings, Imap.Account remote, + ImapDB.Account local) { + base (name, settings, remote, local); if (path_type_map == null) { path_type_map = new Gee.HashMap( @@ -73,7 +73,7 @@ private class Geary.GmailAccount : Geary.GenericImapAccount { } protected override GenericImapFolder new_folder(Geary.FolderPath path, Imap.Account remote_account, - Sqlite.Account local_account, Sqlite.Folder local_folder) { + ImapDB.Account local_account, ImapDB.Folder local_folder) { // although Gmail supports XLIST, this will be called on startup if the XLIST properties // for the folders hasn't been retrieved yet. Once they've been retrieved and stored in // the local database, this won't be called again diff --git a/src/engine/impl/geary-gmail-folder.vala b/src/engine/impl/geary-gmail-folder.vala index a2ab127c..b0af4cfb 100644 --- a/src/engine/impl/geary-gmail-folder.vala +++ b/src/engine/impl/geary-gmail-folder.vala @@ -5,8 +5,8 @@ */ private class Geary.GmailFolder : GenericImapFolder, FolderSupportsArchive { - public GmailFolder(GmailAccount account, Imap.Account remote, Sqlite.Account local, - Sqlite.Folder local_folder, SpecialFolderType special_folder_type) { + public GmailFolder(GmailAccount account, Imap.Account remote, ImapDB.Account local, + ImapDB.Folder local_folder, SpecialFolderType special_folder_type) { base (account, remote, local, local_folder, special_folder_type); } diff --git a/src/engine/impl/geary-other-account.vala b/src/engine/impl/geary-other-account.vala index de1b4c82..74f7afeb 100644 --- a/src/engine/impl/geary-other-account.vala +++ b/src/engine/impl/geary-other-account.vala @@ -5,13 +5,13 @@ */ private class Geary.OtherAccount : Geary.GenericImapAccount { - public OtherAccount(string name, string username, AccountInformation account_info, - File user_data_dir, Imap.Account remote, Sqlite.Account local) { - base (name, username, account_info, user_data_dir, remote, local); + public OtherAccount(string name, AccountSettings settings, Imap.Account remote, + ImapDB.Account local) { + base (name, settings, remote, local); } protected override GenericImapFolder new_folder(Geary.FolderPath path, Imap.Account remote_account, - Sqlite.Account local_account, Sqlite.Folder local_folder) { + ImapDB.Account local_account, ImapDB.Folder local_folder) { return new OtherFolder(this, remote_account, local_account, local_folder, (path.basename == Imap.Account.INBOX_NAME) ? SpecialFolderType.INBOX : SpecialFolderType.NONE); } diff --git a/src/engine/impl/geary-other-folder.vala b/src/engine/impl/geary-other-folder.vala index f7233810..8f433f0d 100644 --- a/src/engine/impl/geary-other-folder.vala +++ b/src/engine/impl/geary-other-folder.vala @@ -5,8 +5,8 @@ */ private class Geary.OtherFolder : GenericImapFolder, Geary.FolderSupportsRemove { - public OtherFolder(OtherAccount account, Imap.Account remote, Sqlite.Account local, - Sqlite.Folder local_folder, SpecialFolderType special_folder_type) { + public OtherFolder(OtherAccount account, Imap.Account remote, ImapDB.Account local, + ImapDB.Folder local_folder, SpecialFolderType special_folder_type) { base (account, remote, local, local_folder, special_folder_type); } diff --git a/src/engine/impl/geary-send-replay-operations.vala b/src/engine/impl/geary-send-replay-operations.vala index 4d9c99cf..eeabe408 100644 --- a/src/engine/impl/geary-send-replay-operations.vala +++ b/src/engine/impl/geary-send-replay-operations.vala @@ -89,9 +89,7 @@ private class Geary.ExpungeEmail : Geary.SendReplayOperation { } public override async ReplayOperation.Status replay_local_async() throws Error { - // TODO: Use a local_folder method that operates on all messages at once - foreach (Geary.EmailIdentifier id in to_remove) - yield engine.local_folder.mark_removed_async(id, true, cancellable); + yield engine.local_folder.mark_removed_async(to_remove, true, cancellable); engine.notify_email_removed(to_remove); @@ -116,9 +114,7 @@ private class Geary.ExpungeEmail : Geary.SendReplayOperation { } public override async void backout_local_async() throws Error { - // TODO: Use a local_folder method that operates on all messages at once - foreach (Geary.EmailIdentifier id in to_remove) - yield engine.local_folder.mark_removed_async(id, false, cancellable); + yield engine.local_folder.mark_removed_async(to_remove, false, cancellable); engine.notify_email_appended(to_remove); engine.notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.ADDED); @@ -203,7 +199,7 @@ private class Geary.ListEmail : Geary.SendReplayOperation { // requested by user ... this ensures the local store is seeded with certain fields required // for it to operate properly if (!remote_only && !local_only) - this.required_fields |= Sqlite.Folder.REQUIRED_FOR_DUPLICATE_DETECTION; + this.required_fields |= ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION; } public override async ReplayOperation.Status replay_local_async() throws Error { @@ -215,7 +211,8 @@ private class Geary.ListEmail : Geary.SendReplayOperation { yield engine.normalize_email_positions_async(low, count, out local_count, cancellable); } else { // local_only means just that - local_count = yield engine.local_folder.get_email_count_async(cancellable); + local_count = yield engine.local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, + cancellable); } // normalize the arguments so they reflect cardinal positions ... remote_count can be -1 @@ -242,7 +239,7 @@ private class Geary.ListEmail : Geary.SendReplayOperation { if (!remote_only && local_low > 0) { try { local_list = yield engine.local_folder.list_email_async(local_low, count, required_fields, - Sqlite.Folder.ListFlags.PARTIAL_OK, cancellable); + ImapDB.Folder.ListFlags.PARTIAL_OK, cancellable); } catch (Error local_err) { if (cb != null && !(local_err is IOError.CANCELLED)) cb (null, local_err); @@ -478,9 +475,11 @@ private class Geary.ListEmailByID : Geary.ListEmail { } public override async ReplayOperation.Status replay_local_async() throws Error { - int local_count = yield engine.local_folder.get_email_count_async(cancellable); + int local_count = yield engine.local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, + cancellable); - int initial_position = yield engine.local_folder.get_id_position_async(initial_id, cancellable); + int initial_position = yield engine.local_folder.get_id_position_async(initial_id, + ImapDB.Folder.ListFlags.NONE, cancellable); if (initial_position <= 0) { throw new EngineError.NOT_FOUND("Email ID %s in %s not known to local store", initial_id.to_string(), engine.to_string()); @@ -554,7 +553,7 @@ private class Geary.ListEmailBySparseID : Geary.SendReplayOperation { public override async Object? execute_async(Cancellable? cancellable) throws Error { try { return yield owner.local_folder.fetch_email_async(id, required_fields, - Sqlite.Folder.ListFlags.PARTIAL_OK, cancellable); + ImapDB.Folder.ListFlags.PARTIAL_OK, cancellable); } catch (Error err) { // only throw errors that are not NOT_FOUND and INCOMPLETE_MESSAGE, as these two // are recoverable @@ -594,12 +593,12 @@ private class Geary.ListEmailBySparseID : Geary.SendReplayOperation { for (int ctr = 0; ctr < list.size; ctr++) { Geary.Email email = list[ctr]; - yield owner.local_folder.create_email_async(email, cancellable); + yield owner.local_folder.create_or_merge_email_async(email, cancellable); // if remote email doesn't fulfills all required fields, fetch full and return that if (!email.fields.fulfills(required_fields)) { email = yield owner.local_folder.fetch_email_async(email.id, required_fields, - Sqlite.Folder.ListFlags.NONE, cancellable); + ImapDB.Folder.ListFlags.NONE, cancellable); list[ctr] = email; } } @@ -768,7 +767,7 @@ private class Geary.FetchEmail : Geary.SendReplayOperation { try { email = yield engine.local_folder.fetch_email_async(id, required_fields, - Sqlite.Folder.ListFlags.PARTIAL_OK, cancellable); + ImapDB.Folder.ListFlags.PARTIAL_OK, cancellable); } catch (Error err) { // If NOT_FOUND or INCOMPLETE_MESSAGE, then fall through, otherwise return to sender if (!(err is Geary.EngineError.NOT_FOUND) && !(err is Geary.EngineError.INCOMPLETE_MESSAGE)) @@ -811,14 +810,14 @@ private class Geary.FetchEmail : Geary.SendReplayOperation { // save to local store email = list[0]; assert(email != null); - if (yield engine.local_folder.create_email_async(email, cancellable)) + if (yield engine.local_folder.create_or_merge_email_async(email, cancellable)) engine.notify_email_locally_appended(new Geary.Singleton(email.id)); // if remote_email doesn't fulfill all required, pull from local database, which should now // be able to do all of that if (!email.fields.fulfills(required_fields)) { email = yield engine.local_folder.fetch_email_async(id, required_fields, - Sqlite.Folder.ListFlags.NONE, cancellable); + ImapDB.Folder.ListFlags.NONE, cancellable); assert(email != null); } @@ -896,10 +895,7 @@ private class Geary.MoveEmail : Geary.SendReplayOperation { } public override async ReplayOperation.Status replay_local_async() throws Error { - // Remove the email from the folder. - // TODO: Use a local_folder method that operates on all messages at once - foreach (Geary.EmailIdentifier id in to_move) - yield engine.local_folder.mark_removed_async(id, true, cancellable); + yield engine.local_folder.mark_removed_async(to_move, true, cancellable); engine.notify_email_removed(to_move); original_count = engine.remote_count; @@ -920,10 +916,7 @@ private class Geary.MoveEmail : Geary.SendReplayOperation { } public override async void backout_local_async() throws Error { - // Add the email back in. - // TODO: Use a local_folder method that operates on all messages at once - foreach (Geary.EmailIdentifier id in to_move) - yield engine.local_folder.mark_removed_async(id, false, cancellable); + yield engine.local_folder.mark_removed_async(to_move, false, cancellable); engine.notify_email_appended(to_move); engine.notify_email_count_changed(original_count, Geary.Folder.CountChangeReason.ADDED); diff --git a/src/engine/impl/geary-yahoo-account.vala b/src/engine/impl/geary-yahoo-account.vala index f716b867..9f5147dc 100644 --- a/src/engine/impl/geary-yahoo-account.vala +++ b/src/engine/impl/geary-yahoo-account.vala @@ -33,9 +33,9 @@ private class Geary.YahooAccount : Geary.GenericImapAccount { private static Gee.HashMap? special_map = null; - public YahooAccount(string name, string username, AccountInformation account_info, - File user_data_dir, Imap.Account remote, Sqlite.Account local) { - base (name, username, account_info, user_data_dir, remote, local); + public YahooAccount(string name, AccountSettings settings, Imap.Account remote, + ImapDB.Account local) { + base (name, settings, remote, local); if (special_map == null) { special_map = new Gee.HashMap( @@ -55,7 +55,7 @@ private class Geary.YahooAccount : Geary.GenericImapAccount { } protected override GenericImapFolder new_folder(Geary.FolderPath path, Imap.Account remote_account, - Sqlite.Account local_account, Sqlite.Folder local_folder) { + ImapDB.Account local_account, ImapDB.Folder local_folder) { return new YahooFolder(this, remote_account, local_account, local_folder, special_map.has_key(path) ? special_map.get(path) : Geary.SpecialFolderType.NONE); } diff --git a/src/engine/impl/geary-yahoo-folder.vala b/src/engine/impl/geary-yahoo-folder.vala index c241ff85..5d005f6b 100644 --- a/src/engine/impl/geary-yahoo-folder.vala +++ b/src/engine/impl/geary-yahoo-folder.vala @@ -5,8 +5,8 @@ */ private class Geary.YahooFolder : GenericImapFolder, Geary.FolderSupportsRemove { - public YahooFolder(YahooAccount account, Imap.Account remote, Sqlite.Account local, - Sqlite.Folder local_folder, SpecialFolderType special_folder_type) { + public YahooFolder(YahooAccount account, Imap.Account remote, ImapDB.Account local, + ImapDB.Folder local_folder, SpecialFolderType special_folder_type) { base (account, remote, local, local_folder, special_folder_type); } diff --git a/src/engine/impl/outbox/smtp-outbox-folder.vala b/src/engine/impl/outbox/smtp-outbox-folder.vala deleted file mode 100644 index 7618b683..00000000 --- a/src/engine/impl/outbox/smtp-outbox-folder.vala +++ /dev/null @@ -1,292 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -private class Geary.SmtpOutboxFolderRoot : Geary.FolderRoot { - public const string MAGIC_BASENAME = "$GearyOutbox$"; - - public SmtpOutboxFolderRoot() { - base(MAGIC_BASENAME, null, false); - } -} - -// Special type of folder that runs an asynchronous send queue. Messages are -// saved to the database, then queued up for sending. -private class Geary.SmtpOutboxFolder : Geary.AbstractFolder, Geary.FolderSupportsRemove, - Geary.FolderSupportsCreate { - private static FolderRoot? path = null; - - private Geary.Sqlite.SmtpOutboxTable local_folder; - private Imap.Account remote; - private Sqlite.Database db; - private bool opened = false; - private NonblockingMailbox outbox_queue = - new NonblockingMailbox(); - - public SmtpOutboxFolder(Imap.Account remote, Geary.Sqlite.SmtpOutboxTable table) { - this.remote = remote; - this.local_folder = table; - db = table.gdb; - - do_postman_async.begin(); - } - - private string message_subject(RFC822.Message message) { - return (message.subject != null && !String.is_empty(message.subject.to_string())) - ? message.subject.to_string() : _("(no subject)"); - } - - // TODO: Use Cancellable to shut down outbox processor when closing account - private async void do_postman_async() { - debug("Starting outbox postman"); - - // Fill the send queue with existing mail (if any) - try { - Gee.List? row_list = yield local_folder.list_email_async( - null, new OutboxEmailIdentifier(-1), -1, null); - if (row_list != null && row_list.size > 0) { - debug("Priming outbox postman with %d stored messages", row_list.size); - foreach (Geary.Sqlite.SmtpOutboxRow row in row_list) - outbox_queue.send(row); - } - } catch (Error prime_err) { - warning("Error priming outbox: %s", prime_err.message); - } - - // Start the send queue. - for (;;) { - // yield until a message is ready - Geary.Sqlite.SmtpOutboxRow row; - try { - row = yield outbox_queue.recv_async(); - } catch (Error wait_err) { - debug("Outbox postman queue error: %s", wait_err.message); - - break; - } - - // Convert row into RFC822 message suitable for sending or framing - RFC822.Message message; - try { - message = new RFC822.Message.from_string(row.message); - } catch (RFC822Error msg_err) { - // TODO: This needs to be reported to the user - debug("Outbox postman message error: %s", msg_err.message); - - continue; - } - - // Send the message, but only remove from database once sent - try { - debug("Outbox postman: Sending \"%s\" (ID:%s)...", message_subject(message), row.to_string()); - yield remote.send_email_async(message); - } catch (Error send_err) { - debug("Outbox postman send error, retrying: %s", send_err.message); - - try { - outbox_queue.send(row); - } catch (Error send_err) { - debug("Outbox postman: Unable to re-send row to outbox, dropping on floor: %s", send_err.message); - } - - continue; - } - - // Remove from database - try { - debug("Outbox postman: Removing \"%s\" (ID:%s) from database", message_subject(message), - row.to_string()); - yield remove_single_email_async(new OutboxEmailIdentifier(row.ordering)); - } catch (Error rm_err) { - debug("Outbox postman: Unable to remove row from database: %s", rm_err.message); - } - } - - debug("Exiting outbox postman"); - } - - public override Geary.FolderPath get_path() { - if (path == null) - path = new SmtpOutboxFolderRoot(); - - return path; - } - - public override Geary.Trillian has_children() { - return Geary.Trillian.FALSE; - } - - public override Geary.SpecialFolderType get_special_folder_type() { - return Geary.SpecialFolderType.OUTBOX; - } - - public override Geary.Folder.OpenState get_open_state() { - return opened ? Geary.Folder.OpenState.LOCAL : Geary.Folder.OpenState.CLOSED; - } - - public override async void open_async(bool readonly, Cancellable? cancellable = null) - throws Error { - if (opened) - throw new EngineError.ALREADY_OPEN("Folder %s already open", to_string()); - - opened = true; - notify_opened(Geary.Folder.OpenState.LOCAL, yield get_email_count_async(cancellable)); - } - - public override async void close_async(Cancellable? cancellable = null) throws Error { - opened = false; - notify_closed(Geary.Folder.CloseReason.LOCAL_CLOSE); - notify_closed(Geary.Folder.CloseReason.FOLDER_CLOSED); - } - - public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { - return yield internal_get_email_count_async(null, cancellable); - } - - private async int internal_get_email_count_async(Sqlite.Transaction? transaction, Cancellable? cancellable) - throws Error { - return yield local_folder.get_email_count_async(transaction, cancellable); - } - - public virtual async Geary.FolderSupportsCreate.Result create_email_async(Geary.RFC822.Message rfc822, - Cancellable? cancellable = null) throws Error { - Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.create_email_async", - cancellable); - - Geary.Sqlite.SmtpOutboxRow row = yield local_folder.create_async(transaction, - rfc822.get_body_rfc822_buffer().to_string(), cancellable); - - int count = yield internal_get_email_count_async(transaction, cancellable); - - // signal message added before adding for delivery - Gee.List list = new Gee.ArrayList(); - list.add(new OutboxEmailIdentifier(row.ordering)); - notify_email_appended(list); - notify_email_count_changed(count, CountChangeReason.ADDED); - - // immediately add to outbox queue for delivery - outbox_queue.send(row); - - return FolderSupportsCreate.Result.CREATED; - } - - public override async Gee.List? list_email_async(int low, int count, - Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null) - throws Error { - return yield list_email_by_id_async(new OutboxEmailIdentifier(low), count, - required_fields, flags, cancellable); - } - - public override async Gee.List? list_email_by_id_async( - Geary.EmailIdentifier initial_id, int count, Geary.Email.Field required_fields, - Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error { - - OutboxEmailIdentifier? id = initial_id as OutboxEmailIdentifier; - assert(id != null); - - Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.list_email_by_id_async", - cancellable); - - Gee.List? row_list = yield local_folder.list_email_async( - transaction, id, count, cancellable); - if (row_list == null || row_list.size == 0) - return null; - - Gee.List list = new Gee.ArrayList(); - foreach (Geary.Sqlite.SmtpOutboxRow row in row_list) { - int position = yield row.get_position_async(transaction, cancellable); - list.add(outbox_email_for_row(row, position)); - } - - return list; - } - - public override async Gee.List? list_email_by_sparse_id_async( - Gee.Collection _ids, Geary.Email.Field required_fields, - Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error { - Gee.List ids = new Gee.ArrayList(); - foreach (Geary.EmailIdentifier id in _ids) { - assert(id is OutboxEmailIdentifier); - ids.add((OutboxEmailIdentifier) id); - } - - Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.list_email_by_sparse_id_async", - cancellable); - - Gee.List? row_list = yield local_folder. - list_email_by_sparse_id_async(transaction, ids, cancellable); - if (row_list == null || row_list.size == 0) - return null; - - Gee.List list = new Gee.ArrayList(); - foreach (Geary.Sqlite.SmtpOutboxRow row in row_list) { - int position = yield row.get_position_async(transaction, cancellable); - list.add(outbox_email_for_row(row, position)); - } - - return list; - } - - public override async Gee.Map? - list_local_email_fields_async(Gee.Collection ids, - Cancellable? cancellable = null) throws Error { - // Not implemented. - return null; - } - - public override async Geary.Email fetch_email_async(Geary.EmailIdentifier _id, - Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, - Cancellable? cancellable = null) throws Error { - OutboxEmailIdentifier? id = _id as OutboxEmailIdentifier; - assert(id != null); - - Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.fetch_email_async", - cancellable); - - Geary.Sqlite.SmtpOutboxRow? row = yield local_folder.fetch_email_async(transaction, id); - if (row == null) - throw new EngineError.NOT_FOUND("No message with ID %lld found in database", row.ordering); - - int position = yield row.get_position_async(transaction, cancellable); - - return outbox_email_for_row(row, position); - } - - public virtual async void remove_email_async(Gee.List email_ids, - Cancellable? cancellable = null) throws Error { - foreach (Geary.EmailIdentifier id in email_ids) - remove_single_email_async(id, cancellable); - } - - public virtual async void remove_single_email_async(Geary.EmailIdentifier _id, - Cancellable? cancellable = null) throws Error { - OutboxEmailIdentifier? id = _id as OutboxEmailIdentifier; - assert(id != null); - - Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.remove_single_email_async", - cancellable); - - yield local_folder.remove_single_email_async(transaction, id, cancellable); - - int count = yield internal_get_email_count_async(transaction, cancellable); - - Gee.ArrayList list = new Gee.ArrayList(); - list.add(id); - notify_email_removed(list); - notify_email_count_changed(count, CountChangeReason.REMOVED); - } - - // Utility for getting an email object back from an outbox row. - private Geary.Email outbox_email_for_row(Geary.Sqlite.SmtpOutboxRow row, int position) throws Error { - RFC822.Message message = new RFC822.Message.from_string(row.message); - - Geary.Email email = message.get_email(position, new OutboxEmailIdentifier(row.ordering)); - email.set_email_properties(new OutboxEmailProperties()); - email.set_flags(new Geary.EmailFlags()); - - return email; - } -} - diff --git a/src/engine/nonblocking/nonblocking-abstract-semaphore.vala b/src/engine/nonblocking/nonblocking-abstract-semaphore.vala index afd696a3..832e3519 100644 --- a/src/engine/nonblocking/nonblocking-abstract-semaphore.vala +++ b/src/engine/nonblocking/nonblocking-abstract-semaphore.vala @@ -56,8 +56,15 @@ public abstract class Geary.NonblockingAbstractSemaphore { } ~NonblockingAbstractSemaphore() { - if (pending_queue.size > 0) + if (pending_queue.size > 0) { warning("Nonblocking semaphore destroyed with %d pending callers", pending_queue.size); + + foreach (Pending pending in pending_queue) + pending.cancelled.disconnect(on_pending_cancelled); + } + + if (cancellable != null) + cancellable.cancelled.disconnect(on_cancelled); } private void trigger(bool all) { diff --git a/src/engine/sqlite/abstract/sqlite-database.vala b/src/engine/sqlite/abstract/sqlite-database.vala deleted file mode 100644 index 1e8a48cd..00000000 --- a/src/engine/sqlite/abstract/sqlite-database.vala +++ /dev/null @@ -1,85 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public abstract class Geary.Sqlite.Database { - internal SQLHeavy.VersionedDatabase db; - internal File data_dir; - internal File schema_dir; - - private Gee.HashMap table_map = new Gee.HashMap< - SQLHeavy.Table, Geary.Sqlite.Table>(); - - public signal void pre_upgrade(int version); - - public signal void post_upgrade(int version); - - public Database(File db_file, File schema_dir) throws Error { - this.schema_dir = schema_dir; - data_dir = db_file.get_parent(); - if (!data_dir.query_exists()) - data_dir.make_directory_with_parents(); - - db = new SQLHeavy.VersionedDatabase(db_file.get_path(), schema_dir.get_path()); - db.foreign_keys = true; - db.synchronous = SQLHeavy.SynchronousMode.OFF; - } - - protected Geary.Sqlite.Table? get_table(string name, out SQLHeavy.Table heavy_table) { - try { - heavy_table = db.get_table(name); - } catch (SQLHeavy.Error err) { - error("Unable to load %s: %s", name, err.message); - } - - return table_map.get(heavy_table); - } - - protected Geary.Sqlite.Table add_table(Geary.Sqlite.Table table) { - table_map.set(table.table, table); - - return table; - } - - public async Transaction begin_transaction_async(string name, Cancellable? cancellable) throws Error { - Transaction t = new Transaction(db, name); - yield t.begin_async(cancellable); - - return t; - } - - public int upgrade() throws Error { - // Get the SQLite database version. - SQLHeavy.QueryResult result = db.execute("PRAGMA user_version;"); - int db_version = result.fetch_int(); - debug("Current db version: %d", db_version); - - // Go through all the version scripts in the schema directory and apply each of them. - File upgrade_script; - while ((upgrade_script = get_upgrade_script(++db_version)).query_exists()) { - pre_upgrade(db_version); - - try { - debug("Upgrading database to to version %d at %s", db_version, upgrade_script.get_path()); - - db.run_script(upgrade_script.get_path()); - db.run("PRAGMA user_version = %d;".printf(db_version)); - } catch (Error e) { - // TODO Add rollback of changes here when switching away from SQLHeavy. - warning("Error upgrading database: %s", e.message); - throw e; - } - - post_upgrade(db_version); - } - - return db.execute("PRAGMA user_version;").fetch_int(); - } - - private File get_upgrade_script(int version) { - return schema_dir.get_child("Version-%03d.sql".printf(version)); - } -} - diff --git a/src/engine/sqlite/abstract/sqlite-row.vala b/src/engine/sqlite/abstract/sqlite-row.vala deleted file mode 100644 index b129b035..00000000 --- a/src/engine/sqlite/abstract/sqlite-row.vala +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public abstract class Geary.Sqlite.Row : Object { - public const int64 INVALID_ID = -1; - - protected Table table; - - public Row(Table table) { - this.table = table; - } - - public int fetch_int_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { - return result.fetch_int(field_index(result, col)); - } - - public int64 fetch_int64_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { - return result.fetch_int64(field_index(result, col)); - } - - public string fetch_string_for(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { - return result.fetch_string(field_index(result, col)); - } - - private int field_index(SQLHeavy.QueryResult result, int col) throws SQLHeavy.Error { - try { - return result.field_index(table.get_field_name(col)); - } catch (SQLHeavy.Error err) { - debug("Bad column #%d in %s: %s", col, table.to_string(), err.message); - - throw err; - } - } -} - diff --git a/src/engine/sqlite/abstract/sqlite-table.vala b/src/engine/sqlite/abstract/sqlite-table.vala deleted file mode 100644 index c13c5a80..00000000 --- a/src/engine/sqlite/abstract/sqlite-table.vala +++ /dev/null @@ -1,94 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public abstract class Geary.Sqlite.Table { - internal class ExecuteQueryOperation : NonblockingBatchOperation { - public SQLHeavy.Query query; - - public ExecuteQueryOperation(SQLHeavy.Query query) { - this.query = query; - } - - public override async Object? execute_async(Cancellable? cancellable) throws Error { - yield query.execute_async(); - - if (cancellable.is_cancelled()) - throw new IOError.CANCELLED("Cancelled"); - - return null; - } - } - - internal weak Geary.Sqlite.Database gdb; - internal SQLHeavy.Table table; - - internal Table(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { - this.gdb = gdb; - this.table = table; - } - - public string get_field_name(int col) throws SQLHeavy.Error { - return table.field_name(col); - } - - protected inline static int bool_to_int(bool b) { - return b ? 1 : 0; - } - - protected inline static bool int_to_bool(int i) { - return !(i == 0); - } - - protected async Transaction obtain_lock_async(Transaction? supplied_lock, string single_use_name, - Cancellable? cancellable) throws Error { - check_cancel(cancellable, "obtain_lock_async"); - - // if the user supplied the lock for multiple operations, use that - if (supplied_lock != null) { - if (!supplied_lock.is_locked) - yield supplied_lock.begin_async(cancellable); - - return supplied_lock; - } - - // create a single-use lock for the transaction - return yield begin_transaction_async(single_use_name, cancellable); - } - - // Technically this only needs to be called for locks that have a required commit. - protected async void release_lock_async(Transaction? supplied_lock, Transaction actual_lock, - Cancellable? cancellable) throws Error { - // if user supplied a lock, don't touch it - if (supplied_lock != null) - return; - - check_cancel(cancellable, "release_lock_async"); - - // only commit if required (and the lock was single-use) - if (actual_lock.is_commit_required) - yield actual_lock.commit_async(cancellable); - } - - protected async Transaction begin_transaction_async(string name, Cancellable? cancellable) - throws Error { - check_cancel(cancellable, "begin_transaction_async"); - - return yield gdb.begin_transaction_async(name, cancellable); - } - - public string to_string() { - return table.name; - } - - // Throws an exception if cancellable is valid and has been cancelled. - // This is necessary because SqlHeavy doesn't properly handle Cancellables. - // For more info see: http://code.google.com/p/sqlheavy/issues/detail?id=20 - protected void check_cancel(Cancellable? cancellable, string name) throws Error { - if (cancellable != null && cancellable.is_cancelled()) - throw new IOError.CANCELLED("Cancelled %s".printf(name)); - } -} - diff --git a/src/engine/sqlite/abstract/sqlite-transaction.vala b/src/engine/sqlite/abstract/sqlite-transaction.vala deleted file mode 100644 index 778afda9..00000000 --- a/src/engine/sqlite/abstract/sqlite-transaction.vala +++ /dev/null @@ -1,98 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.Transaction { - private static NonblockingMutex? transaction_lock = null; - private static int next_id = 0; - private static string? held_by = null; - - public bool is_locked { get { - return claim_stub != NonblockingMutex.INVALID_TOKEN; - } } - - public bool is_commit_required { get; private set; default = false; } - - private SQLHeavy.Database db; - private string name; - private int id; - private int claim_stub = NonblockingMutex.INVALID_TOKEN; - - internal Transaction(SQLHeavy.Database db, string name) throws Error { - if (transaction_lock == null) - transaction_lock = new NonblockingMutex(); - - this.db = db; - this.name = name; - id = next_id++; - } - - ~Transaction() { - if (is_locked) { - // this may be the result of a programming error, but it can also be due to an exception - // being thrown (particularly IOError.CANCELLED) when attempting an operation. - if (is_commit_required) - message("[%s] destroyed without committing or rolling back changes", to_string()); - - resolve(false, null); - } - } - - public async void begin_async(Cancellable? cancellable = null) throws Error { - assert(!is_locked); - - Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] claiming lock held by %s", to_string(), - !String.is_empty(held_by) ? held_by : "(no one)"); - claim_stub = yield transaction_lock.claim_async(cancellable); - held_by = name; - Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] lock claimed", to_string()); - } - - private void resolve(bool commit, Cancellable? cancellable) throws Error { - if (!is_locked) { - warning("[%s] attempting to resolve an unlocked transaction", to_string()); - - return; - } - - if (commit) - is_commit_required = false; - - Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] releasing lock held by %s", to_string(), - !String.is_empty(held_by) ? held_by : "(no one)"); - transaction_lock.release(ref claim_stub); - held_by = null; - Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] released lock", to_string()); - } - - public SQLHeavy.Query prepare(string sql) throws Error { - return db.prepare(sql); - } - - public async void commit_async(Cancellable? cancellable) throws Error { - resolve(true, cancellable); - } - - public async void commit_if_required_async(Cancellable? cancellable) throws Error { - if (is_commit_required) - resolve(true, cancellable); - } - - public async void rollback_async(Cancellable? cancellable) throws Error { - resolve(false, cancellable); - } - - public void set_commit_required() { - Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] commit required", to_string()); - - is_commit_required = true; - } - - public string to_string() { - return "%d %s (%s%s)".printf(id, name, is_locked ? "locked" : "unlocked", - is_commit_required ? ", commit required" : ""); - } -} - diff --git a/src/engine/sqlite/api/sqlite-account.vala b/src/engine/sqlite/api/sqlite-account.vala deleted file mode 100644 index 45bdd8c3..00000000 --- a/src/engine/sqlite/api/sqlite-account.vala +++ /dev/null @@ -1,356 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -private class Geary.Sqlite.Account : Object { - private class FolderReference : Geary.SmartReference { - public Geary.FolderPath path; - - public FolderReference(Sqlite.Folder folder, Geary.FolderPath path) { - base (folder); - - this.path = path; - } - } - - private string name; - private ImapDatabase? db = null; - private FolderTable? folder_table = null; - private ImapFolderPropertiesTable? folder_properties_table = null; - private MessageTable? message_table = null; - private SmtpOutboxTable? outbox_table = null; - private Gee.HashMap folder_refs = - new Gee.HashMap(Hashable.hash_func, Equalable.equal_func); - - public Account(string username) { - name = "SQLite account for %s".printf(username); - } - - private void check_open() throws Error { - if (db == null) - throw new EngineError.OPEN_REQUIRED("Database not open"); - } - - public async void open_async(Geary.Credentials cred, File user_data_dir, File resource_dir, - Cancellable? cancellable) throws Error { - if (db != null) - throw new EngineError.ALREADY_OPEN("IMAP database already open"); - - try { - db = new ImapDatabase(cred.user, user_data_dir, resource_dir); - db.pre_upgrade.connect(on_pre_upgrade); - db.post_upgrade.connect(on_post_upgrade); - - // upgrade and do any processing that should be done on this version of the database - process_database(db.upgrade()); - } catch (Error err) { - warning("Unable to open database: %s", err.message); - - // close database before exiting - db = null; - - throw err; - } - - folder_table = db.get_folder_table(); - folder_properties_table = db.get_imap_folder_properties_table(); - message_table = db.get_message_table(); - outbox_table = db.get_smtp_outbox_table(); - } - - public async void close_async(Cancellable? cancellable) throws Error { - if (db == null) - return; - - folder_table = null; - folder_properties_table = null; - message_table = null; - outbox_table = null; - - db = null; - } - - private async int64 fetch_id_async(Transaction? transaction, Geary.FolderPath path, - Cancellable? cancellable = null) throws Error { - check_open(); - - FolderRow? row = yield folder_table.fetch_descend_async(transaction, path.as_list(), - cancellable); - if (row == null) - throw new EngineError.NOT_FOUND("Cannot find local path to %s", path.to_string()); - - return row.id; - } - - private async int64 fetch_parent_id_async(Transaction? transaction, Geary.FolderPath path, - Cancellable? cancellable = null) throws Error { - check_open(); - - return path.is_root() ? Row.INVALID_ID : yield fetch_id_async(transaction, path.get_parent(), - cancellable); - } - - public async void clone_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null) - throws Error { - check_open(); - - Geary.Imap.FolderProperties? imap_folder_properties = imap_folder.get_properties(); - - // properties *must* be available to perform a clone - assert(imap_folder_properties != null); - - Transaction transaction = yield db.begin_transaction_async("Account.clone_folder_async", - cancellable); - - int64 folder_id = Row.INVALID_ID; - int64 parent_id = Row.INVALID_ID; - for (int index = 0; index < imap_folder.get_path().get_path_length(); index++) { - Geary.FolderPath? current_path = imap_folder.get_path().get_folder_at(index); - assert(current_path != null); - - int64 current_id = Row.INVALID_ID; - try { - current_id = yield fetch_id_async(transaction, current_path, cancellable); - } catch (Error err) { - if (!(err is EngineError.NOT_FOUND)) - throw err; - } - - if (current_id == Row.INVALID_ID) { - folder_id = yield folder_table.create_async(transaction, new FolderRow(folder_table, - current_path.basename, parent_id), cancellable); - } else { - folder_id = current_id; - } - - parent_id = folder_id; - } - - assert(folder_id != Row.INVALID_ID); - - yield folder_properties_table.create_async(transaction, - new ImapFolderPropertiesRow.from_imap_properties(folder_properties_table, folder_id, - imap_folder_properties), cancellable); - - yield transaction.commit_async(cancellable); - } - - public async void update_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null) - throws Error { - check_open(); - - Geary.Imap.FolderProperties? imap_folder_properties = (Geary.Imap.FolderProperties?) - imap_folder.get_properties(); - - // properties *must* be available - assert(imap_folder_properties != null); - - Transaction transaction = yield db.begin_transaction_async("Account.update_folder_async", - cancellable); - - int64 parent_id = yield fetch_parent_id_async(transaction, imap_folder.get_path(), cancellable); - - FolderRow? row = yield folder_table.fetch_async(transaction, parent_id, - imap_folder.get_path().basename, cancellable); - if (row == null) { - throw new EngineError.NOT_FOUND("Can't find in local store %s", - imap_folder.get_path().to_string()); - } - - yield folder_properties_table.update_async(transaction, row.id, - new ImapFolderPropertiesRow.from_imap_properties(folder_properties_table, row.id, - imap_folder_properties), cancellable); - - FolderReference? folder_ref = folder_refs.get(imap_folder.get_path()); - if (folder_ref != null) - ((Geary.Sqlite.Folder) folder_ref.get_reference()).update_properties(imap_folder_properties); - - yield transaction.commit_async(cancellable); - } - - public async Gee.Collection list_folders_async(Geary.FolderPath? parent, - Cancellable? cancellable = null) throws Error { - check_open(); - - Transaction transaction = yield db.begin_transaction_async("Account.list_folders_async", - cancellable); - - int64 parent_id = (parent != null) - ? yield fetch_id_async(transaction, parent, cancellable) - : Row.INVALID_ID; - - if (parent != null) - assert(parent_id != Row.INVALID_ID); - - Gee.List rows = yield folder_table.list_async(transaction, parent_id, cancellable); - if (rows.size == 0) { - throw new EngineError.NOT_FOUND("No local folders in %s", - (parent != null) ? parent.get_fullpath() : "root"); - } - - Gee.Collection folders = new Gee.ArrayList(); - foreach (FolderRow row in rows) { - ImapFolderPropertiesRow? properties = yield folder_properties_table.fetch_async( - transaction, row.id, cancellable); - - Geary.FolderPath path = (parent != null) - ? parent.get_child(row.name) - : new Geary.FolderRoot(row.name, "/", Geary.Imap.Folder.CASE_SENSITIVE); - - Geary.Sqlite.Folder? folder = get_sqlite_folder(path); - if (folder == null) - folder = create_sqlite_folder(row, - (properties != null) ? properties.get_imap_folder_properties() : null, path); - - folders.add(folder); - } - - return folders; - } - - public async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null) - throws Error { - check_open(); - - try { - int64 id = yield fetch_id_async(null, path, cancellable); - - return (id != Row.INVALID_ID); - } catch (EngineError err) { - if (err is EngineError.NOT_FOUND) - return false; - else - throw err; - } - } - - public async Geary.Sqlite.Folder fetch_folder_async(Geary.FolderPath path, - Cancellable? cancellable = null) throws Error { - check_open(); - - // check references table first - Geary.Sqlite.Folder? folder = get_sqlite_folder(path); - if (folder != null) - return folder; - - Transaction transaction = yield db.begin_transaction_async("Account.fetch_folder_async", - cancellable); - - // locate in database - FolderRow? row = yield folder_table.fetch_descend_async(transaction, path.as_list(), - cancellable); - if (row == null) - throw new EngineError.NOT_FOUND("%s not found in local database", path.to_string()); - - // fetch it's IMAP-specific properties - ImapFolderPropertiesRow? properties = yield folder_properties_table.fetch_async( - transaction, row.id, cancellable); - - return create_sqlite_folder(row, - (properties != null) ? properties.get_imap_folder_properties() : null, path); - } - - private Geary.Sqlite.Folder? get_sqlite_folder(Geary.FolderPath path) { - FolderReference? folder_ref = folder_refs.get(path); - - return (folder_ref != null) ? (Geary.Sqlite.Folder) folder_ref.get_reference() : null; - } - - public SmtpOutboxTable get_outbox() { - return outbox_table; - } - - private Geary.Sqlite.Folder create_sqlite_folder(FolderRow row, Imap.FolderProperties? properties, - Geary.FolderPath path) throws Error { - check_open(); - - // create folder - Geary.Sqlite.Folder folder = new Geary.Sqlite.Folder(db, row, properties, path); - - // build a reference to it - FolderReference folder_ref = new FolderReference(folder, path); - folder_ref.reference_broken.connect(on_folder_reference_broken); - - // add to the references table - folder_refs.set(folder_ref.path, folder_ref); - - return folder; - } - - private void on_folder_reference_broken(Geary.SmartReference reference) { - FolderReference folder_ref = (FolderReference) reference; - - // drop from folder references table, all cleaned up - folder_refs.unset(folder_ref.path); - } - - private void on_pre_upgrade(int version){ - // TODO Add per-version data massaging. - } - - private void on_post_upgrade(int version) { - // TODO Add per-version data massaging. - } - - // Called every run after executing db.upgrade(); this gives a chance to perform work that - // cannot be easily expressed in an upgrade script and should happen whether an upgrade to that - // version has happened or not - private void process_database(int version) { - switch (version) { - case 3: - try { - clear_duplicate_folders(); - } catch (SQLHeavy.Error err) { - debug("Unable to clear duplicate folders in version %d: %s", version, err.message); - } - break; - - default: - // nothing to do - break; - } - } - - private void clear_duplicate_folders() throws SQLHeavy.Error { - int count = 0; - - // Find all folders with duplicate names - SQLHeavy.Query dupe_name_query = db.db.prepare( - "SELECT id, name FROM FolderTable WHERE name IN " - + "(SELECT name FROM FolderTable GROUP BY name HAVING (COUNT(name) > 1))"); - SQLHeavy.QueryResult result = dupe_name_query.execute(); - while (!result.finished) { - int64 id = result.fetch_int64(0); - - // see if any folders have this folder as a parent OR if there are messages associated - // with this folder - SQLHeavy.Query child_query = db.db.prepare( - "SELECT id FROM FolderTable WHERE parent_id=?"); - child_query.bind_int64(0, id); - SQLHeavy.QueryResult child_result = child_query.execute(); - - SQLHeavy.Query message_query = db.db.prepare( - "SELECT id FROM MessageLocationTable WHERE folder_id=?"); - message_query.bind_int64(0, id); - SQLHeavy.QueryResult message_result = message_query.execute(); - - if (child_result.finished && message_result.finished) { - // no children and no messages, delete it - SQLHeavy.Query child_delete = db.db.prepare( - "DELETE FROM FolderTable WHERE id=?"); - child_delete.bind_int64(0, id); - - child_delete.execute(); - count++; - } - - result.next(); - } - - if (count > 0) - debug("Deleted %d duplicate folders", count); - } -} - diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala deleted file mode 100644 index c948e729..00000000 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ /dev/null @@ -1,777 +0,0 @@ -/* Copyright 2011-2012 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. - -private class Geary.Sqlite.Folder : Object, Geary.ReferenceSemantics { - public const Geary.Email.Field REQUIRED_FOR_DUPLICATE_DETECTION = Geary.Email.Field.PROPERTIES; - - [Flags] - public enum ListFlags { - NONE = 0, - PARTIAL_OK, - INCLUDE_MARKED_FOR_REMOVE, - EXCLUDING_ID; - - public bool is_all_set(ListFlags flags) { - return (this & flags) == flags; - } - - public bool is_any_set(ListFlags flags) { - return (this & flags) != 0; - } - } - - public bool opened { get; private set; default = false; } - - protected int manual_ref_count { get; protected set; } - - private ImapDatabase db; - private FolderRow folder_row; - private Geary.Imap.FolderProperties properties; - private MessageTable message_table; - private MessageLocationTable location_table; - private MessageAttachmentTable attachment_table; - private ImapMessagePropertiesTable imap_message_properties_table; - private Geary.FolderPath path; - - internal Folder(ImapDatabase db, FolderRow folder_row, Geary.Imap.FolderProperties properties, - Geary.FolderPath path) throws Error { - this.db = db; - this.folder_row = folder_row; - this.properties = properties; - this.path = path; - - message_table = db.get_message_table(); - location_table = db.get_message_location_table(); - attachment_table = db.get_message_attachment_table(); - imap_message_properties_table = db.get_imap_message_properties_table(); - } - - private void check_open() throws Error { - if (!opened) - throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); - } - - public Geary.FolderPath get_path() { - return path; - } - - public Geary.Imap.FolderProperties get_properties() { - return properties; - } - - internal void update_properties(Geary.Imap.FolderProperties properties) { - // TODO: TBD: alteration/updated signals for folders - this.properties = properties; - } - - 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; - - // Possible some messages were marked for removal in prior session and not properly deleted - // (for example, if the EXPUNGE message didn't come back before closing the folder, or - // the app is shut down or crashes) ... folder normalization will take care of messages that - // are still in folder even if they're marked here, so go ahead and remove them from the - // location table - yield location_table.remove_all_marked_for_remove(null, folder_row.id, cancellable); - } - - public async void close_async(Cancellable? cancellable = null) throws Error { - if (!opened) - return; - - opened = false; - } - - public async int get_email_count_async(Cancellable? cancellable = null) throws Error { - return yield internal_get_email_count_async(null, false, cancellable); - } - - public async int get_email_count_including_marked_async(Cancellable? cancellable = null) - throws Error { - return yield internal_get_email_count_async(null, true, cancellable); - } - - private async int internal_get_email_count_async(Transaction? transaction, bool include_marked, - Cancellable? cancellable) throws Error { - check_open(); - - // TODO: This can be cached and updated when changes occur - return yield location_table.fetch_count_for_folder_async(transaction, folder_row.id, - include_marked, cancellable); - } - - public async int get_id_position_async(Geary.EmailIdentifier id, Cancellable? cancellable) - throws Error { - check_open(); - - Transaction transaction = yield db.begin_transaction_async("Folder.get_id_position_async", - cancellable); - - int64 message_id; - if (!yield location_table.does_ordering_exist_async(transaction, folder_row.id, - id.ordering, out message_id, cancellable)) { - return -1; - } - - return yield location_table.fetch_message_position_async(transaction, message_id, folder_row.id, - cancellable); - } - - public async bool create_email_async(Geary.Email email, Cancellable? cancellable = null) - throws Error { - return yield atomic_create_email_async(null, email, cancellable); - } - - // TODO: Need to break out IMAP-specific functionality - private async int64 search_for_duplicate_async(Transaction transaction, Geary.Email email, - Cancellable? cancellable) throws Error { - // if fields not present, then no duplicate can reliably be found - if (!email.fields.is_all_set(REQUIRED_FOR_DUPLICATE_DETECTION)) - return Sqlite.Row.INVALID_ID; - - // 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 location_table.does_ordering_exist_async(transaction, folder_row.id, - email.id.ordering, out message_id, cancellable)) { - return message_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.original : null; - long rfc822_size = (imap_properties != null && imap_properties.rfc822_size != null) - ? imap_properties.rfc822_size.value : -1; - - if (String.is_empty(internaldate) || rfc822_size < 0) - return Sqlite.Row.INVALID_ID; - - // reset - message_id = Sqlite.Row.INVALID_ID; - - // look for duplicate in IMAP message properties - Gee.List? duplicate_ids = yield imap_message_properties_table.search_for_duplicates_async( - transaction, internaldate, rfc822_size, cancellable); - if (duplicate_ids != null && duplicate_ids.size > 0) { - if (duplicate_ids.size > 1) { - debug("Warning: Multiple messages with the same internaldate (%s) and size (%lu) found in %s", - internaldate, rfc822_size, to_string()); - message_id = duplicate_ids[0]; - } else if (duplicate_ids.size == 1) { - message_id = duplicate_ids[0]; - } - } - - return message_id; - } - - // Returns false if the message already exists at the specified position - private async bool associate_with_folder_async(Transaction transaction, int64 message_id, - Geary.Email email, Cancellable? cancellable) throws Error { - // see if an email exists at this position - MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async(transaction, - folder_row.id, email.id.ordering, cancellable); - if (location_row != null) - return false; - - // insert email at supplied position - location_row = new MessageLocationRow(location_table, Row.INVALID_ID, message_id, - folder_row.id, email.id.ordering, email.position); - yield location_table.create_async(transaction, location_row, cancellable); - - return true; - } - - private async bool atomic_create_email_async(Transaction? supplied_transaction, Geary.Email email, - Cancellable? cancellable) throws Error { - check_open(); - - Transaction transaction = supplied_transaction ?? yield db.begin_transaction_async( - "Folder.atomic_create_email_async", cancellable); - - // See if this Email is already associated with the folder - int64 message_id; - bool associated = yield location_table.does_ordering_exist_async(transaction, folder_row.id, - email.id.ordering, out message_id, cancellable); - - // if duplicate found, associate this email with this folder and merge in any new details - if (!associated || message_id == Sqlite.Row.INVALID_ID) - message_id = yield search_for_duplicate_async(transaction, email, cancellable); - - // if already associated or a duplicate, merge and/or associate - if (message_id != Sqlite.Row.INVALID_ID) { - if (!associated) { - if (!yield associate_with_folder_async(transaction, message_id, email, cancellable)) { - debug("Warning: Unable to associate %s (%lld) with %s", email.id.to_string(), message_id, - to_string()); - } - } - - yield merge_email_async(transaction, message_id, email, cancellable); - - if (supplied_transaction == null) - yield transaction.commit_if_required_async(cancellable); - - return false; - } - - // not found, so create and associate with this folder - message_id = yield message_table.create_async(transaction, - new MessageRow.from_email(message_table, email), cancellable); - - // create the message location in the location lookup table - MessageLocationRow location_row = new MessageLocationRow(location_table, Row.INVALID_ID, - message_id, folder_row.id, email.id.ordering, email.position); - yield location_table.create_async(transaction, location_row, cancellable); - - // Also add attachments if we have them. - if (email.fields.fulfills(Attachment.REQUIRED_FIELDS)) { - Gee.List attachments = email.get_message().get_attachments(); - yield save_attachments_async(transaction, attachments, message_id, cancellable); - } - - // only write out the IMAP email properties if they're supplied and there's something to - // write out -- no need to create an empty row - Geary.Imap.EmailProperties? properties = (Geary.Imap.EmailProperties?) email.properties; - Geary.Imap.EmailFlags? email_flags = (Geary.Imap.EmailFlags?) email.email_flags; - if (email.fields.is_any_set(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS)) { - ImapMessagePropertiesRow properties_row = new ImapMessagePropertiesRow.from_imap_properties( - imap_message_properties_table, message_id, properties, - (email_flags != null) ? email_flags.message_flags : null); - - yield imap_message_properties_table.create_async(transaction, properties_row, cancellable); - } - - // only commit if not supplied a transaction - if (supplied_transaction == null) - yield transaction.commit_async(cancellable); - - return true; - } - - public async Gee.List? list_email_async(int low, int count, - Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable) throws Error { - check_open(); - - Transaction transaction = yield db.begin_transaction_async("Folder.list_email_async", - cancellable); - - int local_count = yield internal_get_email_count_async(transaction, - flags.is_all_set(ListFlags.INCLUDE_MARKED_FOR_REMOVE), cancellable); - Geary.Folder.normalize_span_specifiers(ref low, ref count, local_count); - - if (count == 0) - return null; - - Gee.List? list = yield location_table.list_async(transaction, - folder_row.id, low, count, flags.is_all_set(ListFlags.INCLUDE_MARKED_FOR_REMOVE), - cancellable); - - return yield do_list_email_async(transaction, list, required_fields, flags, cancellable); - } - - public async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, - int count, Geary.Email.Field required_fields, Folder.ListFlags flags, - Cancellable? cancellable = null) throws Error { - if (count == 0 || count == 1) { - try { - Geary.Email email = yield fetch_email_async(initial_id, required_fields, flags, - cancellable); - - Gee.List singleton = new Gee.ArrayList(); - singleton.add(email); - - return singleton; - } catch (EngineError engine_err) { - // list_email variants don't return NOT_FOUND or INCOMPLETE_MESSAGE - if ((engine_err is EngineError.NOT_FOUND) || (engine_err is EngineError.INCOMPLETE_MESSAGE)) - return null; - - throw engine_err; - } - } - - check_open(); - - Geary.Imap.UID uid = ((Geary.Imap.EmailIdentifier) initial_id).uid; - bool excluding_id = flags.is_all_set(ListFlags.EXCLUDING_ID); - - Transaction transaction = yield db.begin_transaction_async("Folder.list_email_by_id_async", - cancellable); - - int64 low, high; - if (count < 0) { - high = excluding_id ? uid.value - 1 : uid.value; - low = (count != int.MIN) ? (high + count).clamp(1, uint32.MAX) : -1; - } else { - // count > 1 - low = excluding_id ? uid.value + 1 : uid.value; - high = (count != int.MAX) ? (low + count).clamp(1, uint32.MAX) : -1; - } - - Gee.List? list = yield location_table.list_ordering_async(transaction, - folder_row.id, low, high, cancellable); - - return yield do_list_email_async(transaction, list, required_fields, flags, cancellable); - } - - private async Gee.List? do_list_email_async(Transaction transaction, - Gee.List? list, Geary.Email.Field required_fields, - ListFlags flags, 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) { - try { - emails.add(yield location_to_email_async(transaction, location_row, required_fields, - flags, cancellable)); - } catch (EngineError error) { - if (error is EngineError.NOT_FOUND) { - debug("WARNING: Message not found, dropping: %s", error.message); - } else if (!(error is EngineError.INCOMPLETE_MESSAGE)) { - throw error; - } - } - } - - return (emails.size > 0) ? emails : null; - } - - public async Geary.Email fetch_email_async(Geary.EmailIdentifier id, - Geary.Email.Field required_fields, ListFlags flags, Cancellable? cancellable = null) throws Error { - check_open(); - - Geary.Imap.UID uid = ((Imap.EmailIdentifier) id).uid; - - Transaction transaction = yield db.begin_transaction_async("Folder.fetch_email_async", - cancellable); - - MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async(transaction, - folder_row.id, uid.value, cancellable); - if (location_row == null) { - throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(), - to_string()); - } - - return yield location_to_email_async(transaction, location_row, required_fields, flags, - cancellable); - } - - public async Geary.Email location_to_email_async(Transaction transaction, - MessageLocationRow location_row, Geary.Email.Field required_fields, ListFlags flags, - Cancellable? cancellable = null) throws Error { - - // Prepare our IDs and flags. - Geary.Imap.UID uid = new Geary.Imap.UID(location_row.ordering); - Geary.Imap.EmailIdentifier id = new Geary.Imap.EmailIdentifier(uid); - bool partial_ok = flags.is_all_set(ListFlags.PARTIAL_OK); - bool include_removed = flags.is_all_set(ListFlags.INCLUDE_MARKED_FOR_REMOVE); - - // PROPERTIES and FLAGS are held in separate table from messages, pull from MessageTable - // only if something is needed from there - Geary.Email.Field message_fields = - required_fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS); - - int position = yield location_row.get_position_async(transaction, include_removed, cancellable); - if (position == -1) { - throw new EngineError.NOT_FOUND("Unable to determine position of email %s in %s", - id.to_string(), to_string()); - } - - // loopback on perverse case - if (required_fields == Geary.Email.Field.NONE) - return new Geary.Email(position, id); - - // Only fetch message row if we have fields other than Properties and Flags - MessageRow? message_row = null; - if (message_fields != Geary.Email.Field.NONE) { - message_row = yield message_table.fetch_async(transaction, location_row.message_id, - message_fields, cancellable); - if (message_row == null) { - throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(), - to_string()); - } - - // see if the message row fulfills everything but properties, which are held in - // separate table - if (!partial_ok && !message_row.fields.fulfills(message_fields)) { - throw new EngineError.INCOMPLETE_MESSAGE( - "Message %s in folder %s only fulfills %Xh fields (required: %Xh)", - id.to_string(), to_string(), message_row.fields, required_fields); - } - } - - ImapMessagePropertiesRow? properties = null; - if (required_fields.is_any_set(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS)) { - properties = yield imap_message_properties_table.fetch_async(transaction, - location_row.message_id, cancellable); - if (!partial_ok && properties == null) { - throw new EngineError.INCOMPLETE_MESSAGE( - "Message %s in folder %s does not have PROPERTIES and/or FLAGS fields", - id.to_string(), to_string()); - } - } - - Geary.Email email = message_row != null - ? message_row.to_email(position, id) - : new Geary.Email(position, id); - - if (properties != null) { - if (required_fields.require(Geary.Email.Field.PROPERTIES)) { - Imap.EmailProperties? email_properties = properties.get_imap_email_properties(); - if (email_properties != null) { - email.set_email_properties(email_properties); - } else if (!partial_ok) { - throw new EngineError.INCOMPLETE_MESSAGE( - "Message %s in folder %s does not have PROPERTIES fields", id.to_string(), - to_string()); - } - } - - if (required_fields.require(Geary.Email.Field.FLAGS)) { - EmailFlags? email_flags = properties.get_email_flags(); - if (email_flags != null) { - email.set_flags(email_flags); - } else if (!partial_ok) { - throw new EngineError.INCOMPLETE_MESSAGE( - "Message %s in folder %s does not have FLAGS fields", id.to_string(), - to_string()); - } - } - } - - // Load the attachments as well if we have the full message. - if (required_fields.fulfills(Geary.Attachment.REQUIRED_FIELDS)) { - Gee.List attachments = yield attachment_table.list_async( - transaction, location_row.message_id, cancellable); - foreach (MessageAttachmentRow row in attachments) { - email.add_attachment(row.to_attachment()); - } - } - - return email; - - } - - public async Geary.Imap.UID? get_earliest_uid_async(Cancellable? cancellable = null) throws Error { - return yield get_uid_extremes_async(true, cancellable); - } - - public async Geary.Imap.UID? get_latest_uid_async(Cancellable? cancellable = null) throws Error { - return yield get_uid_extremes_async(false, cancellable); - } - - private async Geary.Imap.UID? get_uid_extremes_async(bool earliest, Cancellable? cancellable) - throws Error { - check_open(); - - int64 ordering = yield location_table.get_ordering_extremes_async(null, folder_row.id, - earliest, cancellable); - - return (ordering >= 1) ? new Geary.Imap.UID(ordering) : null; - } - - public async void remove_single_email_async(Geary.EmailIdentifier email_id, - Cancellable? cancellable = null) throws Error { - // 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 - if (!yield location_table.remove_by_ordering_async(null, folder_row.id, email_id.ordering, - cancellable)) { - throw new EngineError.NOT_FOUND("Message %s not found in %s", email_id.to_string(), - to_string()); - } - } - - public async void mark_email_async( - Gee.List to_mark, Geary.EmailFlags? flags_to_add, - Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error { - - Gee.Map map = yield get_email_flags_async( - to_mark, cancellable); - - foreach (Geary.EmailIdentifier id in map.keys) { - if (flags_to_add != null) - foreach (Geary.EmailFlag flag in flags_to_add.get_all()) - ((Geary.Imap.EmailFlags) map.get(id)).add(flag); - - if (flags_to_remove != null) - foreach (Geary.EmailFlag flag in flags_to_remove.get_all()) - ((Geary.Imap.EmailFlags) map.get(id)).remove(flag); - } - - yield set_email_flags_async(map, cancellable); - } - - public async Gee.Map get_email_flags_async( - Gee.List to_get, Cancellable? cancellable) throws Error { - - Gee.Map map = new Gee.HashMap< - Geary.EmailIdentifier, Geary.EmailFlags>(Hashable.hash_func, Equalable.equal_func); - - Transaction transaction = yield db.begin_transaction_async("Folder.get_email_flags_async", - cancellable); - - foreach (Geary.EmailIdentifier id in to_get) { - MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async( - transaction, folder_row.id, ((Geary.Imap.EmailIdentifier) id).uid.value, - cancellable); - - if (location_row == null) { - throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(), - to_string()); - } - - ImapMessagePropertiesRow? row = yield imap_message_properties_table.fetch_async( - transaction, location_row.message_id, cancellable); - if (row == null) - continue; - - EmailFlags? email_flags = row.get_email_flags(); - if (email_flags != null) - map.set(id, email_flags); - } - - yield transaction.commit_async(cancellable); - - return map; - } - - public async void set_email_flags_async(Gee.Map map, Cancellable? cancellable) throws Error { - check_open(); - - Transaction transaction = yield db.begin_transaction_async("Folder.set_email_flags_async", - cancellable); - - foreach (Geary.EmailIdentifier id in map.keys) { - MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async( - transaction, folder_row.id, ((Geary.Imap.EmailIdentifier) id).uid.value, cancellable); - if (location_row == null) { - throw new EngineError.NOT_FOUND("No message with ID %s in folder %s", id.to_string(), - to_string()); - } - - Geary.Imap.MessageFlags flags = ((Geary.Imap.EmailFlags) map.get(id)).message_flags; - - yield imap_message_properties_table.update_flags_async(transaction, location_row.message_id, - flags.serialize(), cancellable); - } - - yield transaction.commit_async(cancellable); - } - - public async bool is_email_present_async(Geary.EmailIdentifier id, out Geary.Email.Field available_fields, - Cancellable? cancellable = null) throws Error { - check_open(); - - Geary.Imap.UID uid = ((Imap.EmailIdentifier) id).uid; - - available_fields = Geary.Email.Field.NONE; - - Transaction transaction = yield db.begin_transaction_async("Folder.is_email_present", - cancellable); - - MessageLocationRow? location_row = yield location_table.fetch_by_ordering_async(transaction, - folder_row.id, uid.value, cancellable); - if (location_row == null) - return false; - - return yield message_table.fetch_fields_async(transaction, location_row.message_id, - out available_fields, cancellable); - } - - private async void merge_email_async(Transaction transaction, 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; - - // Only merge with MessageTable if has fields applicable to it - if (email.fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS) != 0) { - MessageRow? message_row = yield message_table.fetch_async(transaction, message_id, - (email.fields | Attachment.REQUIRED_FIELDS), cancellable); - - assert(message_row != null); - Geary.Email.Field db_fields = message_row.fields; - message_row.merge_from_remote(email); - - // Get the combined email from the merge which will be used below to save the attachments. - Geary.Email combined_email = message_row.to_email(email.position, email.id); - - // Next see if all the fields we've received are already in the DB. If they are then - // there is nothing for us to do. - if ((db_fields & email.fields) != email.fields) { - yield message_table.merge_async(transaction, message_row, cancellable); - - // Also update the saved attachments if we don't already have them in the database - // and between the database and the new fields we have what is required. - if (!db_fields.fulfills(Attachment.REQUIRED_FIELDS) && - combined_email.fields.fulfills(Attachment.REQUIRED_FIELDS)) { - yield save_attachments_async(transaction, - combined_email.get_message().get_attachments(), message_id, cancellable); - } - } - } - - // update IMAP properties - if (email.fields.fulfills(Geary.Email.Field.PROPERTIES)) { - Geary.Imap.EmailProperties properties = (Geary.Imap.EmailProperties) email.properties; - string? internaldate = - (properties.internaldate != null) ? properties.internaldate.original : null; - long rfc822_size = - (properties.rfc822_size != null) ? properties.rfc822_size.value : -1; - - yield imap_message_properties_table.update_properties_async(transaction, message_id, - internaldate, rfc822_size, cancellable); - } - - // update IMAP flags - if (email.fields.fulfills(Geary.Email.Field.FLAGS)) { - string? flags = ((Geary.Imap.EmailFlags) email.email_flags).message_flags.serialize(); - - yield imap_message_properties_table.update_flags_async(transaction, message_id, - flags, cancellable); - } - } - - private async void save_attachments_async(Transaction transaction, - Gee.List attachments, int64 message_id, Cancellable? cancellable = null) - throws Error { - - // Nothing to do if no attachments. - if (attachments.size == 0){ - return; - } - - foreach (GMime.Part attachment in attachments) { - // Get the info about the attachment. - string? filename = attachment.get_filename(); - string mime_type = attachment.get_content_type().to_string(); - - // Convert the attachment content into a usable ByteArray. - GMime.DataWrapper attachment_data = attachment.get_content_object(); - ByteArray byte_array = new ByteArray(); - GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array); - stream.set_owner(false); - attachment_data.write_to_stream(stream); - uint filesize = byte_array.len; - - // Insert it into the database. - MessageAttachmentRow attachment_row = new MessageAttachmentRow(attachment_table, 0, - message_id, filename, mime_type, filesize); - int64 attachment_id = yield attachment_table.create_async(transaction, attachment_row, - cancellable); - - try { - // Create the file where the attachment will be saved and get the output stream. - string saved_name = Attachment.get_path(db.data_dir, message_id, attachment_id, - filename); - debug("Saving attachment to %s", saved_name); - File saved_file = File.new_for_path(saved_name); - saved_file.get_parent().make_directory_with_parents(); - FileOutputStream saved_stream = yield saved_file.create_async( - FileCreateFlags.REPLACE_DESTINATION, Priority.DEFAULT, cancellable); - - // Save the data to disk and flush it. - yield saved_stream.write_async(byte_array.data[0:filesize], Priority.DEFAULT, - cancellable); - yield saved_stream.flush_async(); - } catch (Error error) { - // An error occurred while saving the attachment, so lets remove the attachment from - // the database. - // TODO Use SQLite transactions here and do a rollback. - debug("Failed to save attachment: %s", error.message); - yield attachment_table.remove_async(transaction, attachment_id, cancellable); - throw error; - } - } - } - - public async void remove_marked_email_async(Geary.EmailIdentifier id, out bool marked, - Cancellable? cancellable) throws Error { - check_open(); - - Transaction transaction = yield db.begin_transaction_async( - "Folder.remove_marked_email_async", cancellable); - - // Get marked status. - marked = yield location_table.is_marked_removed_async(transaction, folder_row.id, - id.ordering, cancellable); - - // Detaching email's association with a folder. - if (!yield location_table.remove_by_ordering_async(transaction, folder_row.id, - id.ordering, cancellable)) { - throw new EngineError.NOT_FOUND("Message %s in local store of %s not found", - id.to_string(), to_string()); - } - - yield transaction.commit_async(cancellable); - } - - public async void mark_removed_async(Geary.EmailIdentifier id, bool remove, - Cancellable? cancellable) throws Error { - check_open(); - - Transaction transaction = yield db.begin_transaction_async("Folder.mark_removed_async", - cancellable); - - yield location_table.mark_removed_async(transaction, folder_row.id, id.ordering, - remove, cancellable); - yield transaction.commit_async(cancellable); - } - - public async Gee.Map? list_email_fields_by_id_async( - Gee.Collection ids, Cancellable? cancellable) throws Error { - check_open(); - - if (ids.size == 0) - return null; - - Gee.HashMap map = new Gee.HashMap< - Geary.EmailIdentifier, Geary.Email.Field>(Hashable.hash_func, Equalable.equal_func); - - Transaction transaction = yield db.begin_transaction_async("get_email_fields_by_id_async", - cancellable); - - foreach (Geary.EmailIdentifier id in ids) { - MessageLocationRow? row = yield location_table.fetch_by_ordering_async(transaction, - folder_row.id, ((Geary.Imap.EmailIdentifier) id).uid.value, cancellable); - if (row == null) - continue; - - Geary.Email.Field fields; - if (yield message_table.fetch_fields_async(transaction, row.message_id, out fields, - cancellable)) { - map.set(id, fields); - } - } - - return (map.size > 0) ? map : null; - } - - public string to_string() { - return path.to_string(); - } -} - diff --git a/src/engine/sqlite/email/sqlite-folder-row.vala b/src/engine/sqlite/email/sqlite-folder-row.vala deleted file mode 100644 index 5f1be0c2..00000000 --- a/src/engine/sqlite/email/sqlite-folder-row.vala +++ /dev/null @@ -1,28 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.FolderRow : Geary.Sqlite.Row { - public int64 id { get; private set; } - public string name { get; private set; } - public int64 parent_id { get; private set; } - - public FolderRow(FolderTable table, string name, int64 parent_id) { - base (table); - - this.id = INVALID_ID; - this.name = name; - this.parent_id = parent_id; - } - - public FolderRow.from_query_result(FolderTable table, SQLHeavy.QueryResult result) throws Error { - base (table); - - id = fetch_int64_for(result, FolderTable.Column.ID); - name = fetch_string_for(result, FolderTable.Column.NAME); - parent_id = fetch_int64_for(result, FolderTable.Column.PARENT_ID); - } -} - diff --git a/src/engine/sqlite/email/sqlite-folder-table.vala b/src/engine/sqlite/email/sqlite-folder-table.vala deleted file mode 100644 index 301566b7..00000000 --- a/src/engine/sqlite/email/sqlite-folder-table.vala +++ /dev/null @@ -1,135 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table { - // This *must* match the column order in the database - public enum Column { - ID, - NAME, - PARENT_ID - } - - internal FolderTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { - base (gdb, table); - } - - public async int64 create_async(Transaction? transaction, FolderRow row, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "FolderTable.create_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "INSERT INTO FolderTable (name, parent_id) VALUES (?, ?)"); - query.bind_string(0, row.name); - if (row.parent_id != Row.INVALID_ID) - query.bind_int64(1, row.parent_id); - else - query.bind_null(1); - - int64 id = yield query.execute_insert_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - - return id; - } - - public async Gee.List list_async(Transaction? transaction, int64 parent_id, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "FolderTable.list_async", - cancellable); - - SQLHeavy.Query query; - if (parent_id != Row.INVALID_ID) { - query = locked.prepare("SELECT * FROM FolderTable WHERE parent_id=?"); - query.bind_int64(0, parent_id); - } else { - query = locked.prepare("SELECT * FROM FolderTable WHERE parent_id IS NULL"); - } - - SQLHeavy.QueryResult result = yield query.execute_async(); - check_cancel(cancellable, "list_async"); - - Gee.List rows = new Gee.ArrayList(); - while (!result.finished) { - rows.add(new FolderRow.from_query_result(this, result)); - - yield result.next_async(); - check_cancel(cancellable, "list_async"); - } - - return rows; - } - - public async FolderRow? fetch_async(Transaction? transaction, int64 parent_id, - string name, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "FolderTable.fetch_async", - cancellable); - - SQLHeavy.Query query; - if (parent_id != Row.INVALID_ID) { - query = locked.prepare("SELECT * FROM FolderTable WHERE parent_id=? AND name=?"); - query.bind_int64(0, parent_id); - query.bind_string(1, name); - } else { - query = locked.prepare("SELECT * FROM FolderTable WHERE name=? AND parent_id IS NULL"); - query.bind_string(0, name); - } - - SQLHeavy.QueryResult result = yield query.execute_async(); - check_cancel(cancellable, "fetch_async"); - - return (!result.finished) ? new FolderRow.from_query_result(this, result) : null; - } - - public async FolderRow? fetch_descend_async(Transaction? transaction, - Gee.List path, Cancellable? cancellable) throws Error { - assert(path.size > 0); - - Transaction locked = yield obtain_lock_async(transaction, "FolderTable.fetch_descend_async", - cancellable); - - int64 parent_id = Row.INVALID_ID; - - // walk the folder tree to the final node (which is at length - 1 - 1) - int length = path.size; - for (int ctr = 0; ctr < length - 1; ctr++) { - SQLHeavy.Query query; - if (parent_id != Row.INVALID_ID) { - query = locked.prepare( - "SELECT id FROM FolderTable WHERE parent_id=? AND name=?"); - query.bind_int64(0, parent_id); - query.bind_string(1, path[ctr]); - } else { - query = locked.prepare( - "SELECT id FROM FolderTable WHERE parent_id IS NULL AND name=?"); - query.bind_string(0, path[ctr]); - } - - SQLHeavy.QueryResult result = yield query.execute_async(); - check_cancel(cancellable, "fetch_descend_async"); - if (result.finished) - return null; - - int64 id = result.fetch_int64(0); - - // watch for loops, real bad if it happens ... could be more thorough here, but at least - // one level of checking is better than none - if (id == parent_id) { - warning("Loop found in database: parent of %lld is %lld in FolderTable", - parent_id, id); - - return null; - } - - parent_id = id; - } - - // do full fetch on this folder - return yield fetch_async(locked, parent_id, path.last(), cancellable); - } -} - diff --git a/src/engine/sqlite/email/sqlite-mail-database.vala b/src/engine/sqlite/email/sqlite-mail-database.vala deleted file mode 100644 index 32f19365..00000000 --- a/src/engine/sqlite/email/sqlite-mail-database.vala +++ /dev/null @@ -1,61 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -private class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database { - public const string FILENAME = "geary.db"; - - public MailDatabase(string user, File user_data_dir, File resource_dir) throws Error { - base (user_data_dir.get_child(user).get_child(FILENAME), resource_dir.get_child("sql")); - } - - public Geary.Sqlite.FolderTable get_folder_table() { - SQLHeavy.Table heavy_table; - FolderTable? folder_table = get_table("FolderTable", out heavy_table) as FolderTable; - - return (folder_table != null) - ? folder_table - : (FolderTable) add_table(new FolderTable(this, heavy_table)); - } - - public Geary.Sqlite.MessageTable get_message_table() { - SQLHeavy.Table heavy_table; - MessageTable? message_table = get_table("MessageTable", out heavy_table) as MessageTable; - - return (message_table != null) - ? message_table - : (MessageTable) add_table(new MessageTable(this, heavy_table)); - } - - public Geary.Sqlite.MessageLocationTable get_message_location_table() { - SQLHeavy.Table heavy_table; - MessageLocationTable? location_table = get_table("MessageLocationTable", out heavy_table) - as MessageLocationTable; - - return (location_table != null) - ? location_table - : (MessageLocationTable) add_table(new MessageLocationTable(this, heavy_table)); - } - - public Geary.Sqlite.MessageAttachmentTable get_message_attachment_table() { - SQLHeavy.Table heavy_table; - MessageAttachmentTable? attachment_table = get_table("MessageAttachmentTable", out heavy_table) - as MessageAttachmentTable; - - return (attachment_table != null) - ? attachment_table - : (MessageAttachmentTable) add_table(new MessageAttachmentTable(this, heavy_table)); - } - - public Geary.Sqlite.SmtpOutboxTable get_smtp_outbox_table() { - SQLHeavy.Table heavy_table; - SmtpOutboxTable? outbox_table = get_table("OutboxTable", out heavy_table) as SmtpOutboxTable; - - return (outbox_table != null) - ? outbox_table - : (SmtpOutboxTable) add_table(new SmtpOutboxTable(this, heavy_table)); - } -} - diff --git a/src/engine/sqlite/email/sqlite-message-attachment-row.vala b/src/engine/sqlite/email/sqlite-message-attachment-row.vala deleted file mode 100644 index 1f7a5703..00000000 --- a/src/engine/sqlite/email/sqlite-message-attachment-row.vala +++ /dev/null @@ -1,39 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.MessageAttachmentRow : Geary.Sqlite.Row { - public int64 id { get; private set; } - public int64 message_id { get; private set; } - public int64 filesize { get; private set; } - public string? filename { get; private set; } - public string mime_type { get; private set; } - - public MessageAttachmentRow(MessageAttachmentTable table, int64 id, int64 message_id, - string? filename, string mime_type, int64 filesize) { - base (table); - - this.id = id; - this.message_id = message_id; - this.filename = filename; - this.mime_type = mime_type; - this.filesize = filesize; - } - - public MessageAttachmentRow.from_query_result(MessageAttachmentTable table, - SQLHeavy.QueryResult result) throws Error { - base (table); - - id = fetch_int64_for(result, MessageAttachmentTable.Column.ID); - message_id = fetch_int64_for(result, MessageAttachmentTable.Column.MESSAGE_ID); - filename = fetch_string_for(result, MessageAttachmentTable.Column.FILENAME); - mime_type = fetch_string_for(result, MessageAttachmentTable.Column.MIME_TYPE); - } - - public Geary.Attachment to_attachment() { - return new Attachment(table.gdb.data_dir, filename, mime_type, filesize, message_id, id); - } -} - diff --git a/src/engine/sqlite/email/sqlite-message-attachment-table.vala b/src/engine/sqlite/email/sqlite-message-attachment-table.vala deleted file mode 100644 index 9038a315..00000000 --- a/src/engine/sqlite/email/sqlite-message-attachment-table.vala +++ /dev/null @@ -1,90 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.MessageAttachmentTable : Geary.Sqlite.Table { - // This row *must* match the order in the schema - public enum Column { - ID, - MESSAGE_ID, - FILENAME, - MIME_TYPE, - FILESIZE - } - - public MessageAttachmentTable(Geary.Sqlite.Database db, SQLHeavy.Table table) { - base (db, table); - } - - public async int64 create_async(Transaction? transaction, MessageAttachmentRow row, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "MessageAttachmentTable.create_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "INSERT INTO MessageAttachmentTable (message_id, filename, mime_type, filesize) " + - "VALUES (?, ?, ?, ?)"); - query.bind_int64(0, row.message_id); - query.bind_string(1, row.filename); - query.bind_string(2, row.mime_type); - query.bind_int64(3, row.filesize); - - int64 id = yield query.execute_insert_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - - check_cancel(cancellable, "create_async"); - - return id; - } - - public async Gee.List? list_async(Transaction? transaction, - int64 message_id, Cancellable? cancellable) throws Error { - - Transaction locked = yield obtain_lock_async(transaction, "MessageAttachmentTable.list_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, filename, mime_type, filesize FROM MessageAttachmentTable " + - "WHERE message_id = ? ORDER BY id"); - query.bind_int64(0, message_id); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "list_async"); - - Gee.List list = new Gee.ArrayList(); - if (results.finished) - return list; - - do { - list.add(new MessageAttachmentRow(this, results.fetch_int64(0), message_id, - results.fetch_string(1), results.fetch_string(2), results.fetch_int64(3))); - - yield results.next_async(); - - check_cancel(cancellable, "list_async"); - } while (!results.finished); - - return list; - } - - public async void remove_async(Transaction? transaction, int64 attachment_id, - Cancellable? cancellable) throws Error { - - Transaction locked = yield obtain_lock_async(transaction, - "MessageAttachmentTable.remove_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "DELETE FROM MessageAttachmentTable WHERE attachment_id = ?"); - query.bind_int64(0, attachment_id); - - yield query.execute_async(); - locked.set_commit_required(); - yield release_lock_async(transaction, locked, cancellable); - check_cancel(cancellable, "remove_async"); - } -} - diff --git a/src/engine/sqlite/email/sqlite-message-location-row.vala b/src/engine/sqlite/email/sqlite-message-location-row.vala deleted file mode 100644 index 3f5ce574..00000000 --- a/src/engine/sqlite/email/sqlite-message-location-row.vala +++ /dev/null @@ -1,58 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.MessageLocationRow : Geary.Sqlite.Row { - public int64 id { get; private set; } - public int64 message_id { get; private set; } - public int64 folder_id { get; private set; } - public int64 ordering { get; private set; } - - private int position; - - public MessageLocationRow(MessageLocationTable table, int64 id, int64 message_id, int64 folder_id, - int64 ordering, int position) { - base (table); - - this.id = id; - this.message_id = message_id; - this.folder_id = folder_id; - this.ordering = ordering; - this.position = position; - } - - public MessageLocationRow.from_query_result(MessageLocationTable table, int position, - SQLHeavy.QueryResult result) throws Error { - base (table); - - id = fetch_int64_for(result, MessageLocationTable.Column.ID); - message_id = fetch_int64_for(result, MessageLocationTable.Column.MESSAGE_ID); - folder_id = fetch_int64_for(result, MessageLocationTable.Column.FOLDER_ID); - ordering = fetch_int64_for(result, MessageLocationTable.Column.ORDERING); - this.position = position; - } - - /** - * Note that position is not stored in the database, but rather determined by its location - * determined by the sorted ordering column. In some cases the database can determine the - * position easily and will supply it to this object at construction time. In other cases it's - * not so straightforward and another database query will be required. This method handles - * both cases. - * - * If the call ever returns a position of -1, that indicates the message does not exist in the - * database. - */ - public async int get_position_async(Transaction? transaction, bool include_removed, - Cancellable? cancellable) throws Error { - if (position >= 1) - return position; - - position = yield ((MessageLocationTable) table).fetch_position_async(transaction, id, folder_id, - include_removed, cancellable); - - return (position >= 1) ? position : -1; - } -} - diff --git a/src/engine/sqlite/email/sqlite-message-location-table.vala b/src/engine/sqlite/email/sqlite-message-location-table.vala deleted file mode 100644 index 4457689d..00000000 --- a/src/engine/sqlite/email/sqlite-message-location-table.vala +++ /dev/null @@ -1,372 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table { - // This row *must* match the order in the schema - public enum Column { - ID, - MESSAGE_ID, - FOLDER_ID, - ORDERING, - REMOVE_MARKER - } - - internal MessageLocationTable(Geary.Sqlite.Database db, SQLHeavy.Table table) { - base (db, table); - } - - public async int64 create_async(Transaction? transaction, MessageLocationRow row, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "MessageLocationTable.create_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "INSERT INTO MessageLocationTable (message_id, folder_id, ordering) VALUES (?, ?, ?)"); - query.bind_int64(0, row.message_id); - query.bind_int64(1, row.folder_id); - query.bind_int64(2, row.ordering); - - int64 id = yield query.execute_insert_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - - check_cancel(cancellable, "create_async"); - - return id; - } - - /** - * low is one-based. If count is -1, all messages starting at low are returned. - */ - public async Gee.List? list_async(Transaction? transaction, - int64 folder_id, int low, int count, bool include_marked, Cancellable? cancellable) - throws Error { - assert(low >= 1); - assert(count >= 0 || count == -1); - - Transaction locked = yield obtain_lock_async(transaction, "MessageLocationTable.list_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, message_id, ordering FROM MessageLocationTable WHERE folder_id = ? " - + "%s ORDER BY ordering LIMIT ? OFFSET ?".printf(include_marked ? "" : - "AND remove_marker = 0")); - query.bind_int64(0, folder_id); - query.bind_int(1, count); - query.bind_int(2, low - 1); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "list_async"); - - if (results.finished) - return null; - - Gee.List list = new Gee.ArrayList(); - int position = low; - do { - list.add(new MessageLocationRow(this, results.fetch_int64(0), results.fetch_int64(1), - folder_id, results.fetch_int64(2), position++)); - - yield results.next_async(); - - check_cancel(cancellable, "list_async"); - } while (!results.finished); - - return list; - } - - public async Gee.List? list_ordering_async(Transaction? transaction, - int64 folder_id, int64 low_ordering, int64 high_ordering, Cancellable? cancellable) - throws Error { - Transaction locked = yield obtain_lock_async(transaction, "MessageLocationTable.list_ordering_async", - cancellable); - - assert(low_ordering >= 0 || low_ordering == -1); - assert(high_ordering >= 0 || high_ordering == -1); - - SQLHeavy.Query query; - if (high_ordering != -1 && low_ordering != -1) { - query = locked.prepare( - "SELECT id, message_id, ordering FROM MessageLocationTable WHERE folder_id = ? " - + "AND ordering >= ? AND ordering <= ? AND remove_marker = 0 ORDER BY ordering ASC"); - query.bind_int64(0, folder_id); - query.bind_int64(1, low_ordering); - query.bind_int64(2, high_ordering); - } else if (high_ordering == -1) { - query = locked.prepare( - "SELECT id, message_id, ordering FROM MessageLocationTable WHERE folder_id = ? " - + "AND ordering >= ? AND remove_marker = 0 ORDER BY ordering ASC"); - query.bind_int64(0, folder_id); - query.bind_int64(1, low_ordering); - } else { - assert(low_ordering == -1); - query = locked.prepare( - "SELECT id, message_id, ordering FROM MessageLocationTable WHERE folder_id = ? " - + "AND ordering <= ? AND remove_marker = 0 ORDER BY ordering ASC"); - query.bind_int64(0, folder_id); - query.bind_int64(1, high_ordering); - } - - SQLHeavy.QueryResult result = yield query.execute_async(); - check_cancel(cancellable, "list_ordering_async"); - - if (result.finished) - return null; - - Gee.List? list = new Gee.ArrayList(); - do { - list.add(new MessageLocationRow(this, result.fetch_int64(0), result.fetch_int64(1), - folder_id, result.fetch_int64(2), -1)); - - yield result.next_async(); - - check_cancel(cancellable, "list_ordering_async"); - - } while (!result.finished); - - return (list.size > 0) ? list : null; - } - - public async MessageLocationRow? fetch_by_ordering_async(Transaction? transaction, - int64 folder_id, int64 ordering, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "MessageLocationTable.fetch_by_ordering_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, message_id FROM MessageLocationTable WHERE folder_id = ? AND ordering = ? " - + "AND remove_marker = 0"); - query.bind_int64(0, folder_id); - query.bind_int64(1, ordering); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "fetch_ordering_async"); - - if (results.finished) - return null; - - return new MessageLocationRow(this, results.fetch_int64(0), results.fetch_int64(1), - folder_id, ordering, -1); - } - - public async MessageLocationRow? fetch_by_message_id_async(Transaction? transaction, - int64 folder_id, int64 message_id, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.fetch_by_message_id_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, ordering FROM MessageLocationTable WHERE folder_id = ? AND message_id = ? " - + "AND remove_marker = 0"); - query.bind_int64(0, folder_id); - query.bind_int64(1, message_id); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "fetch_by_message_id_async"); - if (results.finished) - return null; - - check_cancel(cancellable, "fetch_position_async"); - - return new MessageLocationRow(this, results.fetch_int64(0), message_id, - folder_id, results.fetch_int64(1), -1); - } - - public async int fetch_position_async(Transaction? transaction, int64 id, - int64 folder_id, bool include_marked, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "MessageLocationTable.fetch_position_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id FROM MessageLocationTable WHERE folder_id = ? %s ".printf(include_marked ? "" : - "AND remove_marker = 0") + "ORDER BY ordering"); - query.bind_int64(0, folder_id); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "fetch_position_async"); - - int position = 1; - while (!results.finished) { - if (results.fetch_int64(0) == id) - return position; - - yield results.next_async(); - - check_cancel(cancellable, "fetch_position_async"); - - position++; - } - - // not found - return -1; - } - - public async int fetch_message_position_async(Transaction? transaction, int64 message_id, - int64 folder_id, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.fetch_message_position_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT message_id FROM MessageLocationTable WHERE folder_id=? AND remove_marker = 0 " - + "ORDER BY ordering"); - query.bind_int64(0, folder_id); - - SQLHeavy.QueryResult results = yield query.execute_async(cancellable); - - int position = 1; - while (!results.finished) { - check_cancel(cancellable, "fetch_message_position_async"); - - if (results.fetch_int64(0) == message_id) - return position; - - yield results.next_async(); - - position++; - } - - // not found - return -1; - } - - public async int fetch_count_for_folder_async(Transaction? transaction, - int64 folder_id, bool include_removed, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.fetch_count_for_folder_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT COUNT(*) FROM MessageLocationTable WHERE folder_id = ? %s".printf( - include_removed ? "" : "AND remove_marker = 0")); - query.bind_int64(0, folder_id); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "fetch_count_for_folder_async"); - - return (!results.finished) ? results.fetch_int(0) : 0; - } - - /** - * Find a row based on its ordering value in the folder. - */ - public async bool does_ordering_exist_async(Transaction? transaction, int64 folder_id, - int64 ordering, out int64 message_id, Cancellable? cancellable) throws Error { - message_id = Row.INVALID_ID; - - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.does_ordering_exist_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT message_id FROM MessageLocationTable WHERE folder_id = ? AND ordering = ? " - + "AND remove_marker = 0"); - query.bind_int64(0, folder_id); - query.bind_int64(1, ordering); - - SQLHeavy.QueryResult results = yield query.execute_async(); - if (results.finished) - return false; - - message_id = results.fetch_int64(0); - - return true; - } - - public async int64 get_ordering_extremes_async(Transaction? transaction, int64 folder_id, - bool earliest, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.get_ordering_extremes_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT %s FROM MessageLocationTable WHERE folder_id = ? AND remove_marker = 0".printf( - earliest ? "MIN(ordering)" : "MAX(ordering)")); - query.bind_int64(0, folder_id); - - SQLHeavy.QueryResult result = yield query.execute_async(); - check_cancel(cancellable, "get_ordering_extremes_async"); - - return (!result.finished) ? result.fetch_int64(0) : -1; - } - - public async bool remove_by_ordering_async(Transaction? transaction, int64 folder_id, - int64 ordering, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.remove_by_ordering_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id FROM MessageLocationTable WHERE folder_id=? AND ordering=?"); - query.bind_int64(0, folder_id); - query.bind_int64(1, ordering); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "remove_by_ordering_async"); - - if (results.finished) - return false; - - query = locked.prepare("DELETE FROM MessageLocationTable WHERE id=?"); - query.bind_int64(0, results.fetch_int(0)); - - yield query.execute_async(); - check_cancel(cancellable, "remove_by_ordering_async"); - - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - - return true; - } - - // Marks the given message as removed if "remove" is true, otherwise marks - // it as non-removed. - public async void mark_removed_async(Transaction? transaction, int64 folder_id, int64 ordering, - bool remove, Cancellable? cancellable) throws Error { - - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.mark_removed_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "UPDATE MessageLocationTable SET remove_marker = ? WHERE folder_id = ? AND ordering = ?"); - query.bind_int(0, (int) remove); - query.bind_int64(1, folder_id); - query.bind_int64(2, ordering); - - yield query.execute_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - } - - public async bool is_marked_removed_async(Transaction? transaction, int64 folder_id, - int64 ordering, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.is_mark_removed_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT remove_marker FROM MessageLocationTable WHERE folder_id = ? AND ordering = ?"); - query.bind_int64(0, folder_id); - query.bind_int64(1, ordering); - - SQLHeavy.QueryResult results = yield query.execute_async(); - - check_cancel(cancellable, "is_marked_removed_async"); - - return (bool) results.fetch_int(0); - } - - public async void remove_all_marked_for_remove(Transaction? transaction, int64 folder_id, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "MessageLocationTable.remove_all_marked_for_remove", cancellable); - - SQLHeavy.Query query = locked.prepare( - "DELETE FROM MessageLocationTable WHERE folder_id=? AND remove_marker=1"); - query.bind_int64(0, folder_id); - - yield query.execute_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - } -} - diff --git a/src/engine/sqlite/email/sqlite-message-row.vala b/src/engine/sqlite/email/sqlite-message-row.vala deleted file mode 100644 index 14b855aa..00000000 --- a/src/engine/sqlite/email/sqlite-message-row.vala +++ /dev/null @@ -1,226 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.MessageRow : Geary.Sqlite.Row { - public int64 id { get; set; default = INVALID_ID; } - public Geary.Email.Field fields { get; set; default = Geary.Email.Field.NONE; } - - public string? date { get; set; } - public time_t date_time_t { get; set; default = -1; } - - public string? from { get; set; } - public string? sender { get; set; } - public string? reply_to { get; set; } - - public string? to { get; set; } - public string? cc { get; set; } - public string? bcc { get; set; } - - public string? message_id { get; set; } - public string? in_reply_to { get; set; } - public string? references { get; set; } - - public string? subject { get; set; } - - public string? header { get; set; } - - public string? body { get; set; } - - public string? preview { get; set; } - - public MessageRow(Table table) { - base (table); - } - - public MessageRow.from_email(MessageTable table, Geary.Email email) { - base (table); - - set_from_email(email.fields, email); - } - - public MessageRow.from_query_result(Table table, Geary.Email.Field requested_fields, - SQLHeavy.QueryResult result) throws Error { - base (table); - - id = fetch_int64_for(result, MessageTable.Column.ID); - - // the available fields are an intersection of what's available in the database and - // what was requested - fields = requested_fields & fetch_int_for(result, MessageTable.Column.FIELDS); - - if ((fields & Geary.Email.Field.DATE) != 0) { - date = fetch_string_for(result, MessageTable.Column.DATE_FIELD); - date_time_t = (time_t) fetch_int64_for(result, MessageTable.Column.DATE_TIME_T); - } - - if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { - from = fetch_string_for(result, MessageTable.Column.FROM_FIELD); - sender = fetch_string_for(result, MessageTable.Column.SENDER); - reply_to = fetch_string_for(result, MessageTable.Column.REPLY_TO); - } - - if ((fields & Geary.Email.Field.RECEIVERS) != 0) { - to = fetch_string_for(result, MessageTable.Column.TO_FIELD); - cc = fetch_string_for(result, MessageTable.Column.CC); - bcc = fetch_string_for(result, MessageTable.Column.BCC); - } - - if ((fields & Geary.Email.Field.REFERENCES) != 0) { - message_id = fetch_string_for(result, MessageTable.Column.MESSAGE_ID); - in_reply_to = fetch_string_for(result, MessageTable.Column.IN_REPLY_TO); - references = fetch_string_for(result, MessageTable.Column.REFERENCES); - } - - if ((fields & Geary.Email.Field.SUBJECT) != 0) - subject = fetch_string_for(result, MessageTable.Column.SUBJECT); - - if ((fields & Geary.Email.Field.HEADER) != 0) - header = fetch_string_for(result, MessageTable.Column.HEADER); - - if ((fields & Geary.Email.Field.BODY) != 0) - body = fetch_string_for(result, MessageTable.Column.BODY); - - if ((fields & Geary.Email.Field.PREVIEW) != 0) - preview = fetch_string_for(result, MessageTable.Column.PREVIEW); - } - - public Geary.Email to_email(int position, Geary.EmailIdentifier id) throws Error { - // Important to set something in the Email object if the field bit is set ... for example, - // if the caller expects to see a DATE field, that field is set in the Email's bitmask, - // even if the Date object is null - Geary.Email email = new Geary.Email(position, id); - - if ((fields & Geary.Email.Field.DATE) != 0) - email.set_send_date(!String.is_empty(date) ? new RFC822.Date(date) : null); - - if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { - email.set_originators(unflatten_addresses(from), unflatten_addresses(sender), - unflatten_addresses(reply_to)); - } - - if ((fields & Geary.Email.Field.RECEIVERS) != 0) { - email.set_receivers(unflatten_addresses(to), unflatten_addresses(cc), - unflatten_addresses(bcc)); - } - - if ((fields & Geary.Email.Field.REFERENCES) != 0) { - email.set_full_references( - (message_id != null) ? new RFC822.MessageID(message_id) : null, - (in_reply_to != null) ? new RFC822.MessageID(in_reply_to) : null, - (references != null) ? new RFC822.MessageIDList.from_rfc822_string(references) : null); - } - - if ((fields & Geary.Email.Field.SUBJECT) != 0) - email.set_message_subject(new RFC822.Subject.decode(subject ?? "")); - - if ((fields & Geary.Email.Field.HEADER) != 0) - email.set_message_header(new RFC822.Header(new Geary.Memory.StringBuffer(header ?? ""))); - - if ((fields & Geary.Email.Field.BODY) != 0) - email.set_message_body(new RFC822.Text(new Geary.Memory.StringBuffer(body ?? ""))); - - if ((fields & Geary.Email.Field.PREVIEW) != 0) - email.set_message_preview(new RFC822.PreviewText(new Geary.Memory.StringBuffer(preview ?? ""))); - - return email; - } - - public void merge_from_remote(Geary.Email email) { - foreach (Geary.Email.Field field in Geary.Email.Field.all()) { - if ((email.fields & field) != 0) - set_from_email(field, email); - } - } - - private string? flatten_addresses(RFC822.MailboxAddresses? addrs) { - if (addrs == null) - return null; - - switch (addrs.size) { - case 0: - return null; - - case 1: - return addrs[0].to_rfc822_string(); - - default: - StringBuilder builder = new StringBuilder(); - foreach (RFC822.MailboxAddress addr in addrs) { - if (!String.is_empty(builder.str)) - builder.append(", "); - - builder.append(addr.to_rfc822_string()); - } - - return builder.str; - } - } - - private RFC822.MailboxAddresses? unflatten_addresses(string? str) { - return String.is_empty(str) ? null : new RFC822.MailboxAddresses.from_rfc822_string(str); - } - - private void set_from_email(Geary.Email.Field fields, Geary.Email email) { - // Although the fields bitmask might indicate various fields are set, they may still be - // null if empty - - if ((fields & Geary.Email.Field.DATE) != 0) { - date = (email.date != null) ? email.date.original : null; - date_time_t = (email.date != null) ? email.date.as_time_t : -1; - - this.fields = this.fields.set(Geary.Email.Field.DATE); - } - - if ((fields & Geary.Email.Field.ORIGINATORS) != 0) { - from = flatten_addresses(email.from); - sender = flatten_addresses(email.sender); - reply_to = flatten_addresses(email.reply_to); - - this.fields = this.fields.set(Geary.Email.Field.ORIGINATORS); - } - - if ((fields & Geary.Email.Field.RECEIVERS) != 0) { - to = flatten_addresses(email.to); - cc = flatten_addresses(email.cc); - bcc = flatten_addresses(email.bcc); - - this.fields = this.fields.set(Geary.Email.Field.RECEIVERS); - } - - if ((fields & Geary.Email.Field.REFERENCES) != 0) { - message_id = (email.message_id != null) ? email.message_id.value : null; - in_reply_to = (email.in_reply_to != null) ? email.in_reply_to.value : null; - references = (email.references != null) ? email.references.to_rfc822_string() : null; - - this.fields = this.fields.set(Geary.Email.Field.REFERENCES); - } - - if ((fields & Geary.Email.Field.SUBJECT) != 0) { - subject = (email.subject != null) ? email.subject.original : null; - - this.fields = this.fields.set(Geary.Email.Field.SUBJECT); - } - - if ((fields & Geary.Email.Field.HEADER) != 0) { - header = (email.header != null) ? email.header.buffer.to_string() : null; - - this.fields = this.fields.set(Geary.Email.Field.HEADER); - } - - if ((fields & Geary.Email.Field.BODY) != 0) { - body = (email.body != null) ? email.body.buffer.to_string() : null; - - this.fields = this.fields.set(Geary.Email.Field.BODY); - } - - if ((fields & Geary.Email.Field.PREVIEW) != 0) { - preview = (email.preview != null) ? email.preview.buffer.to_string() : null; - - this.fields = this.fields.set(Geary.Email.Field.PREVIEW); - } - } -} - diff --git a/src/engine/sqlite/email/sqlite-message-table.vala b/src/engine/sqlite/email/sqlite-message-table.vala deleted file mode 100644 index 7e4cf7e0..00000000 --- a/src/engine/sqlite/email/sqlite-message-table.vala +++ /dev/null @@ -1,292 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.MessageTable : Geary.Sqlite.Table { - // This *must* match the column order in the database - public enum Column { - ID, - FIELDS, - - DATE_FIELD, - DATE_TIME_T, - - FROM_FIELD, - SENDER, - REPLY_TO, - - TO_FIELD, - CC, - BCC, - - MESSAGE_ID, - IN_REPLY_TO, - REFERENCES, - - SUBJECT, - - HEADER, - - BODY, - - PREVIEW; - } - - internal MessageTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { - base (gdb, table); - } - - public async int64 create_async(Transaction? transaction, MessageRow row, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "MessageTable.create_async", - cancellable); - - SQLHeavy.Query query = locked.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) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - query.bind_int(0, row.fields); - query.bind_string(1, row.date); - query.bind_int64(2, row.date_time_t); - query.bind_string(3, row.from); - query.bind_string(4, row.sender); - query.bind_string(5, row.reply_to); - query.bind_string(6, row.to); - query.bind_string(7, row.cc); - query.bind_string(8, row.bcc); - query.bind_string(9, row.message_id); - query.bind_string(10, row.in_reply_to); - query.bind_string(11, row.references); - query.bind_string(12, row.subject); - query.bind_string(13, row.header); - query.bind_string(14, row.body); - query.bind_string(15, row.preview); - - int64 id = yield query.execute_insert_async(cancellable); - locked.set_commit_required(); - - check_cancel(cancellable, "create_async"); - - yield release_lock_async(transaction, locked, cancellable); - - return id; - } - - // TODO: This could be made more efficient by executing a single SQL statement. - public async void merge_async(Transaction? transaction, MessageRow row, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "MessageTable.merge_async", - cancellable); - - Geary.Email.Field available_fields; - if (!yield fetch_fields_async(locked, row.id, out available_fields, cancellable)) - throw new EngineError.NOT_FOUND("No message with ID %lld found in database", row.id); - - // This calculates the fields in the row that are not in the database already - Geary.Email.Field new_fields = (row.fields ^ available_fields) & row.fields; - if (new_fields == Geary.Email.Field.NONE) { - // nothing to add - return; - } - - NonblockingBatch batch = new NonblockingBatch(); - - // since the following queries are all updates, the Transaction must be committed - locked.set_commit_required(); - - SQLHeavy.Query? query = null; - if (new_fields.is_any_set(Geary.Email.Field.DATE)) { - query = locked.prepare( - "UPDATE MessageTable SET date_field=?, date_time_t=? WHERE id=?"); - query.bind_string(0, row.date); - query.bind_int64(1, row.date_time_t); - query.bind_int64(2, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - if (new_fields.is_any_set(Geary.Email.Field.ORIGINATORS)) { - query = locked.prepare( - "UPDATE MessageTable SET from_field=?, sender=?, reply_to=? WHERE id=?"); - query.bind_string(0, row.from); - query.bind_string(1, row.sender); - query.bind_string(2, row.reply_to); - query.bind_int64(3, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - if (new_fields.is_any_set(Geary.Email.Field.RECEIVERS)) { - query = locked.prepare( - "UPDATE MessageTable SET to_field=?, cc=?, bcc=? WHERE id=?"); - query.bind_string(0, row.to); - query.bind_string(1, row.cc); - query.bind_string(2, row.bcc); - query.bind_int64(3, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - if (new_fields.is_any_set(Geary.Email.Field.REFERENCES)) { - query = locked.prepare( - "UPDATE MessageTable SET message_id=?, in_reply_to=?, reference_ids=? WHERE id=?"); - query.bind_string(0, row.message_id); - query.bind_string(1, row.in_reply_to); - query.bind_string(2, row.references); - query.bind_int64(3, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - if (new_fields.is_any_set(Geary.Email.Field.SUBJECT)) { - query = locked.prepare( - "UPDATE MessageTable SET subject=? WHERE id=?"); - query.bind_string(0, row.subject); - query.bind_int64(1, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - if (new_fields.is_any_set(Geary.Email.Field.HEADER)) { - query = locked.prepare( - "UPDATE MessageTable SET header=? WHERE id=?"); - query.bind_string(0, row.header); - query.bind_int64(1, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - if (new_fields.is_any_set(Geary.Email.Field.BODY)) { - query = locked.prepare( - "UPDATE MessageTable SET body=? WHERE id=?"); - query.bind_string(0, row.body); - query.bind_int64(1, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - if (new_fields.is_any_set(Geary.Email.Field.PREVIEW)) { - query = locked.prepare( - "UPDATE MessageTable SET preview=? WHERE id=?"); - query.bind_string(0, row.preview); - query.bind_int64(1, row.id); - - batch.add(new ExecuteQueryOperation(query)); - } - - yield batch.execute_all_async(cancellable); - - batch.throw_first_exception(); - - // now merge the new fields in the row (do this *after* adding the fields, particularly - // since we don't have full transaction backing out in the case of cancelled due to bugs - // in SQLHeavy's async code - query = locked.prepare( - "UPDATE MessageTable SET fields = fields | ? WHERE id=?"); - query.bind_int(0, new_fields); - query.bind_int64(1, row.id); - - yield query.execute_async(cancellable); - - yield release_lock_async(transaction, locked, cancellable); - } - - public async MessageRow? fetch_async(Transaction? transaction, int64 id, - Geary.Email.Field requested_fields, Cancellable? cancellable = null) throws Error { - assert(requested_fields != Geary.Email.Field.NONE); - // PROPERTIES and FLAGS are handled by the appropriate PropertiesTable - assert(requested_fields.clear(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS) != 0); - - Transaction locked = yield obtain_lock_async(transaction, "MessageTable.fetch_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT %s FROM MessageTable WHERE id=?".printf(fields_to_columns(requested_fields))); - query.bind_int64(0, id); - - SQLHeavy.QueryResult results = yield query.execute_async(); - if (results.finished) - return null; - - check_cancel(cancellable, "fetch_async"); - - MessageRow row = new MessageRow.from_query_result(this, requested_fields, results); - - return row; - } - - public async bool fetch_fields_async(Transaction? transaction, int64 id, - out Geary.Email.Field available_fields, Cancellable? cancellable) throws Error { - available_fields = Geary.Email.Field.NONE; - - Transaction locked = yield obtain_lock_async(transaction, "MessageTable.fetch_fields_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT fields FROM MessageTable WHERE id=?"); - query.bind_int64(0, id); - - SQLHeavy.QueryResult result = yield query.execute_async(); - if (result.finished) - return false; - - check_cancel(cancellable, "fetch_fields_async"); - - available_fields = (Geary.Email.Field) result.fetch_int(0); - - return true; - } - - private static string fields_to_columns(Geary.Email.Field fields) { - StringBuilder builder = new StringBuilder("id, fields"); - foreach (Geary.Email.Field field in Geary.Email.Field.all()) { - string? append = null; - if ((fields & field) != 0) { - 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; - } - } - - if (append != null) { - builder.append(", "); - builder.append(append); - } - } - - return builder.str; - } -} - diff --git a/src/engine/sqlite/imap/sqlite-imap-database.vala b/src/engine/sqlite/imap/sqlite-imap-database.vala deleted file mode 100644 index f237c02a..00000000 --- a/src/engine/sqlite/imap/sqlite-imap-database.vala +++ /dev/null @@ -1,30 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -private class Geary.Sqlite.ImapDatabase : Geary.Sqlite.MailDatabase { - public ImapDatabase(string user, File user_data_dir, File resource_dir) throws Error { - base (user, user_data_dir, resource_dir); - } - - public Geary.Sqlite.ImapFolderPropertiesTable get_imap_folder_properties_table() { - SQLHeavy.Table heavy_table; - ImapFolderPropertiesTable? imap_folder_properties_table = get_table( - "ImapFolderPropertiesTable", out heavy_table) as ImapFolderPropertiesTable; - - return imap_folder_properties_table - ?? (ImapFolderPropertiesTable) add_table(new ImapFolderPropertiesTable(this, heavy_table)); - } - - public Geary.Sqlite.ImapMessagePropertiesTable get_imap_message_properties_table() { - SQLHeavy.Table heavy_table; - ImapMessagePropertiesTable? imap_message_properties_table = get_table( - "ImapMessagePropertiesTable", out heavy_table) as ImapMessagePropertiesTable; - - return imap_message_properties_table - ?? (ImapMessagePropertiesTable) add_table(new ImapMessagePropertiesTable(this, heavy_table)); - } -} - diff --git a/src/engine/sqlite/imap/sqlite-imap-folder-properties-row.vala b/src/engine/sqlite/imap/sqlite-imap-folder-properties-row.vala deleted file mode 100644 index 0f8916fb..00000000 --- a/src/engine/sqlite/imap/sqlite-imap-folder-properties-row.vala +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.ImapFolderPropertiesRow : Geary.Sqlite.Row { - public int64 id { get; private set; } - public int64 folder_id { get; private set; } - public int last_seen_total { get; private set; } - public Geary.Imap.UIDValidity? uid_validity { get; private set; } - public Geary.Imap.UID? uid_next { get; private set; } - public string attributes { get; private set; } - - public ImapFolderPropertiesRow(ImapFolderPropertiesTable table, int64 id, int64 folder_id, - int last_seen_total, Geary.Imap.UIDValidity? uid_validity, Geary.Imap.UID? uid_next, - string? attributes) { - base (table); - - this.id = id; - this.folder_id = folder_id; - this.last_seen_total = last_seen_total; - this.uid_validity = uid_validity; - this.uid_next = uid_next; - this.attributes = attributes ?? ""; - } - - public ImapFolderPropertiesRow.from_imap_properties(ImapFolderPropertiesTable table, - int64 folder_id, Geary.Imap.FolderProperties properties) { - base (table); - - id = Row.INVALID_ID; - this.folder_id = folder_id; - last_seen_total = properties.messages; - uid_validity = properties.uid_validity; - uid_next = properties.uid_next; - attributes = properties.attrs.serialize(); - } - - public Geary.Imap.FolderProperties get_imap_folder_properties() { - return new Geary.Imap.FolderProperties(last_seen_total, 0, 0, uid_validity, uid_next, - Geary.Imap.MailboxAttributes.deserialize(attributes)); - } -} - diff --git a/src/engine/sqlite/imap/sqlite-imap-folder-properties-table.vala b/src/engine/sqlite/imap/sqlite-imap-folder-properties-table.vala deleted file mode 100644 index 0fa5a2fc..00000000 --- a/src/engine/sqlite/imap/sqlite-imap-folder-properties-table.vala +++ /dev/null @@ -1,93 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.ImapFolderPropertiesTable : Geary.Sqlite.Table { - // This *must* be in the same order as the schema. - public enum Column { - ID, - FOLDER_ID, - LAST_SEEN_TOTAL, - UID_VALIDITY, - UID_NEXT, - ATTRIBUTES - } - - public ImapFolderPropertiesTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { - base (gdb, table); - } - - public async int64 create_async(Transaction? transaction, ImapFolderPropertiesRow row, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "ImapFolderPropertiesTable.create_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "INSERT INTO ImapFolderPropertiesTable (folder_id, last_seen_total, uid_validity, uid_next, attributes) " - + "VALUES (?, ?, ?, ?, ?)"); - query.bind_int64(0, row.folder_id); - query.bind_int(1, row.last_seen_total); - query.bind_int64(2, (row.uid_validity != null) ? row.uid_validity.value : -1); - query.bind_int64(3, (row.uid_next != null) ? row.uid_next.value : -1); - query.bind_string(4, row.attributes); - - int64 id = yield query.execute_insert_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - - return id; - } - - public async void update_async(Transaction? transaction, int64 folder_id, - ImapFolderPropertiesRow row, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "ImapFolderPropertiesTable.update_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "UPDATE ImapFolderPropertiesTable " - + "SET last_seen_total = ?, uid_validity = ?, uid_next = ?, attributes = ? " - + "WHERE folder_id = ?"); - query.bind_int(0, row.last_seen_total); - query.bind_int64(1, (row.uid_validity != null) ? row.uid_validity.value : -1); - query.bind_int64(2, (row.uid_next != null) ? row.uid_next.value : -1); - query.bind_string(3, row.attributes); - query.bind_int64(4, folder_id); - - yield query.execute_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - } - - public async ImapFolderPropertiesRow? fetch_async(Transaction? transaction, - int64 folder_id, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "ImapFolderPropertiesTable.fetch_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, last_seen_total, uid_validity, uid_next, attributes " - + "FROM ImapFolderPropertiesTable WHERE folder_id = ?"); - query.bind_int64(0, folder_id); - - SQLHeavy.QueryResult result = yield query.execute_async(); - if (result.finished) - return null; - - check_cancel(cancellable, "fetch_async"); - - Geary.Imap.UIDValidity? uid_validity = null; - if (result.fetch_int64(2) >= 0) - uid_validity = new Geary.Imap.UIDValidity(result.fetch_int64(2)); - - Geary.Imap.UID? uid_next = null; - if (result.fetch_int64(3) >= 0) - uid_next = new Geary.Imap.UID(result.fetch_int64(3)); - - return new ImapFolderPropertiesRow(this, result.fetch_int64(0), folder_id, result.fetch_int(1), - uid_validity, uid_next, result.fetch_string(4)); - } -} - diff --git a/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala b/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala deleted file mode 100644 index b9cf188b..00000000 --- a/src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala +++ /dev/null @@ -1,57 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.ImapMessagePropertiesRow : Geary.Sqlite.Row { - public int64 id { get; private set; } - public int64 message_id { get; private set; } - public string? flags { get; private set; } - public string? internaldate { get; private set; } - public long rfc822_size { get; private set; } - - public ImapMessagePropertiesRow(ImapMessagePropertiesTable table, int64 id, int64 message_id, - string? flags, string? internaldate, long rfc822_size) { - base (table); - - this.id = id; - this.message_id = message_id; - this.flags = flags; - this.internaldate = internaldate; - this.rfc822_size = rfc822_size; - } - - public ImapMessagePropertiesRow.from_imap_properties(ImapMessagePropertiesTable table, - int64 message_id, Geary.Imap.EmailProperties? properties, Imap.MessageFlags? message_flags) { - base (table); - - id = Row.INVALID_ID; - this.message_id = message_id; - flags = (message_flags != null) ? message_flags.serialize() : null;; - internaldate = (properties != null) ? properties.internaldate.original : null; - rfc822_size = (properties != null) ? properties.rfc822_size.value : -1; - } - - public Geary.Imap.EmailProperties? get_imap_email_properties() { - if (internaldate == null || rfc822_size < 0) - return null; - - Imap.InternalDate? constructed = null; - try { - constructed = new Imap.InternalDate(internaldate); - } catch (Error err) { - debug("Unable to construct internaldate object from \"%s\": %s", internaldate, - err.message); - - return null; - } - - return new Geary.Imap.EmailProperties(constructed, new RFC822.Size(rfc822_size)); - } - - public Geary.EmailFlags? get_email_flags() { - return (flags != null) ? new Geary.Imap.EmailFlags(Geary.Imap.MessageFlags.deserialize(flags)) : null; - } -} - diff --git a/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala b/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala deleted file mode 100644 index 77c44bc0..00000000 --- a/src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala +++ /dev/null @@ -1,141 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -public class Geary.Sqlite.ImapMessagePropertiesTable : Geary.Sqlite.Table { - // This *must* be in the same order as the schema. - public enum Column { - ID, - MESSAGE_ID, - FLAGS, - INTERNALDATE, - RFC822_SIZE - } - - public ImapMessagePropertiesTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { - base (gdb, table); - } - - public async int64 create_async(Transaction? transaction, ImapMessagePropertiesRow row, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "ImapMessagePropertiesTable.create_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "INSERT INTO ImapMessagePropertiesTable (message_id, flags, internaldate, rfc822_size) " - + "VALUES (?, ?, ?, ?)"); - query.bind_int64(0, row.message_id); - query.bind_string(1, row.flags); - query.bind_string(2, row.internaldate); - query.bind_int64(3, row.rfc822_size); - - int64 id = yield query.execute_insert_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - - return id; - } - - public async ImapMessagePropertiesRow? fetch_async(Transaction? transaction, int64 message_id, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "ImapMessagePropertiesTable.fetch_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, flags, internaldate, rfc822_size FROM ImapMessagePropertiesTable " - + "WHERE message_id = ?"); - query.bind_int64(0, message_id); - - SQLHeavy.QueryResult result = yield query.execute_async(); - if (result.finished) - return null; - - check_cancel(cancellable, "fetch_async"); - - return new ImapMessagePropertiesRow(this, result.fetch_int64(0), message_id, - result.fetch_string(1), result.fetch_string(2), (long) result.fetch_int64(3)); - } - - public async void update_properties_async(Transaction? transaction, int64 message_id, - string? internaldate, long rfc822_size, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "ImapMessagePropertiesTable.update_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "UPDATE ImapMessagePropertiesTable SET internaldate = ?, rfc822_size = ? WHERE message_id = ?"); - query.bind_string(0, internaldate); - query.bind_int64(1, rfc822_size); - query.bind_int64(2, message_id); - - yield query.execute_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - } - - public async void update_flags_async(Transaction? transaction, int64 message_id, string? flags, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "ImapMessagePropertiesTable.update_flags_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "UPDATE ImapMessagePropertiesTable SET flags = ? WHERE message_id = ?"); - query.bind_string(0, flags); - query.bind_int64(1, message_id); - - yield query.execute_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(transaction, locked, cancellable); - } - - public async Gee.List? search_for_duplicates_async(Transaction? transaction, string? internaldate, - long rfc822_size, Cancellable? cancellable) throws Error { - bool has_internaldate = !String.is_empty(internaldate); - bool has_size = rfc822_size >= 0; - - // at least one parameter must be available - if (!has_internaldate && !has_size) - throw new EngineError.BAD_PARAMETERS("Cannot search for IMAP duplicates without a valid parameter"); - - Transaction locked = yield obtain_lock_async(transaction, "ImapMessagePropertiesTable.search_for_duplicates", - cancellable); - - SQLHeavy.Query query; - if (has_internaldate && has_size) { - query = locked.prepare( - "SELECT message_id FROM ImapMessagePropertiesTable WHERE internaldate=? AND rfc822_size=?"); - query.bind_string(0, internaldate); - query.bind_int64(1, rfc822_size); - } else if (has_internaldate) { - query = locked.prepare( - "SELECT message_id FROM ImapMessagePropertiesTable WHERE internaldate=?"); - query.bind_string(0, internaldate); - } else { - assert(has_size); - query = locked.prepare( - "SELECT message_id FROM ImapMessagePropertiesTable WHERE rfc822_size=?"); - query.bind_int64(0, rfc822_size); - } - - SQLHeavy.QueryResult result = yield query.execute_async(); - check_cancel(cancellable, "search_for_duplicates_async"); - - if (result.finished) - return null; - - Gee.List list = new Gee.ArrayList(); - do { - list.add(result.fetch_int64(0)); - yield result.next_async(); - - check_cancel(cancellable, "search_for_duplicates_async"); - } while (!result.finished); - - return list; - } -} - diff --git a/src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala b/src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala deleted file mode 100644 index 0981fd00..00000000 --- a/src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -private class Geary.Sqlite.SmtpOutboxRow : Geary.Sqlite.Row { - public int64 id { get; set; default = INVALID_ID; } - public int64 ordering { get; set; } - public string? message { get; set; } - - private int position; - - public SmtpOutboxRow(SmtpOutboxTable table, int64 id, int64 ordering, string message, int position) { - base (table); - - this.id = id; - this.ordering = ordering; - this.message = message; - this.position = position; - } - - public async int get_position_async(Transaction? transaction, Cancellable? cancellable) - throws Error { - if (position >= 1) - return position; - - position = yield ((SmtpOutboxTable) table).fetch_position_async(transaction, ordering, - cancellable); - - return (position >= 1) ? position : -1; - } - - public string to_string() { - return "%lld".printf(ordering); - } -} - diff --git a/src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala b/src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala deleted file mode 100644 index 403ef597..00000000 --- a/src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala +++ /dev/null @@ -1,196 +0,0 @@ -/* Copyright 2011-2012 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. - */ - -private class Geary.Sqlite.SmtpOutboxTable : Geary.Sqlite.Table { - public SmtpOutboxTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { - base(gdb, table); - } - - public async Geary.Sqlite.SmtpOutboxRow create_async(Transaction? transaction, - string message, Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.create_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "INSERT INTO SmtpOutboxTable " - + "(message, ordering)" - + "VALUES (?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM SmtpOutboxTable))"); - query.bind_string(0, message); - - int64 id = yield query.execute_insert_async(cancellable); - locked.set_commit_required(); - - yield release_lock_async(null, locked, cancellable); - - SmtpOutboxRow? row = yield fetch_email_by_row_id_async(transaction, id); - if (row == null) - throw new EngineError.NOT_FOUND("Unable to locate created row %lld", id); - - return row; - } - - public async int get_email_count_async(Transaction? transaction, Cancellable? cancellable) - throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "SmtpOutboxTable.get_email_count_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT COUNT(*) FROM SmtpOutboxTable"); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "get_email_count_for_folder_async"); - - return (!results.finished) ? results.fetch_int(0) : 0; - } - - public async Gee.List? list_email_async(Transaction? transaction, - OutboxEmailIdentifier initial_id, int count, Cancellable? cancellable = null) throws Error { - int64 low = initial_id.ordering; - assert(low >= 1 || low == -1); - assert(count >= 0 || count == -1); - - Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.list_email_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, ordering, message FROM SmtpOutboxTable " - + "ORDER BY ordering %s %s".printf(count != -1 ? "LIMIT ?" : "", - low != -1 ? "OFFSET ?" : "")); - - int bind = 0; - if (count != -1) - query.bind_int(bind++, count); - - if (low != -1) - query.bind_int64(bind++, low - 1); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "list_email_async"); - - if (results.finished) - return null; - - Gee.List list = new Gee.ArrayList(); - do { - list.add(new SmtpOutboxRow(this, results.fetch_int64(0), results.fetch_int64(1), - results.fetch_string(2), -1)); - - yield results.next_async(); - - check_cancel(cancellable, "list_email_async"); - } while (!results.finished); - - return list; - } - - public async Gee.List? list_email_by_sparse_id_async( - Transaction? transaction, Gee.Collection ids, - Cancellable? cancellable = null) throws Error { - - Gee.List list = new Gee.ArrayList(); - - foreach (OutboxEmailIdentifier id in ids) { - Geary.Sqlite.SmtpOutboxRow? row = yield fetch_email_internal_async(transaction, - id, cancellable); - if (row != null) - list.add(row); - } - - return list.size > 0 ? list : null; - } - - public async int fetch_position_async(Transaction? transaction, int64 ordering, - Cancellable? cancellable) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.fetch_position_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT ordering FROM SmtpOutboxTable ORDER BY ordering"); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "fetch_position_async"); - - int position = 1; - while (!results.finished) { - if (results.fetch_int64(0) == ordering) - return position; - - yield results.next_async(); - - check_cancel(cancellable, "fetch_position_async"); - - position++; - } - - // not found - return -1; - } - - // Fetch an email given an outbox ID. - public async Geary.Sqlite.SmtpOutboxRow? fetch_email_async(Transaction? transaction, - OutboxEmailIdentifier id, Cancellable? cancellable = null) throws Error { - return yield fetch_email_internal_async(transaction, id, cancellable); - } - - private async Geary.Sqlite.SmtpOutboxRow? fetch_email_internal_async(Transaction? transaction, - OutboxEmailIdentifier id, Cancellable? cancellable = null) throws Error { - Transaction locked = yield obtain_lock_async(transaction, - "SmtpOutboxTable.fetch_email_internal_async", cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, ordering, message FROM SmtpOutboxTable " - + "WHERE ordering = ?"); - query.bind_int64(0, id.ordering); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "fetch_email_internal_async"); - - if (results.finished) - return null; - - SmtpOutboxRow? ret = new SmtpOutboxRow(this, results.fetch_int64(0), results.fetch_int64(1), - results.fetch_string(2), -1); - - check_cancel(cancellable, "fetch_email_internal_async"); - return ret; - } - - // Fetch an email given a database row ID. - private async Geary.Sqlite.SmtpOutboxRow? fetch_email_by_row_id_async(Transaction? transaction, - int64 id, Cancellable? cancellable = null) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.fetch_email_by_row_id_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "SELECT id, ordering, message FROM SmtpOutboxTable WHERE id = ?"); - query.bind_int64(0, id); - - SQLHeavy.QueryResult results = yield query.execute_async(); - check_cancel(cancellable, "fetch_email_by_row_id_async"); - - if (results.finished) - return null; - - SmtpOutboxRow? ret = new SmtpOutboxRow(this, results.fetch_int64(0), results.fetch_int64(1), - results.fetch_string(2), -1); - - check_cancel(cancellable, "fetch_email_by_row_id_async"); - return ret; - } - - public async void remove_single_email_async(Transaction? transaction, OutboxEmailIdentifier id, - Cancellable? cancellable = null) throws Error { - Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.remove_email_async", - cancellable); - - SQLHeavy.Query query = locked.prepare( - "DELETE FROM SmtpOutboxTable WHERE ordering=?"); - query.bind_int64(0, id.ordering); - - yield query.execute_async(); - } -} - diff --git a/src/engine/util/util-string.vala b/src/engine/util/util-string.vala index 2d6e4dcd..e065947b 100644 --- a/src/engine/util/util-string.vala +++ b/src/engine/util/util-string.vala @@ -26,7 +26,7 @@ public int ascii_cmpi(string a, string b) { char *aptr = a; char *bptr = b; for (;;) { - int diff = *aptr - *bptr; + int diff = (int) (*aptr).tolower() - (int) (*bptr).tolower(); if (diff != 0) return diff;