Send outgoing messages via Outbox folder: Closes #4569

Squashed commit of many patches that merged Eric's outbox patch
as well as additional changes to upgrade the database rather than
require it be wiped and some refactoring suggested by the Outbox
implementation.  Also updated Outbox to be fully atomic via
Transactions.
This commit is contained in:
Eric Gregory 2012-06-11 12:03:57 -07:00 committed by Jim Nelson
parent b7a2993d6f
commit 6963063fd5
36 changed files with 1035 additions and 100 deletions

13
sql/Version-003.sql Normal file
View file

@ -0,0 +1,13 @@
--
-- SmtpOutboxTable
--
CREATE TABLE SmtpOutboxTable (
id INTEGER PRIMARY KEY,
ordering INTEGER,
message TEXT
);
CREATE INDEX SmtpOutboxOrderingIndex ON SmtpOutboxTable(ordering ASC);

View file

@ -96,6 +96,9 @@ engine/impl/geary-receive-replay-operations.vala
engine/impl/geary-replay-operation.vala
engine/impl/geary-replay-queue.vala
engine/impl/geary-send-replay-operations.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
@ -146,6 +149,8 @@ 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

View file

@ -86,6 +86,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
static bool log_replay_queue = false;
static bool log_conversations = false;
static bool log_periodic = false;
static bool log_transactions = false;
static bool version = false;
const OptionEntry[] options = {
{ "debug", 0, 0, OptionArg.NONE, ref log_debug, N_("Output debugging information"), null },
@ -94,6 +95,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
{ "log-replay-queue", 0, 0, OptionArg.NONE, ref log_replay_queue, N_("Output replay queue log"), null },
{ "log-serializer", 0, 0, OptionArg.NONE, ref log_serializer, N_("Output serializer log"), null },
{ "log-periodic", 0, 0, OptionArg.NONE, ref log_periodic, N_("Output periodic activity"), null },
{ "log-transactions", 0, 0, OptionArg.NONE, ref log_transactions, N_("Output database transactions"), null },
{ "version", 'V', 0, OptionArg.NONE, ref version, N_("Display program version"), null },
{ null }
};
@ -132,6 +134,9 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
if (log_periodic)
Geary.Logging.enable_flags(Geary.Logging.Flag.PERIODIC);
if (log_transactions)
Geary.Logging.enable_flags(Geary.Logging.Flag.TRANSACTIONS);
if (log_debug)
Geary.Logging.log_to(stdout);
@ -177,7 +182,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
if (this.account != null)
this.account.report_problem.connect(on_report_problem);
controller.connect_account(this.account);
controller.connect_account_async.begin(this.account, null);
}
private void initialize_account() {
@ -228,7 +233,12 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
if (success) {
account_information.store_async.begin(cancellable);
try {
set_account(account_information.get_account());
} catch (Error err) {
// TODO: Handle more gracefully
error("Unable to retrieve email account: %s", err.message);
}
} else {
Geary.AccountInformation new_account_information =
request_account_information(account_information);
@ -265,7 +275,13 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
account_information.store_async.begin(cancellable);
account_information.credentials.pass = password;
try {
set_account(account_information.get_account());
} catch (Error err) {
// TODO: Handle more gracefull
error("Unable to retrieve email account: %s", err.message);
}
}
private string? get_username() {
@ -375,6 +391,8 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
if (controller.main_window != null)
controller.main_window.destroy();
controller.disconnect_account_async.begin(null);
return true;
}

View file

@ -120,7 +120,7 @@ public class GearyController {
}
~GearyController() {
connect_account(null);
assert(account == null);
}
private void add_accelerator(string accelerator, string action) {
@ -242,7 +242,7 @@ public class GearyController {
return entries;
}
public void connect_account(Geary.EngineAccount? new_account) {
public async void connect_account_async(Geary.EngineAccount? new_account, Cancellable? cancellable) {
if (account == new_account)
return;
@ -261,12 +261,29 @@ public class GearyController {
main_window.title = GearyApplication.NAME;
main_window.folder_list.remove_all_branches();
try {
yield account.close_async(cancellable);
} catch (Error close_err) {
debug("Unable to close account %s: %s", account.to_string(), close_err.message);
}
}
account = new_account;
// Connect the new account, if any.
if (account != null) {
try {
yield account.open_async(cancellable);
} catch (Error open_err) {
// TODO: Better error reporting to user
debug("Unable to open account %s: %s", account.to_string(), open_err.message);
account = null;
GearyApplication.instance.panic();
}
account.folders_added_removed.connect(on_folders_added_removed);
// Personality-specific setup.
@ -285,6 +302,10 @@ public class GearyController {
}
}
public async void disconnect_account_async(Cancellable? cancellable) throws Error {
yield connect_account_async(null, cancellable);
}
private bool is_viewed_conversation(Geary.Conversation? conversation) {
return conversation != null && selected_conversations.length > 0 &&
selected_conversations[0] == conversation;

View file

@ -91,6 +91,9 @@ public class FolderList : Sidebar.Tree {
case Geary.SpecialFolderType.TRASH:
return new ThemedIcon("user-trash");
case Geary.SpecialFolderType.OUTBOX:
return new ThemedIcon("mail-outbox");
default:
assert_not_reached();
}

View file

@ -44,7 +44,8 @@ public class FormattedMessageData : Object {
assert(email.fields.fulfills(MessageListStore.REQUIRED_FIELDS));
string who = "";
if (folder.get_special_folder_type() == Geary.SpecialFolderType.SENT &&
if ((folder.get_special_folder_type() == Geary.SpecialFolderType.SENT ||
folder.get_special_folder_type() == Geary.SpecialFolderType.OUTBOX) &&
email.to != null && email.to.size > 0) {
who = email.to[0].get_short_address();
} else if (email.from != null && email.from.size > 0) {

View file

@ -109,22 +109,20 @@ public class Geary.AccountInformation : Object {
return false;
}
public Geary.EngineAccount get_account() {
File? user_data_dir = Geary.Engine.user_data_dir;
File? resource_dir = Geary.Engine.resource_dir;
public Geary.EngineAccount get_account() throws Error {
Geary.Sqlite.Account sqlite_account =
new Geary.Sqlite.Account(credentials, user_data_dir, resource_dir);
new Geary.Sqlite.Account(credentials.user);
switch (service_provider) {
case ServiceProvider.GMAIL:
return new GmailAccount("Gmail account %s".printf(credentials.to_string()),
credentials.user, this, user_data_dir, new Geary.Imap.Account(
credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account(
GmailAccount.IMAP_ENDPOINT, GmailAccount.SMTP_ENDPOINT, credentials, this),
sqlite_account);
case ServiceProvider.YAHOO:
return new YahooAccount("Yahoo account %s".printf(credentials.to_string()),
credentials.user, this, user_data_dir, new Geary.Imap.Account(
credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account(
YahooAccount.IMAP_ENDPOINT, YahooAccount.SMTP_ENDPOINT, credentials, this),
sqlite_account);
@ -142,11 +140,12 @@ public class Geary.AccountInformation : Object {
smtp_flags, Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC);
return new OtherAccount("Other account %s".printf(credentials.to_string()),
credentials.user, this, user_data_dir, new Geary.Imap.Account(imap_endpoint,
credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account(imap_endpoint,
smtp_endpoint, credentials, this), sqlite_account);
default:
assert_not_reached();
throw new EngineError.NOT_FOUND("Service provider of type %s not known",
service_provider.to_string());
}
}

View file

@ -12,29 +12,47 @@ public interface Geary.Account : Object {
DATABASE_FAILURE
}
public signal void opened();
public signal void closed();
public signal void report_problem(Geary.Account.Problem problem, Geary.Credentials? credentials,
Error? err);
public signal void folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed);
/**
* Signal notification method for subclasses to use.
*/
protected abstract void notify_opened();
/**
* Signal notification method for subclasses to use.
*/
protected abstract void notify_closed();
/**
* Signal notification method for subclasses to use.
*/
protected abstract void notify_report_problem(Geary.Account.Problem problem,
Geary.Credentials? credentials, Error? err);
/**
* Signal notification method for subclasses to use.
*/
protected abstract void notify_folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed);
/**
* This method returns which Geary.Email.Field fields must be available in a Geary.Email to
* write (or save or store) the message to the backing medium. Different implementations will
* have different requirements, which must be reconciled.
*
* In this case, Geary.Email.Field.NONE means "any".
*
* If a write operation is attempted on an email that does not have all these fields fulfilled,
* an EngineError.INCOMPLETE_MESSAGE will be thrown.
*/
public abstract Geary.Email.Field get_required_fields_for_writing();
public abstract async void open_async(Cancellable? cancellable = null) throws Error;
/**
*
*/
public abstract async void close_async(Cancellable? cancellable = null) throws Error;
/**
* Lists all the folders found under the parent path unless it's null, in which case it lists
@ -60,6 +78,9 @@ public interface Geary.Account : Object {
public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error;
/**
* Used only for debugging. Should not be used for user-visible strings.
*/
public abstract string to_string();
}

View file

@ -104,7 +104,9 @@ public class Geary.ConversationMonitor : Object {
id_ascending.remove(email);
id_descending.remove(email);
message_ids.remove_all(email.get_ancestors());
Gee.Set<RFC822.MessageID>? ancestors = email.get_ancestors();
if (ancestors != null)
message_ids.remove_all(ancestors);
}
private static int compare_date_ascending(Email a, Email b) {
@ -491,6 +493,9 @@ public class Geary.ConversationMonitor : Object {
}
private void process_email(Gee.List<Geary.Email> emails) {
Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::process_email: %d emails",
folder.to_string(), emails.size);
Gee.HashSet<Conversation> new_conversations = new Gee.HashSet<Conversation>();
Gee.MultiMap<Conversation, Geary.Email> appended_conversations = new Gee.HashMultiMap<
Conversation, Geary.Email>();

View file

@ -17,15 +17,27 @@
* fields in the same Folder that match the email's position within it. The ordering field may
* or may not be the actual unique identifier; in IMAP, for example, it is, while in other systems
* it may not be.
*
* TODO:
* EmailIdentifier currently does not verify it is in the same Folder as the other EmailIdentifier
* passed to equals() and compare(). This may be added in the future.
*/
public abstract class Geary.EmailIdentifier : Object, Geary.Equalable, Geary.Comparable, Geary.Hashable {
public abstract int64 ordering { get; protected set; }
public int64 ordering { get; protected set; }
protected EmailIdentifier(int64 ordering) {
this.ordering = ordering;
}
public virtual uint to_hash() {
return Geary.Hashable.int64_hash(ordering);
}
// Virtual default implementation not provided because base class *must* verify that the
// Equalable is of its own type.
public abstract bool equals(Geary.Equalable other);
public abstract uint to_hash();
public virtual int compare(Geary.Comparable o) {
Geary.EmailIdentifier? other = o as Geary.EmailIdentifier;
if (other == null)
@ -43,6 +55,8 @@ public abstract class Geary.EmailIdentifier : Object, Geary.Equalable, Geary.Com
return 0;
}
public abstract string to_string();
public virtual string to_string() {
return ordering.to_string();
}
}

View file

@ -64,6 +64,20 @@ public class Geary.Email : Object {
public inline bool require(Field required_fields) {
return is_all_set(required_fields);
}
public string to_list_string() {
StringBuilder builder = new StringBuilder();
foreach (Field f in all()) {
if (is_all_set(f)) {
if (!String.is_empty(builder.str))
builder.append(", ");
builder.append(f.to_string());
}
}
return builder.str;
}
}
/**

View file

@ -261,7 +261,7 @@ public interface Geary.Folder : Object {
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async bool create_email_async(Geary.Email email, Cancellable? cancellable = null)
public abstract async bool create_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable = null)
throws Error;
/**

View file

@ -13,7 +13,8 @@ public enum Flag {
SERIALIZER,
REPLAY,
CONVERSATIONS,
PERIODIC;
PERIODIC,
TRANSACTIONS;
public inline bool is_all_set(Flag flags) {
return (flags & this) == flags;

View file

@ -11,7 +11,8 @@ public enum Geary.SpecialFolderType {
FLAGGED,
ALL_MAIL,
SPAM,
TRASH
TRASH,
OUTBOX
}
public class Geary.SpecialFolder : Object {

View file

@ -45,6 +45,16 @@ private class Geary.Imap.Account : Object {
smtp = new Geary.Smtp.ClientSession(smtp_endpoint);
}
public async void open_async(Cancellable? cancellable) throws Error {
// Nothing to do -- ClientSessionManager deals with maintaining connections
// TODO: Start ClientSessionManager here, not in ctor
}
public async void close_async(Cancellable? cancellable) throws Error {
// Nothing to do -- ClientSessionManager deals with maintaining connections
// TODO: Stop ClientSessionManager here
}
public async Gee.Collection<Geary.Imap.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error {
Geary.FolderPath? processed = process_path(parent, null,
@ -175,10 +185,8 @@ private class Geary.Imap.Account : Object {
return parent;
}
public async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null)
public async void send_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable = null)
throws Error {
Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(composed);
yield smtp.login_async(cred, cancellable);
try {
yield smtp.send_email_async(rfc822, cancellable);

View file

@ -5,13 +5,12 @@
*/
private class Geary.Imap.EmailIdentifier : Geary.EmailIdentifier {
public override int64 ordering { get; protected set; }
public Imap.UID uid { get; private set; }
public EmailIdentifier(Imap.UID uid) {
base (uid.value);
this.uid = uid;
ordering = uid.value;
}
public override uint to_hash() {
@ -26,7 +25,7 @@ private class Geary.Imap.EmailIdentifier : Geary.EmailIdentifier {
if (this == other)
return true;
return uid.value == other.uid.value;
return ordering == other.ordering;
}
public override string to_string() {

View file

@ -21,7 +21,17 @@ public abstract class Geary.AbstractAccount : Object, Geary.Account {
folders_added_removed(added, removed);
}
public abstract Geary.Email.Field get_required_fields_for_writing();
protected virtual void notify_opened() {
opened();
}
protected virtual void notify_closed() {
closed();
}
public abstract async void open_async(Cancellable? cancellable = null) throws Error;
public abstract async void close_async(Cancellable? cancellable = null) throws Error;
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error;

View file

@ -57,31 +57,81 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
public abstract async int get_email_count_async(Cancellable? cancellable = null) throws Error;
public abstract async bool create_email_async(Geary.Email email, Cancellable? cancellable = null)
throws Error;
public abstract async bool create_email_async(Geary.RFC822.Message rfc822,
Cancellable? cancellable = null) throws Error;
public abstract async Gee.List<Geary.Email>? list_email_async(int low, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null)
throws Error;
public abstract void lazy_list_email(int low, int count, Geary.Email.Field required_fields,
Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null);
public virtual void lazy_list_email(int low, int count, Geary.Email.Field required_fields,
Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) {
do_lazy_list_email_async.begin(low, count, required_fields, flags, cb, cancellable);
}
private async void do_lazy_list_email_async(int low, int count, Geary.Email.Field required_fields,
Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) {
try {
Gee.List<Geary.Email>? list = yield list_email_async(low, count, required_fields, flags,
cancellable);
if (list != null && list.size > 0)
cb(list, null);
cb(null, null);
} catch (Error err) {
cb(null, err);
}
}
public abstract 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;
public abstract void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count,
public virtual void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
Cancellable? cancellable = null);
Cancellable? cancellable = null) {
do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, flags, cb, cancellable);
}
private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier initial_id, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
Cancellable? cancellable) {
try {
Gee.List<Geary.Email>? list = yield list_email_by_id_async(initial_id, count,
required_fields, flags, cancellable);
if (list != null && list.size > 0)
cb(list, null);
cb(null, null);
} catch (Error err) {
cb(null, err);
}
}
public abstract async Gee.List<Geary.Email>? list_email_by_sparse_id_async(
Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
Cancellable? cancellable = null) throws Error;
public abstract void lazy_list_email_by_sparse_id(Gee.Collection<Geary.EmailIdentifier> ids,
public virtual void lazy_list_email_by_sparse_id(Gee.Collection<Geary.EmailIdentifier> ids,
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
Cancellable? cancellable = null);
Cancellable? cancellable = null) {
do_lazy_list_email_by_sparse_id_async.begin(ids, required_fields, flags, cb, cancellable);
}
private async void do_lazy_list_email_by_sparse_id_async(Gee.Collection<Geary.EmailIdentifier> ids,
Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
Cancellable? cancellable) {
try {
Gee.List<Geary.Email>? list = yield list_email_by_sparse_id_async(ids,
required_fields, flags, cancellable);
if (list != null && list.size > 0)
cb(list, null);
cb(null, null);
} catch (Error err) {
cb(null, err);
}
}
public abstract async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error;

View file

@ -9,6 +9,7 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount {
private Sqlite.Account local;
private Gee.HashMap<FolderPath, Imap.FolderProperties> properties_map = new Gee.HashMap<
FolderPath, Imap.FolderProperties>(Hashable.hash_func, Equalable.equal_func);
private SmtpOutboxFolder? outbox = null;
public GenericImapAccount(string name, string username, AccountInformation? account_info,
File user_data_dir, Imap.Account remote, Sqlite.Account local) {
@ -24,10 +25,52 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount {
return properties_map.get(path);
}
public override Geary.Email.Field get_required_fields_for_writing() {
// Return the more restrictive of the two, which is the NetworkAccount's.
// TODO: This could be determined at runtime rather than fixed in stone here.
return Geary.Email.Field.HEADER | Geary.Email.Field.BODY;
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);
// need to back out local.open_async() if remote fails
try {
yield remote.open_async(cancellable);
} catch (Error err) {
// back out
try {
yield local.close_async(cancellable);
} catch (Error close_err) {
// ignored
}
throw err;
}
outbox = new SmtpOutboxFolder(remote, local.get_outbox());
notify_opened();
}
public override async void close_async(Cancellable? cancellable = null) throws Error {
// attempt to close both regardless of errors
Error? local_err = null;
try {
yield local.close_async(cancellable);
} catch (Error lclose_err) {
local_err = lclose_err;
}
Error? remote_err = null;
try {
yield remote.close_async(cancellable);
} catch (Error rclose_err) {
remote_err = rclose_err;
}
outbox = null;
if (local_err != null)
throw local_err;
if (remote_err != null)
throw remote_err;
}
public override async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
@ -50,6 +93,7 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount {
}
background_update_folders.begin(parent, engine_list, cancellable);
engine_list.add(outbox);
return engine_list;
}
@ -64,6 +108,10 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount {
public override async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error {
if (path.equals(outbox.get_path()))
return outbox;
Sqlite.Folder? local_folder = null;
try {
local_folder = (Sqlite.Folder) yield local.fetch_folder_async(path, cancellable);
@ -179,9 +227,10 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount {
return false;
}
public override async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null)
throws Error {
yield remote.send_email_async(composed, cancellable);
public override async void send_email_async(Geary.ComposedEmail composed,
Cancellable? cancellable = null) throws Error {
Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(composed);
yield outbox.create_email_async(rfc822, cancellable);
}
private void on_login_failed(Geary.Credentials? credentials) {

View file

@ -94,9 +94,8 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder {
return Geary.Folder.OpenState.OPENING;
}
public override async bool create_email_async(Geary.Email email, Cancellable? cancellable) throws Error {
check_open("create_email_async");
public override async bool create_email_async(Geary.RFC822.Message rfc822, Cancellable?
cancellable) throws Error {
throw new EngineError.READONLY("Engine currently read-only");
}

View file

@ -47,10 +47,13 @@ private class Geary.GmailAccount : Geary.GenericImapAccount {
private static void initialize_personality() {
Geary.FolderPath gmail_root = new Geary.FolderRoot(GMAIL_FOLDER, Imap.Account.ASSUMED_SEPARATOR,
true);
Geary.FolderRoot inbox_folder = new Geary.FolderRoot(Imap.Account.INBOX_NAME,
Imap.Account.ASSUMED_SEPARATOR, false);
Geary.FolderRoot outbox_folder = new SmtpOutboxFolderRoot();
special_folder_map = new SpecialFolderMap();
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.INBOX, _("Inbox"),
new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, false), 0));
inbox_folder, 0));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.DRAFTS, _("Drafts"),
gmail_root.get_child("Drafts"), 1));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SENT, _("Sent Mail"),
@ -61,13 +64,15 @@ private class Geary.GmailAccount : Geary.GenericImapAccount {
gmail_root.get_child("All Mail"), 4));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SPAM, _("Spam"),
gmail_root.get_child("Spam"), 5));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.OUTBOX,
_("Outbox"), outbox_folder, 6));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.TRASH, _("Trash"),
gmail_root.get_child("Trash"), 6));
gmail_root.get_child("Trash"), 7));
ignored_paths = new Gee.HashSet<Geary.FolderPath>(Hashable.hash_func, Equalable.equal_func);
ignored_paths.add(gmail_root);
ignored_paths.add(new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR,
true));
ignored_paths.add(inbox_folder);
ignored_paths.add(outbox_folder);
}
public override string get_user_folders_label() {

View file

@ -53,6 +53,7 @@ private class Geary.YahooAccount : Geary.GenericImapAccount {
FolderRoot spam_folder = new Geary.FolderRoot("Bulk Mail", Imap.Account.ASSUMED_SEPARATOR,
false);
FolderRoot trash_folder = new Geary.FolderRoot("Trash", Imap.Account.ASSUMED_SEPARATOR, false);
FolderRoot outbox_folder = new SmtpOutboxFolderRoot();
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.INBOX, _("Inbox"),
inbox_folder, 0));
@ -62,14 +63,17 @@ private class Geary.YahooAccount : Geary.GenericImapAccount {
sent_folder, 2));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SPAM, _("Spam"),
spam_folder, 3));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.OUTBOX,
_("Outbox"), outbox_folder, 4));
special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.TRASH, _("Trash"),
trash_folder, 4));
trash_folder, 5));
ignored_paths = new Gee.HashSet<Geary.FolderPath>(Hashable.hash_func, Equalable.equal_func);
ignored_paths.add(inbox_folder);
ignored_paths.add(drafts_folder);
ignored_paths.add(sent_folder);
ignored_paths.add(spam_folder);
ignored_paths.add(outbox_folder);
ignored_paths.add(trash_folder);
}

