From 0e2a533438ddf21ba675f85957fac729a178c348 Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Thu, 14 Jun 2012 14:47:53 -0700 Subject: [PATCH] Remove SQLHeavy: Closes #5034 It is done. Initial implementation of the new database subsystem These pieces represent the foundation for ticket #5034 Expanded transactions, added VersionedDatabase Further expansions of the async code. Moved async pool logic into Database, where it realistically belongs. Further improvements. Introduced geary-db-test. Added SQL create and update files for Geary.Db version-001 to version-003 are exact copies of the SQLHeavy scripts to ensure no slight changes when migrating. version-004 upgrades the database to remove the ImapFolderPropertiesTable and ImapMessagePropertiesTable, now that the database code is pure IMAP. When we support other messaging systems (such as POP3), those subsystems will need to code their own database layers OR rely on the IMAP schema and simply ignore the IMAP-specific fields. ImapDB.Account fleshed out ImapDB.Folder is commented out, however. Need to port next. ImapDB.Folder fleshed out MessageTable, MessageLocationTable, and AttachementTable are now handled inside ImapDB.Folder. chmod -x imap-db-database.vala OutboxEmailIdentifier/Properties -> SmtpOutboxEmailIdentifier/Properties Moved SmtpOutboxFolderRoot into its own source file SmtpOutboxFolder ported to new database code Move Engine implementations to ImapDB. Integration and cleanup of new database code with main source This commit performs the final integration steps to move Geary completely over to the new database model. This also cleans out the old SQLHeavy-based code and fixes a handful of small bugs that were detected during basic test runs. Moved Outbox to ImapDB As the Outbox is tied to the database that ImapDB runs, move the Outbox code into that folder. Outbox fixes and better parameter checking Bumped Database thread pool count and made them exclusive My reasoning is that there may be a need for a lot of threads at once (when a big batch of commands comes in, especially at startup). If performance looks ok, we might consider relaxing this later. --- .gitignore | 1 + Makefile.in | 2 +- debian/control | 4 +- sql/CMakeLists.txt | 8 +- sql/{Create.sql => version-001.sql} | 0 sql/{Version-002.sql => version-002.sql} | 0 sql/{Version-003.sql => version-003.sql} | 0 sql/version-004.sql | 42 + src/CMakeLists.txt | 81 +- src/client/geary-application.vala | 2 +- src/client/geary-controller.vala | 5 +- src/db-test/main.vala | 122 ++ src/db-test/version-001.sql | 9 + src/db-test/version-002.sql | 7 + src/db-test/version-003.sql | 5 + src/engine/api/geary-account-information.vala | 32 +- src/engine/api/geary-account-settings.vala | 31 + src/engine/api/geary-batch-operations.vala | 17 +- src/engine/api/geary-email.vala | 6 +- src/engine/api/geary-engine-account.vala | 13 +- src/engine/api/geary-engine.vala | 13 +- src/engine/db/database-error.vala | 19 + src/engine/db/db-connection.vala | 334 +++++ src/engine/db/db-context.vala | 36 + src/engine/db/db-database.vala | 246 ++++ src/engine/db/db-result.vala | 207 +++ src/engine/db/db-statement.vala | 233 ++++ src/engine/db/db-synchronous-mode.vala | 40 + src/engine/db/db-transaction-async-job.vala | 81 ++ src/engine/db/db-transaction-outcome.vala | 40 + src/engine/db/db-transaction-type.vala | 48 + src/engine/db/db-versioned-database.vala | 73 + src/engine/db/db.vala | 147 ++ src/engine/imap-db/imap-db-account.vala | 456 +++++++ src/engine/imap-db/imap-db-database.vala | 14 + src/engine/imap-db/imap-db-folder.vala | 1203 +++++++++++++++++ src/engine/imap-db/imap-db-message-row.vala | 276 ++++ .../outbox/smtp-outbox-email-identifier.vala | 6 +- .../outbox/smtp-outbox-email-properties.vala | 6 +- .../outbox/smtp-outbox-folder-root.vala | 14 + .../imap-db/outbox/smtp-outbox-folder.vala | 547 ++++++++ src/engine/imap/api/imap-account.vala | 31 +- .../imap/message/imap-message-data.vala | 7 +- .../imap-client-session-manager.vala | 19 +- .../imap/transport/imap-client-session.vala | 26 +- .../impl/geary-generic-imap-account.vala | 64 +- .../impl/geary-generic-imap-folder.vala | 53 +- src/engine/impl/geary-gmail-account.vala | 8 +- src/engine/impl/geary-gmail-folder.vala | 4 +- src/engine/impl/geary-other-account.vala | 8 +- src/engine/impl/geary-other-folder.vala | 4 +- .../impl/geary-send-replay-operations.vala | 43 +- src/engine/impl/geary-yahoo-account.vala | 8 +- src/engine/impl/geary-yahoo-folder.vala | 4 +- .../impl/outbox/smtp-outbox-folder.vala | 292 ---- .../nonblocking-abstract-semaphore.vala | 9 +- .../sqlite/abstract/sqlite-database.vala | 85 -- src/engine/sqlite/abstract/sqlite-row.vala | 38 - src/engine/sqlite/abstract/sqlite-table.vala | 94 -- .../sqlite/abstract/sqlite-transaction.vala | 98 -- src/engine/sqlite/api/sqlite-account.vala | 356 ----- src/engine/sqlite/api/sqlite-folder.vala | 777 ----------- .../sqlite/email/sqlite-folder-row.vala | 28 - .../sqlite/email/sqlite-folder-table.vala | 135 -- .../sqlite/email/sqlite-mail-database.vala | 61 - .../email/sqlite-message-attachment-row.vala | 39 - .../sqlite-message-attachment-table.vala | 90 -- .../email/sqlite-message-location-row.vala | 58 - .../email/sqlite-message-location-table.vala | 372 ----- .../sqlite/email/sqlite-message-row.vala | 226 ---- .../sqlite/email/sqlite-message-table.vala | 292 ---- .../sqlite/imap/sqlite-imap-database.vala | 30 - .../sqlite-imap-folder-properties-row.vala | 45 - .../sqlite-imap-folder-properties-table.vala | 93 -- .../sqlite-imap-message-properties-row.vala | 57 - .../sqlite-imap-message-properties-table.vala | 141 -- .../sqlite/smtp/sqlite-smtp-outbox-row.vala | 38 - .../sqlite/smtp/sqlite-smtp-outbox-table.vala | 196 --- src/engine/util/util-string.vala | 2 +- 79 files changed, 4492 insertions(+), 3865 deletions(-) rename sql/{Create.sql => version-001.sql} (100%) rename sql/{Version-002.sql => version-002.sql} (100%) rename sql/{Version-003.sql => version-003.sql} (100%) create mode 100644 sql/version-004.sql create mode 100644 src/db-test/main.vala create mode 100644 src/db-test/version-001.sql create mode 100644 src/db-test/version-002.sql create mode 100644 src/db-test/version-003.sql create mode 100644 src/engine/api/geary-account-settings.vala create mode 100755 src/engine/db/database-error.vala create mode 100755 src/engine/db/db-connection.vala create mode 100644 src/engine/db/db-context.vala create mode 100644 src/engine/db/db-database.vala create mode 100644 src/engine/db/db-result.vala create mode 100644 src/engine/db/db-statement.vala create mode 100644 src/engine/db/db-synchronous-mode.vala create mode 100755 src/engine/db/db-transaction-async-job.vala create mode 100644 src/engine/db/db-transaction-outcome.vala create mode 100755 src/engine/db/db-transaction-type.vala create mode 100644 src/engine/db/db-versioned-database.vala create mode 100644 src/engine/db/db.vala create mode 100644 src/engine/imap-db/imap-db-account.vala create mode 100644 src/engine/imap-db/imap-db-database.vala create mode 100644 src/engine/imap-db/imap-db-folder.vala create mode 100644 src/engine/imap-db/imap-db-message-row.vala rename src/engine/{impl => imap-db}/outbox/smtp-outbox-email-identifier.vala (69%) rename src/engine/{impl => imap-db}/outbox/smtp-outbox-email-properties.vala (62%) create mode 100644 src/engine/imap-db/outbox/smtp-outbox-folder-root.vala create mode 100644 src/engine/imap-db/outbox/smtp-outbox-folder.vala delete mode 100644 src/engine/impl/outbox/smtp-outbox-folder.vala delete mode 100644 src/engine/sqlite/abstract/sqlite-database.vala delete mode 100644 src/engine/sqlite/abstract/sqlite-row.vala delete mode 100644 src/engine/sqlite/abstract/sqlite-table.vala delete mode 100644 src/engine/sqlite/abstract/sqlite-transaction.vala delete mode 100644 src/engine/sqlite/api/sqlite-account.vala delete mode 100644 src/engine/sqlite/api/sqlite-folder.vala delete mode 100644 src/engine/sqlite/email/sqlite-folder-row.vala delete mode 100644 src/engine/sqlite/email/sqlite-folder-table.vala delete mode 100644 src/engine/sqlite/email/sqlite-mail-database.vala delete mode 100644 src/engine/sqlite/email/sqlite-message-attachment-row.vala delete mode 100644 src/engine/sqlite/email/sqlite-message-attachment-table.vala delete mode 100644 src/engine/sqlite/email/sqlite-message-location-row.vala delete mode 100644 src/engine/sqlite/email/sqlite-message-location-table.vala delete mode 100644 src/engine/sqlite/email/sqlite-message-row.vala delete mode 100644 src/engine/sqlite/email/sqlite-message-table.vala delete mode 100644 src/engine/sqlite/imap/sqlite-imap-database.vala delete mode 100644 src/engine/sqlite/imap/sqlite-imap-folder-properties-row.vala delete mode 100644 src/engine/sqlite/imap/sqlite-imap-folder-properties-table.vala delete mode 100644 src/engine/sqlite/imap/sqlite-imap-message-properties-row.vala delete mode 100644 src/engine/sqlite/imap/sqlite-imap-message-properties-table.vala delete mode 100644 src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala delete mode 100644 src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala 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;