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.
This commit is contained in:
Jim Nelson 2012-06-14 14:47:53 -07:00
parent bbc0da0b39
commit 0e2a533438
79 changed files with 4492 additions and 3865 deletions

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ build/
/gearyd
/geary-mailer
/geary-console
/geary-db-test
*.xz
*.swp
vapi/gmime-2.6/gmime-2.6.gi

View file

@ -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))

4
debian/control vendored
View file

@ -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.

View file

@ -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})

42
sql/version-004.sql Normal file
View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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() {

122
src/db-test/main.vala Normal file
View file

@ -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 <data-dir>\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();
}

View file

@ -0,0 +1,9 @@
CREATE TABLE TestTable (
id INTEGER PRIMARY KEY,
str TEXT,
num INTEGER
);
CREATE INDEX TestTableIntIndex ON TestTable(num);

View file

@ -0,0 +1,7 @@
CREATE TABLE AnotherTable (
id INTEGER PRIMARY KEY,
str TEXT
);
CREATE INDEX AnotherTableIndex ON AnotherTable(str)

View file

@ -0,0 +1,5 @@
CREATE TABLE MultiTable (
str TEXT
);

View file

@ -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",

View file

@ -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();
}
}

View file

@ -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<Geary.EmailIdentifier> list = new Gee.ArrayList<Geary.EmailIdentifier>();
list.add(email_id);
yield folder.remove_email_async(list, cancellable);
return null;
}

View file

@ -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<Geary.Attachment> 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.

View file

@ -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;
}

View file

@ -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.

View file

@ -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
}

334
src/engine/db/db-connection.vala Executable file
View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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<TransactionAsyncJob>? 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<TransactionAsyncJob>.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;
}
}

View file

@ -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;
}
}

View file

@ -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<string, int>? 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<string, int>(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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

147
src/engine/db/db.vala Normal file
View file

@ -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);
}
}
}

View file

@ -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<Geary.FolderPath, FolderReference> folder_refs =
new Gee.HashMap<Geary.FolderPath, FolderReference>(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<Geary.ImapDB.Folder> 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<Geary.FolderPath, int64?> id_map = new Gee.HashMap<
Geary.FolderPath, int64?>(Hashable.hash_func, Equalable.equal_func);
Gee.HashMap<Geary.FolderPath, Geary.Imap.FolderProperties> 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<Geary.ImapDB.Folder> folders = new Gee.ArrayList<Geary.ImapDB.Folder>();
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);
}
}

View file

@ -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);
}
}

File diff suppressed because it is too large Load diff

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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";
}
}

View file

@ -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);
}
}

View file