View file

@ -0,0 +1,23 @@
/* 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.OutboxEmailIdentifier : Geary.EmailIdentifier {
public OutboxEmailIdentifier(int64 ordering) {
base (ordering);
}
public override bool equals(Geary.Equalable o) {
EmailIdentifier? other = o as EmailIdentifier;
if (other == null)
return false;
if (this == other)
return true;
return ordering == other.ordering;
}
}

View file

@ -0,0 +1,15 @@
/* 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.OutboxEmailProperties : Geary.EmailProperties {
public OutboxEmailProperties() {
}
public override string to_string() {
return "OutboxProperties";
}
}

View file

@ -0,0 +1,307 @@
/* 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 {
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 override async bool 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 true;
}
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 override 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 override 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);
}
public override 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 {
// Not implemented.
}
// 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;
}
public override async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
// Not implemented.
}
public override async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
// Not implemented.
}
}

View file

@ -277,5 +277,9 @@ public class Geary.RFC822.PreviewText : Geary.RFC822.Text {
base (buffer);
}
public PreviewText.from_string(string preview) {
base (new Geary.Memory.StringBuffer(preview));
}
}

View file

@ -30,6 +30,10 @@ public class Geary.RFC822.Message : Object {
stock_from_gmime();
}
public Message.from_string(string full_email) throws RFC822Error {
this(new Geary.RFC822.Full(new Geary.Memory.StringBuffer(full_email)));
}
public Message.from_parts(Header header, Text body) throws RFC822Error {
GMime.StreamCat stream_cat = new GMime.StreamCat();
stream_cat.add_source(new GMime.StreamMem.with_buffer(header.buffer.get_array()));
@ -177,6 +181,43 @@ public class Geary.RFC822.Message : Object {
message.set_mime_part(email.message.get_mime_part());
}
public Geary.Email get_email(int position, Geary.EmailIdentifier id) throws Error {
Geary.Email email = new Geary.Email(position, id);
email.set_message_header(new Geary.RFC822.Header(new Geary.Memory.StringBuffer(
message.get_headers())));
email.set_send_date(new Geary.RFC822.Date(message.get_date_as_string()));
email.set_originators(from, new Geary.RFC822.MailboxAddresses.single(sender), null);
email.set_receivers(to, cc, bcc);
email.set_full_references(null, in_reply_to, references);
email.set_message_subject(subject);
email.set_message_body(new Geary.RFC822.Text(new Geary.Memory.StringBuffer(
message.get_body().to_string())));
email.set_message_preview(new Geary.RFC822.PreviewText.from_string(
preview_from_email(email)));
return email;
}
// Takes an e-mail object with a body and generates a preview. If there is no body
// or the body is the empty string, the empty string will be returned.
//
// Note that this is intended for outgoing messages, and as such we rely on the text
// section existing.
private string preview_from_email(Geary.Email email) {
try {
return Geary.String.safe_byte_substring(email.get_message().
get_first_mime_part_of_content_type("text/plain").to_string().
chug(), Geary.Email.MAX_PREVIEW_BYTES);
} catch (Error e) {
debug("Could not generate outbox preview: %s", e.message);
// fall through
}
return "";
}
private void stock_from_gmime() {
from = new RFC822.MailboxAddresses.from_rfc822_string(message.get_sender());
if (from.size == 0) {

View file

@ -62,10 +62,9 @@ public abstract class Geary.Sqlite.Database {
pre_upgrade(db_version);
try {
debug("Upgrading to %d", db_version);
string upgrade_contents;
FileUtils.get_contents(upgrade_script.get_path(), out upgrade_contents);
db.run(upgrade_contents);
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.
@ -78,7 +77,7 @@ public abstract class Geary.Sqlite.Database {
}
private File get_upgrade_script(int version) {
return File.new_for_path("%s/Version-%03d.sql".printf(schema_dir.get_path(), version));
return schema_dir.get_child("Version-%03d.sql".printf(version));
}
}

View file

@ -7,6 +7,7 @@
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;
@ -41,13 +42,12 @@ public class Geary.Sqlite.Transaction {
public async void begin_async(Cancellable? cancellable = null) throws Error {
assert(!is_locked);
#if TRACE_TRANSACTIONS
debug("[%s] claiming lock", to_string());
#endif
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);
#if TRACE_TRANSACTIONS
debug("[%s] lock claimed", to_string());
#endif
held_by = name;
Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] lock claimed", to_string());
}
private void resolve(bool commit, Cancellable? cancellable) throws Error {
@ -60,13 +60,11 @@ public class Geary.Sqlite.Transaction {
if (commit)
is_commit_required = false;
#if TRACE_TRANSACTIONS
debug("[%s] releasing lock", to_string());
#endif
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);
#if TRACE_TRANSACTIONS
debug("[%s] released lock", to_string());
#endif
held_by = null;
Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] released lock", to_string());
}
public SQLHeavy.Query prepare(string sql) throws Error {
@ -87,9 +85,8 @@ public class Geary.Sqlite.Transaction {
}
public void set_commit_required() {
#if TRACE_TRANSACTIONS
debug("[%s] commit required", to_string());
#endif
Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] commit required", to_string());
is_commit_required = true;
}

View file

@ -16,32 +16,65 @@ private class Geary.Sqlite.Account : Object {
}
private string name;
private ImapDatabase db;
private FolderTable folder_table;
private ImapFolderPropertiesTable folder_properties_table;
private MessageTable message_table;
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(Geary.Credentials cred, File user_data_dir, File resource_dir) {
name = "SQLite account for %s".printf(cred.to_string());
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);
db.upgrade();
} catch (Error err) {
error("Unable to open database: %s", err.message);
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)
@ -52,12 +85,16 @@ private class Geary.Sqlite.Account : Object {
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
@ -80,6 +117,8 @@ private class Geary.Sqlite.Account : Object {
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();
@ -111,6 +150,8 @@ private class Geary.Sqlite.Account : Object {
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);
@ -149,6 +190,8 @@ private class Geary.Sqlite.Account : Object {
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);
@ -163,6 +206,8 @@ private class Geary.Sqlite.Account : Object {
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)
@ -191,8 +236,14 @@ private class Geary.Sqlite.Account : Object {
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);

View file

@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database {
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 {
@ -48,5 +48,14 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database {
? 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

@ -14,7 +14,7 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table {
REMOVE_MARKER
}
public MessageLocationTable(Geary.Sqlite.Database db, SQLHeavy.Table table) {
internal MessageLocationTable(Geary.Sqlite.Database db, SQLHeavy.Table table) {
base (db, table);
}

View file

@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.ImapDatabase : Geary.Sqlite.MailDatabase {
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);
}

View file

@ -0,0 +1,38 @@
/* 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

@ -0,0 +1,196 @@
/* 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

@ -4,6 +4,10 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
// GLib's character-based substring function.
[CCode (cname = "g_utf8_substring")]
extern string glib_substring(string str, long start_pos, long end_pos);
namespace Geary.String {
public inline bool is_null_or_whitespace(string? str) {
@ -89,5 +93,16 @@ public string reduce_whitespace(string _s) {
return s;
}
// Slices a string to, at most, max_length number of bytes (NOT including the null.)
// Due to the nature of UTF-8, it may be a few bytes shorter than the maximum.
//
// If the string is less than max_length bytes, it will be return unchanged.
public string safe_byte_substring(string s, ssize_t max_length) {
if (s.length < max_length)
return s;
return glib_substring(s, 0, s.char_count(max_length));
}
}