Implemented IMAP-specific folder and message properties tables: #3805

This commit adds support for IMAP-specific properties, of which UIDValidity is crucial toward completing #3805.  The additional code is to integrate these tables into the SQLite Geary backend and to make sure this information is requested from the IMAP server.

NOTE: This commit changes the database schema.  Old databases will need to be blown away before running.
This commit is contained in:
Jim Nelson 2011-07-08 12:45:22 -07:00
parent 9792780edb
commit 6b8951bfd8
30 changed files with 496 additions and 99 deletions

View file

@ -69,8 +69,7 @@ CREATE TABLE ImapFolderPropertiesTable (
id INTEGER PRIMARY KEY,
folder_id INTEGER UNIQUE REFERENCES FolderTable ON DELETE CASCADE,
uid_validity INTEGER,
supports_children INTEGER,
is_openable INTEGER
attributes TEXT
);
CREATE INDEX ImapFolderPropertiesTableFolderIDIndex ON ImapFolderPropertiesTable(folder_id);
@ -82,13 +81,7 @@ CREATE INDEX ImapFolderPropertiesTableFolderIDIndex ON ImapFolderPropertiesTable
CREATE TABLE ImapMessagePropertiesTable (
id INTEGER PRIMARY KEY,
message_id INTEGER UNIQUE REFERENCES MessageTable ON DELETE CASCADE,
answered INTEGER,
deleted INTEGER,
draft INTEGER,
flagged INTEGER,
recent INTEGER,
seen INTEGER,
all_flags TEXT
flags TEXT
);
CREATE INDEX ImapMessagePropertiesTableMessageIDIndex ON ImapMessagePropertiesTable(message_id);

View file

@ -93,7 +93,7 @@ public class MainWindow : Gtk.Window {
else
debug("no folders");
} catch (Error err) {
error("%s", err.message);
warning("%s", err.message);
}
}

View file