@ -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<OutboxRow> outbox_queue = new NonblockingMailbox<OutboxRow>();
// 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<OutboxRow> list = new Gee.ArrayList<OutboxRow>();
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<SmtpOutboxEmailIdentifier> list = new Gee.ArrayList<SmtpOutboxEmailIdentifier>();
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<SmtpOutboxEmailIdentifier> list = new Gee.ArrayList<SmtpOutboxEmailIdentifier>();
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<Geary.Email>? 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<Geary.Email>? 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<Geary.Email>();
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<Geary.Email>? 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<Geary.Email>? 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<Geary.Email>();
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<Geary.Email>? list_email_by_sparse_id_async(
Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields,
Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error {
check_open();
Gee.List<Geary.Email> list = new Gee.ArrayList<Geary.Email>();
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<Geary.EmailIdentifier, Geary.Email.Field>?
list_local_email_fields_async(Gee.Collection<Geary.EmailIdentifier> ids,
Cancellable? cancellable = null) throws Error {
check_open();
Gee.Map<Geary.EmailIdentifier, Geary.Email.Field> 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<Geary.EmailIdentifier> 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<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable) throws Error {
Gee.List<Geary.EmailIdentifier> removed = new Gee.ArrayList<Geary.EmailIdentifier>();
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<Geary.EmailIdentifier> list = new Gee.ArrayList<Geary.EmailIdentifier>();
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;
}
}

View file

@ -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<string, string?> delims = new Gee.HashMap<string, string?>();
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() {

View file

@ -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);
}
/**

View file

@ -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<ClientSession> sessions = new Gee.HashSet<ClientSession>();
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();
}
}

View file

@ -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() {

View file

@ -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<FolderPath, Imap.FolderProperties> properties_map = new Gee.HashMap<
FolderPath, Imap.FolderProperties>(Hashable.hash_func, Equalable.equal_func);
private SmtpOutboxFolder? outbox = null;
private Gee.HashMap<FolderPath, GenericImapFolder> existing_folders = new Gee.HashMap<
FolderPath, GenericImapFolder>(Hashable.hash_func, Equalable.equal_func);
private Gee.HashSet<FolderPath> local_only = new Gee.HashSet<FolderPath>(
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<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error {
Gee.Collection<Geary.Sqlite.Folder>? local_list = null;
check_open();
Gee.Collection<ImapDB.Folder>? 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<Geary.Folder> engine_list = new Gee.ArrayList<Geary.Folder>();
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<Geary.Folder>();
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) {

View file

@ -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<Geary.Email>? 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<Geary.Email>? 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<Geary.Email>? 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<Geary.Email>? 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<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
Gee.Collection<Geary.EmailIdentifier> 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<Geary.EmailIdentifier> 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<EmailIdentifier> 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<Geary.Email>? 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);

View file

@ -36,9 +36,9 @@ private class Geary.GmailAccount : Geary.GenericImapAccount {
private static Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>? 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<Geary.FolderPath, Geary.SpecialFolderType>(
@ -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

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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<Geary.EmailIdentifier>(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);

View file

@ -33,9 +33,9 @@ private class Geary.YahooAccount : Geary.GenericImapAccount {
private static Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>? 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<Geary.FolderPath, Geary.SpecialFolderType>(
@ -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);
}

View file

@ -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);
}

View file

@ -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<Geary.Sqlite.SmtpOutboxRow> outbox_queue =
new NonblockingMailbox<Geary.Sqlite.SmtpOutboxRow>();
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<Geary.Sqlite.SmtpOutboxRow>? 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<OutboxEmailIdentifier> list = new Gee.ArrayList<OutboxEmailIdentifier>();
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<Geary.Email>? 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<Geary.Email>? 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<Geary.Sqlite.SmtpOutboxRow>? row_list = yield local_folder.list_email_async(
transaction, id, count, cancellable);
if (row_list == null || row_list.size == 0)
return null;
Gee.List<Geary.Email> list = new Gee.ArrayList<Geary.Email>();
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<Geary.Email>? list_email_by_sparse_id_async(
Gee.Collection<Geary.EmailIdentifier> _ids, Geary.Email.Field required_fields,
Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error {
Gee.List<OutboxEmailIdentifier> ids = new Gee.ArrayList<OutboxEmailIdentifier>();
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<Geary.Sqlite.SmtpOutboxRow>? 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<Geary.Email> list = new Gee.ArrayList<Geary.Email>();
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<Geary.EmailIdentifier, Geary.Email.Field>?
list_local_email_fields_async(Gee.Collection<Geary.EmailIdentifier> 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<Geary.EmailIdentifier> 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<OutboxEmailIdentifier> list = new Gee.ArrayList<OutboxEmailIdentifier>();
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;
}
}

View file

@ -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) {

View file

@ -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<SQLHeavy.Table, Geary.Sqlite.Table> 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));
}
}

View file

@ -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;
}
}
}

View file

@ -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));
}
}

View file

@ -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" : "");
}
}

View file

@ -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<Geary.FolderPath, FolderReference> folder_refs =
new Gee.HashMap<Geary.FolderPath, FolderReference>(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<Geary.Sqlite.Folder> 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<FolderRow> 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<Geary.Sqlite.Folder> folders = new Gee.ArrayList<Geary.Sqlite.Folder>();
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);
}
}

View file

@ -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<int64?>? 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<GMime.Part> 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<Geary.Email>? 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<MessageLocationRow>? 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<Geary.Email>? 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<Geary.Email> singleton = new Gee.ArrayList<Geary.Email>();
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<MessageLocationRow>? 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<Geary.Email>? do_list_email_async(Transaction transaction,
Gee.List<MessageLocationRow>? 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<Geary.Email> emails = new Gee.ArrayList<Geary.Email>();
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<MessageAttachmentRow> 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<Geary.EmailIdentifier> to_mark, Geary.EmailFlags? flags_to_add,
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error {
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> 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<Geary.EmailIdentifier, Geary.EmailFlags> get_email_flags_async(
Gee.List<Geary.EmailIdentifier> to_get, Cancellable? cancellable) throws Error {
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> 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<Geary.EmailIdentifier,
Geary.EmailFlags> 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<GMime.Part> 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<Geary.EmailIdentifier, Geary.Email.Field>? list_email_fields_by_id_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
check_open();
if (ids.size == 0)
return null;
Gee.HashMap<Geary.EmailIdentifier, Geary.Email.Field> 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();
}
}

View file

@ -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);
}
}

View file

@ -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<FolderRow> 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<FolderRow> rows = new Gee.ArrayList<FolderRow>();
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<string> 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);
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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<MessageAttachmentRow>? 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<MessageAttachmentRow> list = new Gee.ArrayList<MessageAttachmentRow>();
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");
}
}

View file

@ -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;
}
}

View file

@ -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<MessageLocationRow>? 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<MessageLocationRow> list = new Gee.ArrayList<MessageLocationRow>();
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<MessageLocationRow>? 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<MessageLocationRow>? list = new Gee.ArrayList<MessageLocationRow>();
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);
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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<int64?>? 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<int64?> list = new Gee.ArrayList<int64?>();
do {
list.add(result.fetch_int64(0));
yield result.next_async();
check_cancel(cancellable, "search_for_duplicates_async");
} while (!result.finished);
return list;
}
}

View file

@ -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);
}
}

View file

@ -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<Geary.Sqlite.SmtpOutboxRow>? 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<SmtpOutboxRow> list = new Gee.ArrayList<SmtpOutboxRow>();
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<Geary.Sqlite.SmtpOutboxRow>? list_email_by_sparse_id_async(
Transaction? transaction, Gee.Collection<OutboxEmailIdentifier> ids,
Cancellable? cancellable = null) throws Error {
Gee.List<SmtpOutboxRow> list = new Gee.ArrayList<SmtpOutboxRow>();
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();
}
}

View file

@ -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;