@ -91,7 +91,8 @@ class ImapConsole : Gtk.Window {
"exit",
"quit",
"gmail",
"keepalive"
"keepalive",
"status"
};
private void exec(string input) {
@ -180,6 +181,10 @@ class ImapConsole : Gtk.Window {
keepalive(cmd, args);
break;
case "status":
folder_status(cmd, args);
break;
default:
status("Unknown command \"%s\"".printf(cmd));
break;
@ -393,6 +398,28 @@ class ImapConsole : Gtk.Window {
}
}
private void folder_status(string cmd, string[] args) throws Error {
check_min_connected(cmd, args, 2, "<folder> <data-item...>");
status("Status %s".printf(args[0]));
Geary.Imap.StatusDataType[] data_items = new Geary.Imap.StatusDataType[0];
for (int ctr = 1; ctr < args.length; ctr++)
data_items += Geary.Imap.StatusDataType.decode(args[ctr]);
cx.send_async.begin(new Geary.Imap.StatusCommand(cx.generate_tag(), args[0], data_items),
null, on_get_status);
}
private void on_get_status(Object? source, AsyncResult result) {
try {
cx.send_async.end(result);
status("Get status");
} catch (Error err) {
exception(err);
}
}
private void quit(string cmd, string[] args) throws Error {
Gtk.main_quit();
}

View file

@ -5,8 +5,8 @@
*/
public abstract class Geary.AbstractFolder : Object, Geary.Folder {
protected virtual void notify_opened() {
opened();
protected virtual void notify_opened(Geary.Folder.OpenState state) {
opened(state);
}
protected virtual void notify_closed(Geary.Folder.CloseReason reason) {

View file

@ -7,10 +7,11 @@
private class Geary.EngineFolder : Geary.AbstractFolder {
private const int REMOTE_FETCH_CHUNK_COUNT = 10;
private RemoteAccount remote;
private LocalAccount local;
private RemoteFolder? remote_folder = null;
private LocalFolder local_folder;
protected RemoteAccount remote;
protected LocalAccount local;
protected RemoteFolder? remote_folder = null;
protected LocalFolder local_folder;
private bool opened = false;
private Geary.Common.NonblockingSemaphore remote_semaphore =
new Geary.Common.NonblockingSemaphore(true);
@ -59,8 +60,6 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
open_remote_async.begin(readonly, cancellable, on_open_remote_completed);
opened = true;
notify_opened();
}
private async void open_remote_async(bool readonly, Cancellable? cancellable) throws Error {
@ -77,6 +76,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
private void on_open_remote_completed(Object? source, AsyncResult result) {
try {
open_remote_async.end(result);
notify_opened(Geary.Folder.OpenState.BOTH);
} catch (Error err) {
debug("Unable to open remote folder %s: %s", to_string(), err.message);
@ -86,6 +87,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
} catch (Error err) {
debug("Unable to notify remote folder ready: %s", err.message);
}
notify_opened(Geary.Folder.OpenState.LOCAL);
}
}
@ -142,7 +145,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable = null)
throws Error {
assert(low >= 1);
assert(count >= 0);
assert(count >= 0 || count == -1);
if (!opened)
throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
@ -361,8 +364,10 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
list = needed_by_position;
}
// Always get the flags, and the generic end-user won't know to ask for them until they
// need them
Gee.List<Geary.Email>? remote_list = yield remote_folder.list_email_sparse_async(
list, required_fields, cancellable);
list, required_fields | Geary.Email.Field.PROPERTIES, cancellable);
if (remote_list == null || remote_list.size == 0)
break;

View file

@ -7,6 +7,12 @@
public delegate void Geary.EmailCallback(Gee.List<Geary.Email>? emails, Error? err);
public interface Geary.Folder : Object {
public enum OpenState {
REMOTE,
LOCAL,
BOTH
}
public enum CloseReason {
LOCAL_CLOSE,
REMOTE_CLOSE,
@ -15,9 +21,9 @@ public interface Geary.Folder : Object {
/**
* This is fired when the Folder is successfully opened by a caller. It will only fire once
* until the Folder is closed.
* until the Folder is closed, with the OpenState indicating what has been opened.
*/
public signal void opened();
public signal void opened(OpenState state);
/**
* This is fired when the Folder is successfully closed by a caller. It will only fire once
@ -50,8 +56,8 @@ public interface Geary.Folder : Object {
* directly. This allows subclasses and superclasses the opportunity to inspect the email
* and update state before and/or after the signal has been fired.
*/
protected virtual void notify_opened() {
opened();
protected virtual void notify_opened(OpenState state) {
opened(state);
}
/**
@ -140,7 +146,8 @@ public interface Geary.Folder : Object {
/**
* Returns a list of messages that fulfill the required_fields flags starting at the low
* position and moving up to (low + count). The list is not guaranteed to be in any
* position and moving up to (low + count). If count is -1, the returned list starts at low
* and proceeds to all available emails. The returned list is not guaranteed to be in any
* particular order.
*
* If any position in low to (low + count) are out of range, only the email within range are

View file

@ -50,7 +50,7 @@ private 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.Folder local_folder in local_list)
engine_list.add(new EngineFolder(remote, local, (LocalFolder) local_folder));
engine_list.add(new GenericImapFolder(remote, local, (LocalFolder) local_folder));
}
background_update_folders.begin(parent, engine_list);
@ -72,7 +72,7 @@ private class Geary.GenericImapAccount : Geary.EngineAccount {
try {
local_folder = (LocalFolder) yield local.fetch_folder_async(path, cancellable);
return new EngineFolder(remote, local, local_folder);
return new GenericImapFolder(remote, local, local_folder);
} catch (EngineError err) {
// don't thrown NOT_FOUND's, that means we need to fall through and clone from the
// server
@ -94,10 +94,10 @@ private class Geary.GenericImapAccount : Geary.EngineAccount {
yield local.clone_folder_async(remote_folder, cancellable);
}
// Fetch the local account's version of the folder for the EngineFolder
// Fetch the local account's version of the folder for the GenericImapFolder
local_folder = (LocalFolder) yield local.fetch_folder_async(path, cancellable);
return new EngineFolder(remote, local, local_folder);
return new GenericImapFolder(remote, local, local_folder);
}
private Gee.Set<string> get_folder_names(Gee.Collection<Geary.Folder> folders) {
@ -140,11 +140,15 @@ private class Geary.GenericImapAccount : Geary.EngineAccount {
if (to_remove.size == 0)
to_remove = null;
try {
if (to_add != null)
yield local.clone_many_folders_async(to_add);
} catch (Error err) {
error("Unable to add/remove folders: %s", err.message);
if (to_add != null) {
foreach (Geary.Folder folder in to_add) {
try {
yield local.clone_folder_async(folder);
} catch (Error err) {
debug("Unable to add/remove folder %s: %s", folder.get_path().to_string(),
err.message);
}
}
}
Gee.Collection<Geary.Folder> engine_added = null;
@ -154,7 +158,7 @@ private class Geary.GenericImapAccount : Geary.EngineAccount {
try {
LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(
remote_folder.get_path());
engine_added.add(new EngineFolder(remote, local, local_folder));
engine_added.add(new GenericImapFolder(remote, local, local_folder));
} catch (Error convert_err) {
error("Unable to fetch local folder: %s", convert_err.message);
}

View file

@ -0,0 +1,11 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.GenericImapFolder : Geary.EngineFolder {
public GenericImapFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) {
base (remote, local, local_folder);
}
}

View file

@ -8,9 +8,6 @@ public interface Geary.LocalAccount : Object, Geary.Account {
public abstract async void clone_folder_async(Geary.Folder folder, Cancellable? cancellable = null)
throws Error;
public abstract async void clone_many_folders_async(Gee.Collection<Geary.Folder> folders,
Cancellable? cancellable = null) throws Error;
/**
* Returns true if the email (identified by its Message-ID) already exists in the account's
* local store, no matter the folder.

View file

@ -66,7 +66,18 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
if (processed == null)
delims.set(path.get_root().basename, mbox.delim);
folders.add(new Geary.Imap.Folder(session_mgr, path, mbox));
UIDValidity? uid_validity = null;
if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT)) {
try {
StatusResults results = yield session_mgr.status_async(path.get_fullpath(),
{ StatusDataType.UIDVALIDITY }, cancellable);
uid_validity = results.uidvalidity;
} catch (Error status_err) {
message("Unable to fetch UID Validity for %s: %s", path.to_string(), status_err.message);
}
}
folders.add(new Geary.Imap.Folder(session_mgr, path, uid_validity, mbox));
}
return folders;
@ -93,7 +104,14 @@ public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
if (mbox == null)
throw_not_found(path);
return new Geary.Imap.Folder(session_mgr, processed, mbox);
UIDValidity? uid_validity = null;
if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT)) {
StatusResults results = yield session_mgr.status_async(processed.get_fullpath(),
{ StatusDataType.UIDVALIDITY }, cancellable);
uid_validity = results.uidvalidity;
}
return new Geary.Imap.Folder(session_mgr, processed, uid_validity, mbox);
} catch (ImapError err) {
if (err is ImapError.SERVER_ERROR)
throw_not_found(path);

View file

@ -5,10 +5,27 @@
*/
public class Geary.Imap.EmailProperties : Geary.EmailProperties {
public bool answered { get; private set; }
public bool deleted { get; private set; }
public bool draft { get; private set; }
public bool flagged { get; private set; }
public bool recent { get; private set; }
public bool seen { get; private set; }
public MessageFlags flags { get; private set; }
public EmailProperties(MessageFlags flags) {
this.flags = flags;
answered = flags.contains(MessageFlag.ANSWERED);
deleted = flags.contains(MessageFlag.DELETED);
draft = flags.contains(MessageFlag.DRAFT);
flagged = flags.contains(MessageFlag.FLAGGED);
recent = flags.contains(MessageFlag.RECENT);
seen = flags.contains(MessageFlag.SEEN);
}
public bool is_empty() {
return (flags.size == 0);
}
public override bool is_unread() {

View file

@ -5,13 +5,13 @@
*/
public class Geary.Imap.FolderProperties : Geary.FolderProperties {
public UID? uid_validity { get; set; }
public UIDValidity? uid_validity { get; set; }
public MailboxAttributes attrs { get; private set; }
public Trillian supports_children { get; private set; }
public Trillian has_children { get; private set; }
public Trillian is_openable { get; private set; }
public FolderProperties(UID? uid_validity, MailboxAttributes attrs) {
public FolderProperties(UIDValidity? uid_validity, MailboxAttributes attrs) {
this.uid_validity = uid_validity;
this.attrs = attrs;

View file

@ -14,13 +14,14 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
private Imap.FolderProperties properties;
private Mailbox? mailbox = null;
internal Folder(ClientSessionManager session_mgr, Geary.FolderPath path, MailboxInformation info) {
internal Folder(ClientSessionManager session_mgr, Geary.FolderPath path, UIDValidity? uid_validity,
MailboxInformation info) {
this.session_mgr = session_mgr;
this.info = info;
this.path = path;
readonly = Trillian.UNKNOWN;
properties = new Imap.FolderProperties(null, info.attrs);
properties = new Imap.FolderProperties(uid_validity, info.attrs);
}
public override Geary.FolderPath get_path() {
@ -46,7 +47,7 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
this.readonly = Trillian.from_boolean(readonly);
properties.uid_validity = mailbox.uid_validity;
notify_opened();
notify_opened(Geary.Folder.OpenState.REMOTE);
}
public override async void close_async(Cancellable? cancellable = null) throws Error {
@ -79,7 +80,11 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
if (mailbox == null)
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
return yield mailbox.list_set_async(new MessageSet.range(low, count), fields, cancellable);
MessageSet msg_set = (count != -1)
? new MessageSet.range(low, count)
: new MessageSet.range_to_highest(low);
return yield mailbox.list_set_async(msg_set, fields, cancellable);
}
public override async Gee.List<Geary.Email>? list_email_sparse_async(int[] by_position,

View file

@ -17,13 +17,13 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults {
* -1 if not specified.
*/
public int unseen { get; private set; }
public UID? uid_validity { get; private set; }
public UIDValidity? uid_validity { get; private set; }
public Flags? flags { get; private set; }
public Flags? permanentflags { get; private set; }
public bool readonly { get; private set; }
private SelectExamineResults(StatusResponse status_response, int exists, int recent, int unseen,
UID? uidvalidity, Flags? flags, Flags? permanentflags, bool readonly) {
UIDValidity? uidvalidity, Flags? flags, Flags? permanentflags, bool readonly) {
base (status_response);
this.exists = exists;
@ -41,7 +41,7 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults {
int exists = -1;
int recent = -1;
int unseen = -1;
UID? uidvalidity = null;
UIDValidity? uidvalidity = null;
UID? uidnext = null;
MessageFlags? flags = null;
MessageFlags? permanentflags = null;
@ -75,7 +75,7 @@ public class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults {
break;
case ResponseCodeType.UIDVALIDITY:
uidvalidity = new UID(
uidvalidity = new UIDValidity(
ok_response.response_code.get_as_string(1).as_int());
break;

View file

@ -15,14 +15,14 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
*/
public int recent { get; private set; }
public UID? uidnext { get; private set; }
public UID? uidvalidity { get; private set; }
public UIDValidity? uidvalidity { get; private set; }
/**
* -1 if not set.
*/
public int unseen { get; private set; }
public StatusResults(StatusResponse status_response, string mailbox, int messages, int recent,
UID? uidnext, UID? uidvalidity, int unseen) {
UID? uidnext, UIDValidity? uidvalidity, int unseen) {
base (status_response);
this.mailbox = mailbox;
@ -54,7 +54,7 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
int messages = -1;
int recent = -1;
UID? uidnext = null;
UID? uidvalidity = null;
UIDValidity? uidvalidity = null;
int unseen = -1;
for (int ctr = 0; ctr < values.get_count(); ctr += 2) {
@ -76,7 +76,7 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
break;
case StatusDataType.UIDVALIDITY:
uidvalidity = new UID(valuep.as_int());
uidvalidity = new UIDValidity(valuep.as_int());
break;
case StatusDataType.UNSEEN:

View file

@ -25,6 +25,12 @@ public class Geary.Imap.UID : Geary.Common.Int64MessageData, Geary.Imap.MessageD
}
}
public class Geary.Imap.UIDValidity : Geary.Common.Int64MessageData, Geary.Imap.MessageData {
public UIDValidity(int64 value) {
base (value);
}
}
public class Geary.Imap.MessageNumber : Geary.Common.IntMessageData, Geary.Imap.MessageData {
public MessageNumber(int value) {
base (value);
@ -32,6 +38,8 @@ public class Geary.Imap.MessageNumber : Geary.Common.IntMessageData, Geary.Imap.
}
public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.MessageData {
public int size { get { return list.size; } }
private Gee.Set<Flag> list;
public Flags(Gee.Collection<Flag> flags) {
@ -47,6 +55,14 @@ public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.Me
return list.read_only_view;
}
/**
* Returns the flags in serialized form, which is each flag separated by a space (legal in
* IMAP, as flags must be atoms and atoms prohibit spaces).
*/
public virtual string serialize() {
return to_string();
}
public override string to_string() {
StringBuilder builder = new StringBuilder();
foreach (Flag flag in list) {
@ -79,12 +95,32 @@ public class Geary.Imap.MessageFlags : Geary.Imap.Flags {
return new MessageFlags(list);
}
public static MessageFlags deserialize(string str) {
string[] tokens = str.split(" ");
Gee.Collection<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
foreach (string token in tokens)
flags.add(new MessageFlag(token));
return new MessageFlags(flags);
}
}
public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags {
public MailboxAttributes(Gee.Collection<MailboxAttribute> attrs) {
base (attrs);
}
public static MailboxAttributes deserialize(string str) {
string[] tokens = str.split(" ");
Gee.Collection<MailboxAttribute> attrs = new Gee.ArrayList<MailboxAttribute>();
foreach (string token in tokens)
attrs.add(new MailboxAttribute(token));
return new MailboxAttributes(attrs);
}
}
public class Geary.Imap.InternalDate : Geary.RFC822.Date, Geary.Imap.MessageData {

View file

@ -98,6 +98,19 @@ public class Geary.Imap.ClientSessionManager {
return (results.get_count() > 0) ? results.get_all()[0] : null;
}
public async Geary.Imap.StatusResults status_async(string path, StatusDataType[] types,
Cancellable? cancellable = null) throws Error {
ClientSession session = yield get_authorized_session(cancellable);
StatusResults results = StatusResults.decode(yield session.send_command_async(
new StatusCommand(session.generate_tag(), path, types), cancellable));
if (results.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
return results;
}
public async Mailbox select_mailbox(string path, Cancellable? cancellable = null) throws Error {
return yield select_examine_mailbox(path, true, cancellable);
}

View file

@ -8,7 +8,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference {
public string name { get; private set; }
public int count { get; private set; }
public bool is_readonly { get; private set; }
public UID uid_validity { get; private set; }
public UIDValidity uid_validity { get; private set; }
private SelectedContext context;
@ -203,7 +203,7 @@ internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics {
public int exists { get; protected set; }
public int recent { get; protected set; }
public bool is_readonly { get; protected set; }
public UID uid_validity { get; protected set; }
public UIDValidity uid_validity { get; protected set; }
public signal void exists_changed(int exists);

View file

@ -23,6 +23,14 @@ public abstract class Geary.Sqlite.Table {
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);
}
public string to_string() {
return table.name;
}

View file

@ -7,6 +7,7 @@
public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount {
private MailDatabase db;
private FolderTable folder_table;
private ImapFolderPropertiesTable folder_properties_table;
private MessageTable message_table;
public Account(Geary.Credentials cred) {
@ -19,6 +20,7 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount {
}
folder_table = db.get_folder_table();
folder_properties_table = db.get_imap_folder_properties_table();
message_table = db.get_message_table();
}
@ -42,20 +44,21 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount {
public async void clone_folder_async(Geary.Folder folder, Cancellable? cancellable = null)
throws Error {
Geary.Imap.Folder imap_folder = (Geary.Imap.Folder) folder;
Geary.Imap.FolderProperties? imap_folder_properties = (Geary.Imap.FolderProperties?)
imap_folder.get_properties();
// properties *must* be available to perform a clone
assert(imap_folder_properties != null);
int64 parent_id = yield fetch_parent_id_async(folder.get_path(), cancellable);
yield folder_table.create_async(new FolderRow(folder_table, folder.get_path().basename,
parent_id), cancellable);
}
public async void clone_many_folders_async(Gee.Collection<Geary.Folder> folders,
Cancellable? cancellable = null) throws Error {
Gee.List<FolderRow> rows = new Gee.ArrayList<FolderRow>();
foreach (Geary.Folder folder in folders) {
int64 parent_id = yield fetch_parent_id_async(folder.get_path(), cancellable);
rows.add(new FolderRow(db.get_folder_table(), folder.get_path().basename, parent_id));
}
int64 folder_id = yield folder_table.create_async(new FolderRow(folder_table,
imap_folder.get_path().basename, parent_id), cancellable);
yield folder_table.create_many_async(rows, cancellable);
yield folder_properties_table.create_async(
new ImapFolderPropertiesRow.from_imap_properties(folder_properties_table, folder_id,
imap_folder_properties));
}
public override async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
@ -75,11 +78,14 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount {
Gee.Collection<Geary.Folder> folders = new Gee.ArrayList<Geary.Sqlite.Folder>();
foreach (FolderRow row in rows) {
ImapFolderPropertiesRow? properties = yield folder_properties_table.fetch_async(row.id,
cancellable);
Geary.FolderPath path = (parent != null)
? parent.get_child(row.name)
: new Geary.FolderRoot(row.name, "/", Geary.Imap.Folder.CASE_SENSITIVE);
folders.add(new Geary.Sqlite.Folder(db, row, path));
folders.add(new Geary.Sqlite.Folder(db, row, properties, path));
}
return folders;
@ -105,7 +111,10 @@ public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount {
if (row == null)
throw new EngineError.NOT_FOUND("%s not found in local database", path.to_string());
return new Geary.Sqlite.Folder(db, row, path);
ImapFolderPropertiesRow? properties = yield folder_properties_table.fetch_async(row.id,
cancellable);
return new Geary.Sqlite.Folder(db, row, properties, path);
}
public async bool has_message_id_async(Geary.RFC822.MessageID message_id, out int count,

View file

@ -10,20 +10,25 @@
public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
private MailDatabase db;
private FolderRow folder_row;
private Geary.FolderProperties? properties;
private MessageTable message_table;
private MessageLocationTable location_table;
private ImapMessageLocationPropertiesTable imap_location_table;
private ImapMessagePropertiesTable imap_message_properties_table;
private Geary.FolderPath path;
private bool opened = false;
internal Folder(MailDatabase db, FolderRow folder_row, Geary.FolderPath path) throws Error {
internal Folder(MailDatabase db, FolderRow folder_row, ImapFolderPropertiesRow? properties,
Geary.FolderPath path) throws Error {
this.db = db;
this.folder_row = folder_row;
this.properties = (properties != null) ? properties.get_imap_folder_properties() : null;
this.path = path;
message_table = db.get_message_table();
location_table = db.get_message_location_table();
imap_location_table = db.get_imap_message_location_table();
imap_message_properties_table = db.get_imap_message_properties_table();
}
private void check_open() throws Error {
@ -36,7 +41,7 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
}
public override Geary.FolderProperties? get_properties() {
return null;
return properties;
}
public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error {
@ -44,7 +49,8 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
throw new EngineError.ALREADY_OPEN("%s already open", to_string());
opened = true;
notify_opened();
notify_opened(Geary.Folder.OpenState.LOCAL);
}
public override async void close_async(Cancellable? cancellable = null) throws Error {
@ -52,6 +58,7 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
return;
opened = false;
notify_closed(CloseReason.FOLDER_CLOSED);
}
@ -89,15 +96,27 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
ImapMessageLocationPropertiesRow imap_location_row = new ImapMessageLocationPropertiesRow(
imap_location_table, Row.INVALID_ID, location_id, location.uid);
yield imap_location_table.create_async(imap_location_row, cancellable);
// 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;
if (email.fields.fulfills(Geary.Email.Field.PROPERTIES) && properties != null && !properties.is_empty()) {
ImapMessagePropertiesRow properties_row = new ImapMessagePropertiesRow.from_imap_properties(
imap_message_properties_table, message_id, properties);
yield imap_message_properties_table.create_async(properties_row, cancellable);
}
}
public override async Gee.List<Geary.Email>? list_email_async(int low, int count,
Geary.Email.Field required_fields, Cancellable? cancellable) throws Error {
assert(low >= 1);
assert(count >= 1);
assert(count >= 0 || count == -1);
check_open();
if (count == 0)
return null;
Gee.List<MessageLocationRow>? list = yield location_table.list_async(folder_row.id, low,
count, cancellable);
@ -136,12 +155,23 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
MessageRow? message_row = yield message_table.fetch_async(location_row.message_id,
required_fields, cancellable);
assert(message_row != null);
// only add to the list if the email contains all the required fields
if (!message_row.fields.is_set(required_fields))
continue;
emails.add(message_row.to_email(new Geary.Imap.EmailLocation(location_row.position,
imap_location_row.uid)));
ImapMessagePropertiesRow? properties = null;
if (required_fields.fulfills(Geary.Email.Field.PROPERTIES)) {
properties = yield imap_message_properties_table.fetch_async(location_row.message_id,
cancellable);
}
Geary.Email email = message_row.to_email(new Geary.Imap.EmailLocation(location_row.position,
imap_location_row.uid));
if (properties != null)
email.set_email_properties(properties.get_imap_email_properties());
emails.add(email);
}
return (emails.size > 0) ? emails : null;
@ -184,8 +214,18 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
message_row.fields);
}
return message_row.to_email(new Geary.Imap.EmailLocation(location_row.position,
ImapMessagePropertiesRow? properties = null;
if (required_fields.fulfills(Geary.Email.Field.PROPERTIES)) {
properties = yield imap_message_properties_table.fetch_async(location_row.message_id,
cancellable);
}
Geary.Email email = message_row.to_email(new Geary.Imap.EmailLocation(location_row.position,
imap_location_row.uid));
if (properties != null)
email.set_email_properties(properties.get_imap_email_properties());
return email;
}
public async bool is_email_present_at(int position, out Geary.Email.Field available_fields,
@ -292,7 +332,6 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
}
// TODO: The database should be locked around this method, as it should be atomic.
// TODO: Merge email properties
private async void merge_email_async(int64 message_id, Geary.Email email,
Cancellable? cancellable = null) throws Error {
assert(message_id != Row.INVALID_ID);
@ -310,6 +349,12 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
// possible nothing has changed or been added
if (message_row.fields != Geary.Email.Field.NONE)
yield message_table.merge_async(message_row, cancellable);
// update IMAP properties
if (email.fields.fulfills(Geary.Email.Field.PROPERTIES)) {
yield imap_message_properties_table.update_async(message_id,
((Geary.Imap.EmailProperties) email.properties).flags.serialize(), cancellable);
}
}
}

View file

@ -33,20 +33,11 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table {
query.bind_null(1);
}
public async void create_async(FolderRow row, Cancellable? cancellable = null) throws Error {
public async int64 create_async(FolderRow row, Cancellable? cancellable = null) throws Error {
SQLHeavy.Query query = create_query();
create_binding(query, row);
yield query.execute_insert_async(cancellable);
}
public async void create_many_async(Gee.Collection<FolderRow> rows, Cancellable? cancellable = null)
throws Error {
SQLHeavy.Query query = create_query();
foreach (FolderRow row in rows) {
create_binding(query, row);
query.execute_insert();
}
return yield query.execute_insert_async(cancellable);
}
public async Gee.List<FolderRow> list_async(int64 parent_id, Cancellable? cancellable = null)

View file

@ -40,6 +40,7 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database {
: (MessageLocationTable) add_table(new MessageLocationTable(this, heavy_table));
}
// TODO: This belongs in a subclass.
public Geary.Sqlite.ImapMessageLocationPropertiesTable get_imap_message_location_table() {
SQLHeavy.Table heavy_table;
ImapMessageLocationPropertiesTable? imap_location_table = get_table(
@ -49,5 +50,25 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database {
? imap_location_table
: (ImapMessageLocationPropertiesTable) add_table(new ImapMessageLocationPropertiesTable(this, heavy_table));
}
// TODO: This belongs in a subclass.
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));
}
// TODO: This belongs in a subclass.
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

@ -29,18 +29,29 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table {
}
/**
* low is one-based.
* low is one-based. If count is -1, all messages starting at low are returned.
*/
public async Gee.List<MessageLocationRow>? list_async(int64 folder_id, int low, int count,
Cancellable? cancellable = null) throws Error {
assert(low >= 1);
assert(count >= 0 || count == -1);
SQLHeavy.Query query = db.prepare(
"SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? "
+ "ORDER BY position LIMIT ? OFFSET ?");
query.bind_int64(0, folder_id);
query.bind_int(1, count);
query.bind_int(2, low - 1);
SQLHeavy.Query query;
if (count >= 0) {
query = db.prepare(
"SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? "
+ "ORDER BY position LIMIT ? OFFSET ?");
query.bind_int64(0, folder_id);
query.bind_int(1, count);
query.bind_int(2, low - 1);
} else {
// count == -1
query = db.prepare(
"SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? "
+ "ORDER BY position OFFSET ?");
query.bind_int64(0, folder_id);
query.bind_int(1, low - 1);
}
SQLHeavy.QueryResult results = yield query.execute_async(cancellable);
if (results.finished)
@ -103,7 +114,7 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table {
"SELECT id, message_id, position FROM MessageLocationTable WHERE folder_id = ? "
+ "AND position = ?");
query.bind_int64(0, folder_id);
query.bind_int64(1, position);
query.bind_int(1, position);
SQLHeavy.QueryResult results = yield query.execute_async(cancellable);
if (results.finished)

View file

@ -0,0 +1,38 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.ImapFolderPropertiesRow : Geary.Sqlite.Row {
public int64 id { get; private set; }
public int64 folder_id { get; private set; }
public Geary.Imap.UIDValidity? uid_validity { get; private set; }
public string attributes { get; private set; }
public ImapFolderPropertiesRow(ImapFolderPropertiesTable table, int64 id, int64 folder_id,
Geary.Imap.UIDValidity? uid_validity, string attributes) {
base (table);
this.id = id;
this.folder_id = folder_id;
this.uid_validity = uid_validity;
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;
uid_validity = properties.uid_validity;
attributes = properties.attrs.serialize();
}
public Geary.Imap.FolderProperties get_imap_folder_properties() {
return new Geary.Imap.FolderProperties(uid_validity,
Geary.Imap.MailboxAttributes.deserialize(attributes));
}
}

View file

@ -0,0 +1,49 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.ImapFolderPropertiesTable : Geary.Sqlite.Table {
// This *must* be in the same order as the schema.
public enum Column {
ID,
FOLDER_ID,
UID_VALIDITY,
FLAGS
}
public ImapFolderPropertiesTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) {
base (gdb, table);
}
public async int64 create_async(ImapFolderPropertiesRow row, Cancellable? cancellable = null)
throws Error {
SQLHeavy.Query query = db.prepare(
"INSERT INTO ImapFolderPropertiesTable (folder_id, uid_validity, attributes) VALUES (?, ?, ?)");
query.bind_int64(0, row.folder_id);
query.bind_int64(1, (row.uid_validity != null) ? row.uid_validity.value : -1);
query.bind_string(2, row.attributes);
return yield query.execute_insert_async(cancellable);
}
public async ImapFolderPropertiesRow? fetch_async(int64 folder_id, Cancellable? cancellable = null)
throws Error {
SQLHeavy.Query query = db.prepare(
"SELECT id, uid_validity, attributes FROM ImapFolderPropertiesTable WHERE folder_id = ?");
query.bind_int64(0, folder_id);
SQLHeavy.QueryResult result = yield query.execute_async(cancellable);
if (result.finished)
return null;
Geary.Imap.UIDValidity? uid_validity = null;
if (result.fetch_int64(1) >= 0)
uid_validity = new Geary.Imap.UIDValidity(result.fetch_int64(1));
return new ImapFolderPropertiesRow(this, result.fetch_int64(0), folder_id, uid_validity,
result.fetch_string(2));
}
}

View file

@ -0,0 +1,34 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
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 ImapMessagePropertiesRow(ImapMessagePropertiesTable table, int64 id, int64 message_id,
string flags) {
base (table);
this.id = id;
this.message_id = message_id;
this.flags = flags;
}
public ImapMessagePropertiesRow.from_imap_properties(ImapMessagePropertiesTable table,
int64 message_id, Geary.Imap.EmailProperties properties) {
base (table);
id = Row.INVALID_ID;
this.message_id = message_id;
flags = properties.flags.serialize();
}
public Geary.Imap.EmailProperties get_imap_email_properties() {
return new Geary.Imap.EmailProperties(Geary.Imap.MessageFlags.deserialize(flags));
}
}

View file

@ -0,0 +1,52 @@
/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.ImapMessagePropertiesTable : Geary.Sqlite.Table {
// This *must* be in the same order as the schema.
public enum Column {
ID,
FLAGS
}
public ImapMessagePropertiesTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) {
base (gdb, table);
}
public async int64 create_async(ImapMessagePropertiesRow row, Cancellable? cancellable = null)
throws Error {
SQLHeavy.Query query = db.prepare(
"INSERT INTO ImapMessagePropertiesTable (message_id, flags) VALUES (?, ?)");
query.bind_int64(0, row.message_id);
query.bind_string(1, row.flags);
return yield query.execute_insert_async(cancellable);
}
public async ImapMessagePropertiesRow? fetch_async(int64 message_id, Cancellable? cancellable = null)
throws Error {
SQLHeavy.Query query = db.prepare(
"SELECT id, flags FROM ImapMessagePropertiesTable WHERE message_id = ?");
query.bind_int64(0, message_id);
SQLHeavy.QueryResult result = yield query.execute_async(cancellable);
if (result.finished)
return null;
return new ImapMessagePropertiesRow(this, result.fetch_int64(0), message_id,
result.fetch_string(1));
}
public async void update_async(int64 message_id, string flags, Cancellable? cancellable = null)
throws Error {
SQLHeavy.Query query = db.prepare(
"UPDATE ImapMessagePropertiesTable SET flags = ? WHERE message_id = ?");
query.bind_string(0, flags);
query.bind_int64(1, message_id);
yield query.execute_async(cancellable);
}
}

View file

@ -9,6 +9,7 @@
*/
public enum Geary.Trillian {
// DO NOT MODIFY unless you know what you're doing. These values are persisted.
UNKNOWN = -1,
FALSE = 0,
TRUE = 1;

View file

@ -29,6 +29,7 @@ def build(bld):
'../engine/api/geary-folder-properties.vala',
'../engine/api/geary-folder.vala',
'../engine/api/geary-generic-imap-account.vala',
'../engine/api/geary-generic-imap-folder.vala',
'../engine/api/geary-gmail-account.vala',
'../engine/api/geary-local-interfaces.vala',
'../engine/api/geary-personality.vala',
@ -99,8 +100,12 @@ def build(bld):
'../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-folder-properties-row.vala',
'../engine/sqlite/imap/sqlite-imap-folder-properties-table.vala',
'../engine/sqlite/imap/sqlite-imap-message-location-properties-row.vala',
'../engine/sqlite/imap/sqlite-imap-message-location-properties-table.vala',
'../engine/sqlite/imap/sqlite-imap-message-properties-row.vala',
'../engine/sqlite/imap/sqlite-imap-message-properties-table.vala',
'../engine/state/state-machine-descriptor.vala',
'../engine/state/state-machine.vala',