Folder heirarchies: #3788

Now supporting folder heirarchies.  The client will now descend looking for subfolders.  This task now opens up multiple outstanding requests to the Engine as well as exercises the database schema.

Closing this ticket opens the door to finishing #3692.
This commit is contained in:
Jim Nelson 2011-07-01 15:40:20 -07:00
parent a774034aff
commit 0273b78005
36 changed files with 846 additions and 249 deletions

View file

@ -46,7 +46,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc.,
private static GearyApplication? _instance = null;
private MainWindow main_window = new MainWindow();
private Geary.Account? account = null;
private Geary.EngineAccount? account = null;
private GearyApplication() {
base (NAME, "geary", "org.yorba.geary");

View file

@ -52,20 +52,19 @@ public class FolderListStore : Gtk.TreeStore {
}
public void add_folder(Geary.Folder folder) {
Gtk.TreeIter? parent_iter = !folder.get_path().is_root()
? find_path(folder.get_path().get_parent())
: null;
Gtk.TreeIter iter;
append(out iter, null);
append(out iter, parent_iter);
set(iter,
Column.NAME, folder.get_name(),
Column.NAME, folder.get_path().basename,
Column.FOLDER_OBJECT, folder
);
}
public void add_folders(Gee.Collection<Geary.Folder> folders) {
foreach (Geary.Folder folder in folders)
add_folder(folder);
}
public Geary.Folder? get_folder_at(Gtk.TreePath path) {
Gtk.TreeIter iter;
if (!get_iter(out iter, path))
@ -77,6 +76,36 @@ public class FolderListStore : Gtk.TreeStore {
return folder;
}
// TODO: This could be replaced with a binary search
private Gtk.TreeIter? find_path(Geary.FolderPath path, Gtk.TreeIter? parent = null) {
Gtk.TreeIter iter;
// no parent, start at the root, otherwise start at the parent's children
if (parent == null) {
if (!get_iter_first(out iter))
return null;
} else {
if (!iter_children(out iter, parent))
return null;
}
do {
Geary.Folder folder;
get(iter, Column.FOLDER_OBJECT, out folder);
if (folder.get_path().equals(path))
return iter;
// recurse
if (iter_has_child(iter)) {
Gtk.TreeIter? found = find_path(path, iter);
if (found != null)
return found;
}
} while (iter_next(ref iter));
return null;
}
private int sort_by_name(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) {
string aname;
model.get(aiter, Column.NAME, out aname);

View file

@ -26,7 +26,7 @@ public class MainWindow : Gtk.Window {
private MessageViewer message_viewer = new MessageViewer();
private MessageBuffer message_buffer = new MessageBuffer();
private Gtk.UIManager ui = new Gtk.UIManager();
private Geary.Account? account = null;
private Geary.EngineAccount? account = null;
private Geary.Folder? current_folder = null;
public MainWindow() {
@ -61,7 +61,7 @@ public class MainWindow : Gtk.Window {
account.folders_added_removed.disconnect(on_folders_added_removed);
}
public void start(Geary.Account account) {
public void start(Geary.EngineAccount account) {
this.account = account;
account.folders_added_removed.connect(on_folders_added_removed);
@ -180,7 +180,7 @@ public class MainWindow : Gtk.Window {
return;
}
debug("Folder %s selected", folder.get_name());
debug("Folder %s selected", folder.to_string());
do_select_folder.begin(folder, on_select_folder_completed);
}
@ -252,9 +252,11 @@ public class MainWindow : Gtk.Window {
private void on_folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed) {
if (added != null) {
folder_list_store.add_folders(added);
debug("%d folders added", added.size);
if (added != null && added.size > 0) {
foreach (Geary.Folder folder in added)
folder_list_store.add_folder(folder);
search_folders_for_children.begin(added);
}
}
@ -264,5 +266,21 @@ public class MainWindow : Gtk.Window {
message_list_store.append_envelope(email);
}
}
private async void search_folders_for_children(Gee.Collection<Geary.Folder> folders) {
Gee.ArrayList<Geary.Folder> accumulator = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.Folder folder in folders) {
try {
Gee.Collection<Geary.Folder> children = yield account.list_folders_async(
folder.get_path(), null);
accumulator.add_all(children);
} catch (Error err) {
debug("Unable to list children of %s: %s", folder.to_string(), err.message);
}
}
if (accumulator.size > 0)
on_folders_added_removed(accumulator, null);
}
}

View file

@ -13,9 +13,7 @@ client_src = [
'ui/message-buffer.vala',
'ui/message-list-store.vala',
'ui/message-list-view.vala',
'ui/message-viewer.vala',
'util/intl.vala'
'ui/message-viewer.vala'
]
client_uselib = 'GLIB GEE GTK'

View file

@ -217,7 +217,7 @@ class ImapConsole : Gtk.Window {
private void capabilities(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
cx.send_async.begin(new Geary.Imap.CapabilityCommand(cx.generate_tag()), Priority.DEFAULT, null,
cx.send_async.begin(new Geary.Imap.CapabilityCommand(cx.generate_tag()), null,
on_capabilities);
}
@ -233,8 +233,7 @@ class ImapConsole : Gtk.Window {
private void noop(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
cx.send_async.begin(new Geary.Imap.NoopCommand(cx.generate_tag()), Priority.DEFAULT, null,
on_noop);
cx.send_async.begin(new Geary.Imap.NoopCommand(cx.generate_tag()), null, on_noop);
}
private void on_noop(Object? source, AsyncResult result) {
@ -301,12 +300,13 @@ class ImapConsole : Gtk.Window {
check_connected(cmd, args, 2, "user pass");
status("Logging in...");
cx.post(new Geary.Imap.LoginCommand(cx.generate_tag(), args[0], args[1]), on_logged_in);
cx.send_async.begin(new Geary.Imap.LoginCommand(cx.generate_tag(), args[0], args[1]),
null, on_logged_in);
}
private void on_logged_in(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
cx.send_async.end(result);
status("Login completed");
} catch (Error err) {
exception(err);
@ -317,12 +317,12 @@ class ImapConsole : Gtk.Window {
check_connected(cmd, args, 0, null);
status("Logging out...");
cx.post(new Geary.Imap.LogoutCommand(cx.generate_tag()), on_logout);
cx.send_async.begin(new Geary.Imap.LogoutCommand(cx.generate_tag()), null, on_logout);
}
private void on_logout(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
cx.send_async.end(result);
status("Logged out");
} catch (Error err) {
exception(err);
@ -333,12 +333,13 @@ class ImapConsole : Gtk.Window {
check_connected(cmd, args, 2, "<reference> <mailbox>");
status("Listing...");
cx.post(new Geary.Imap.ListCommand.wildcarded(cx.generate_tag(), args[0], args[1]), on_list);
cx.send_async.begin(new Geary.Imap.ListCommand.wildcarded(cx.generate_tag(), args[0], args[1]),
null, on_list);
}
private void on_list(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
cx.send_async.end(result);
status("Listed");
} catch (Error err) {
exception(err);
@ -349,20 +350,21 @@ class ImapConsole : Gtk.Window {
check_connected(cmd, args, 2, "<reference> <mailbox>");
status("Xlisting...");
cx.post(new Geary.Imap.XListCommand.wildcarded(cx.generate_tag(), args[0], args[1]),
on_list);
cx.send_async.begin(new Geary.Imap.XListCommand.wildcarded(cx.generate_tag(), args[0], args[1]),
null, on_list);
}
private void examine(string cmd, string[] args) throws Error {
check_connected(cmd, args, 1, "<mailbox>");
status("Opening %s read-only".printf(args[0]));
cx.post(new Geary.Imap.ExamineCommand(cx.generate_tag(), args[0]), on_examine);
cx.send_async.begin(new Geary.Imap.ExamineCommand(cx.generate_tag(), args[0]), null,
on_examine);
}
private void on_examine(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
cx.send_async.end(result);
status("Opened read-only");
} catch (Error err) {
exception(err);
@ -378,13 +380,13 @@ class ImapConsole : Gtk.Window {
for (int ctr = 1; ctr < args.length; ctr++)
data_items += Geary.Imap.FetchDataType.decode(args[ctr]);
cx.post(new Geary.Imap.FetchCommand(cx.generate_tag(),
new Geary.Imap.MessageSet.custom(args[0]), data_items), on_fetch);
cx.send_async.begin(new Geary.Imap.FetchCommand(cx.generate_tag(),
new Geary.Imap.MessageSet.custom(args[0]), data_items), null, on_fetch);
}
private void on_fetch(Object? source, AsyncResult result) {
try {
cx.finish_post(result);
cx.send_async.end(result);
status("Fetched");
} catch (Error err) {
exception(err);

View file

@ -0,0 +1,31 @@
/* 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 abstract class Geary.AbstractAccount : Object, Geary.Account {
private string name;
public AbstractAccount(string name) {
this.name = name;
}
protected virtual void notify_folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed) {
folders_added_removed(added, removed);
}
public abstract Geary.Email.Field get_required_fields_for_writing();
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error;
public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error;
public virtual string to_string() {
return name;
}
}

View file

@ -22,7 +22,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
updated();
}
public abstract string get_name();
public abstract Geary.FolderPath get_path();
public abstract Geary.FolderProperties? get_properties();
@ -85,7 +85,7 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder {
Cancellable? cancellable = null) throws Error;
public virtual string to_string() {
return get_name();
return get_path().to_string();
}
}

View file

@ -8,10 +8,8 @@ public interface Geary.Account : Object {
public signal void folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed);
protected virtual void notify_folders_added_removed(Gee.Collection<Geary.Folder>? added,
Gee.Collection<Geary.Folder>? removed) {
folders_added_removed(added, removed);
}
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
@ -25,22 +23,22 @@ public interface Geary.Account : Object {
*/
public abstract Geary.Email.Field get_required_fields_for_writing();
public abstract async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
/**
* Lists all the folders found under the parent path unless it's null, in which case it lists
* all the root folders. If the parent path cannot be found, EngineError.NOT_FOUND is thrown.
* If no folders exist in the root, EngineError.NOT_FOUND may be thrown as well. However,
* the caller should be prepared to deal with an empty list being returned instead.
*/
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error;
public abstract async void create_many_folders_async(Geary.Folder? parent,
Gee.Collection<Geary.Folder> folders, Cancellable? cancellable = null) throws Error;
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.Folder? parent,
/**
* Fetches a Folder object corresponding to the supplied path. If the backing medium does
* not have a record of a folder at the path, EngineError.NOT_FOUND will be thrown.
*/
public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error;
public abstract async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name,
Cancellable? cancellable = null) throws Error;
public abstract async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null)
throws Error;
public abstract async void remove_many_folders_async(Gee.Set<Geary.Folder> folders,
Cancellable? cancellable = null) throws Error;
public abstract string to_string();
}

View file

@ -14,5 +14,8 @@ public class Geary.Credentials {
this.user = user;
this.pass = pass;
}
}
public string to_string() {
return "%s/%s".printf(user, server);
}
}

View file

@ -0,0 +1,12 @@
/* 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 abstract class Geary.EngineAccount : Geary.AbstractAccount {
public EngineAccount(string name) {
base (name);
}
}

View file

@ -12,7 +12,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
private RemoteFolder? remote_folder = null;
private LocalFolder local_folder;
private bool opened = false;
private Geary.Common.NonblockingSemaphore remote_semaphore = new Geary.Common.NonblockingSemaphore();
private Geary.Common.NonblockingSemaphore remote_semaphore =
new Geary.Common.NonblockingSemaphore(true);
public EngineFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) {
this.remote = remote;
@ -24,13 +25,13 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
~EngineFolder() {
if (opened)
warning("Folder %s destroyed without closing", get_name());
warning("Folder %s destroyed without closing", to_string());
local_folder.updated.disconnect(on_local_updated);
}
public override string get_name() {
return local_folder.get_name();
public override Geary.FolderPath get_path() {
return local_folder.get_path();
}
public override Geary.FolderProperties? get_properties() {
@ -43,7 +44,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error {
if (opened)
throw new EngineError.ALREADY_OPEN("Folder %s already open", get_name());
throw new EngineError.ALREADY_OPEN("Folder %s already open", to_string());
yield local_folder.open_async(readonly, cancellable);
@ -55,8 +56,8 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// wait_for_remote_to_open(), which uses a NonblockingSemaphore to indicate that the remote
// is open (or has failed to open). This allows for early calls to list and fetch emails
// can work out of the local cache until the remote is ready.
RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(null, local_folder.get_name(),
cancellable);
RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(local_folder.get_path(),
cancellable);
open_remote_async.begin(folder, readonly, cancellable, on_open_remote_completed);
opened = true;
@ -100,7 +101,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// this method to complete (much like open_async())
if (remote_folder != null) {
yield remote_semaphore.wait_async();
remote_semaphore = new Geary.Common.NonblockingSemaphore();
remote_semaphore = new Geary.Common.NonblockingSemaphore(true);
remote_folder.updated.disconnect(on_remote_updated);
RemoteFolder? folder = remote_folder;
@ -145,7 +146,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
assert(count >= 0);
if (!opened)
throw new EngineError.OPEN_REQUIRED("%s is not open", get_name());
throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
if (count == 0) {
// signal finished
@ -246,7 +247,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable = null)
throws Error {
if (!opened)
throw new EngineError.OPEN_REQUIRED("%s is not open", get_name());
throw new EngineError.OPEN_REQUIRED("%s is not open", to_string());
if (by_position.length == 0) {
// signal finished
@ -345,7 +346,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
// possible to call remote multiple times, wait for it to open once and go
yield wait_for_remote_to_open();
debug("Background fetching %d emails for %s", needed_by_position.length, get_name());
debug("Background fetching %d emails for %s", needed_by_position.length, to_string());
Gee.List<Geary.Email> full = new Gee.ArrayList<Geary.Email>();
@ -419,7 +420,7 @@ private class Geary.EngineFolder : Geary.AbstractFolder {
public override async Geary.Email fetch_email_async(int num, Geary.Email.Field fields,
Cancellable? cancellable = null) throws Error {
if (!opened)
throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name());
throw new EngineError.OPEN_REQUIRED("Folder %s not opened", to_string());
try {
return yield local_folder.fetch_email_async(num, fields, cancellable);

View file

@ -5,11 +5,11 @@
*/
public class Geary.Engine {
public static Geary.Account open(Geary.Credentials cred) throws Error {
// Only ImapEngine today
return new ImapEngine(
public static Geary.EngineAccount open(Geary.Credentials cred) throws Error {
// Only Gmail today
return new GenericImapAccount(
"Gmail account %s".printf(cred.to_string()),
new Geary.Imap.Account(cred, Imap.ClientConnection.DEFAULT_PORT_TLS),
new Geary.Sqlite.Account(cred));
}
}

View file

@ -0,0 +1,187 @@
/* 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.FolderPath : Object, Hashable, Equalable {
public string basename { get; private set; }
private Gee.List<Geary.FolderPath>? path = null;
private string? fullpath = null;
private string? fullpath_separator = null;
private uint hash = uint.MAX;
protected FolderPath(string basename) {
assert(this is FolderRoot);
this.basename = basename;
}
private FolderPath.child(Gee.List<Geary.FolderPath> path, string basename) {
assert(path[0] is FolderRoot);
this.path = path;
this.basename = basename;
}
public bool is_root() {
return (path == null || path.size == 0);
}
public Geary.FolderRoot get_root() {
return (FolderRoot) ((path != null && path.size > 0) ? path[0] : this);
}
public Geary.FolderPath? get_parent() {
return (path != null && path.size > 0) ? path.last() : null;
}
public int get_path_length() {
// include self, which is not stored in the path list
return (path != null) ? path.size + 1 : 1;
}
/**
* Returns null if index is out of bounds. There is always at least one element in the path,
* namely this one.
*/
public Geary.FolderPath? get_folder_at(int index) {
// include self, which is not stored in the path list ... essentially, this logic makes it
// look like "this" is stored at the end of the path list
if (path == null)
return (index == 0) ? this : null;
int length = path.size;
if (index < length)
return path[index];
if (index == length)
return this;
return null;
}
public Gee.List<string> as_list() {
Gee.List<string> list = new Gee.ArrayList<string>();
if (path != null) {
foreach (Geary.FolderPath folder in path)
list.add(folder.basename);
}
list.add(basename);
return list;
}
public Geary.FolderPath get_child(string basename) {
// Build the child's path, which is this node's path plus this node
Gee.List<FolderPath> child_path = new Gee.ArrayList<FolderPath>();
if (path != null)
child_path.add_all(path);
child_path.add(this);
return new FolderPath.child(child_path, basename);
}
public string get_fullpath(string? use_separator = null) {
string? separator = use_separator ?? get_root().default_separator;
// no separator, no heirarchy
if (separator == null)
return basename;
if (fullpath != null && fullpath_separator == separator)
return fullpath;
StringBuilder builder = new StringBuilder();
if (path != null) {
foreach (Geary.FolderPath folder in path) {
builder.append(folder.basename);
builder.append(separator);
}
}
builder.append(basename);
fullpath = builder.str;
fullpath_separator = separator;
return fullpath;
}
private uint get_basename_hash(bool cs) {
return cs ? str_hash(basename) : str_hash(basename.down());
}
public uint to_hash() {
if (hash != uint.MAX)
return hash;
bool cs = get_root().case_sensitive;
// always one element in path
uint calc = get_folder_at(0).get_basename_hash(cs);
int path_length = get_path_length();
for (int ctr = 1; ctr < path_length; ctr++)
calc ^= get_folder_at(ctr).get_basename_hash(cs);
hash = calc;
return hash;
}
private bool is_basename_equal(string cmp, bool cs) {
return cs ? (basename == cmp) : (basename.down() == cmp.down());
}
public bool equals(Equalable o) {
FolderPath? other = o as FolderPath;
if (o == null)
return false;
if (o == this)
return true;
int path_length = get_path_length();
if (other.get_path_length() != path_length)
return false;
bool cs = get_root().case_sensitive;
if (other.get_root().case_sensitive != cs) {
message("Comparing %s and %s with different case sensitivities", to_string(),
other.to_string());
}
for (int ctr = 0; ctr < path_length; ctr++) {
if (!get_folder_at(ctr).is_basename_equal(other.get_folder_at(ctr).basename, cs))
return false;
}
return true;
}
/**
* Returns the fullpath using the default separator. Using only for debugging and logging.
*/
public string to_string() {
return get_fullpath();
}
}
public class Geary.FolderRoot : Geary.FolderPath {
public string? default_separator { get; private set; }
public bool case_sensitive { get; private set; }
public FolderRoot(string basename, string? default_separator, bool case_sensitive) {
base (basename);
this.default_separator = default_separator;
this.case_sensitive;
}
}

View file

@ -82,7 +82,7 @@ public interface Geary.Folder : Object {
updated();
}
public abstract string get_name();
public abstract Geary.FolderPath get_path();
public abstract Geary.FolderProperties? get_properties();

View file

@ -4,48 +4,50 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapEngine : Object, Geary.Account {
private class Geary.GenericImapAccount : Geary.EngineAccount {
public const string INBOX = "Inbox";
private RemoteAccount remote;
private LocalAccount local;
public ImapEngine(RemoteAccount remote, LocalAccount local) {
public GenericImapAccount(string name, RemoteAccount remote, LocalAccount local) {
base (name);
this.remote = remote;
this.local = local;
}
public Geary.Email.Field get_required_fields_for_writing() {
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 async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
public override async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error {
}
public async void create_many_folders_async(Geary.Folder? parent,
Gee.Collection<Geary.Folder> folders, Cancellable? cancellable = null) throws Error {
}
public async Gee.Collection<Geary.Folder> list_folders_async(Geary.Folder? parent,
Cancellable? cancellable = null) throws Error {
Gee.Collection<Geary.Folder> local_list = yield local.list_folders_async(parent, cancellable);
Gee.Collection<Geary.Folder>? local_list = null;
try {
local_list = yield local.list_folders_async(parent, cancellable);
} catch (EngineError err) {
// don't pass on NOT_FOUND's, that means we need to go to the server for more info
if (!(err is EngineError.NOT_FOUND))
throw err;
}
Gee.Collection<Geary.Folder> engine_list = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.Folder local_folder in local_list)
engine_list.add(new EngineFolder(remote, local, (LocalFolder) local_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));
}
background_update_folders.begin(parent, engine_list);
debug("Reporting %d folders", engine_list.size);
return engine_list;
}
public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name,
public override async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error {
LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(parent, folder_name,
cancellable);
LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(path, cancellable);
Geary.Folder engine_folder = new EngineFolder(remote, local, local_folder);
return engine_folder;
@ -54,7 +56,7 @@ private class Geary.ImapEngine : Object, Geary.Account {
private Gee.Set<string> get_folder_names(Gee.Collection<Geary.Folder> folders) {
Gee.Set<string> names = new Gee.HashSet<string>();
foreach (Geary.Folder folder in folders)
names.add(folder.get_name());
names.add(folder.get_path().basename);
return names;
}
@ -63,14 +65,14 @@ private class Geary.ImapEngine : Object, Geary.Account {
Gee.Set<string> names) {
Gee.List<Geary.Folder> excluded = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.Folder folder in folders) {
if (!names.contains(folder.get_name()))
if (!names.contains(folder.get_path().basename))
excluded.add(folder);
}
return excluded;
}
private async void background_update_folders(Geary.Folder? parent,
private async void background_update_folders(Geary.FolderPath? parent,
Gee.Collection<Geary.Folder> engine_folders) {
Gee.Collection<Geary.Folder> remote_folders;
try {
@ -82,13 +84,9 @@ private class Geary.ImapEngine : Object, Geary.Account {
Gee.Set<string> local_names = get_folder_names(engine_folders);
Gee.Set<string> remote_names = get_folder_names(remote_folders);
debug("%d local names, %d remote names", local_names.size, remote_names.size);
Gee.List<Geary.Folder>? to_add = get_excluded_folders(remote_folders, local_names);
Gee.List<Geary.Folder>? to_remove = get_excluded_folders(engine_folders, remote_names);
debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size);
if (to_add.size == 0)
to_add = null;
@ -97,7 +95,7 @@ private class Geary.ImapEngine : Object, Geary.Account {
try {
if (to_add != null)
yield local.create_many_folders_async(parent, to_add);
yield local.clone_many_folders_async(to_add);
} catch (Error err) {
error("Unable to add/remove folders: %s", err.message);
}
@ -107,8 +105,9 @@ private class Geary.ImapEngine : Object, Geary.Account {
engine_added = new Gee.ArrayList<Geary.Folder>();
foreach (Geary.Folder remote_folder in to_add) {
try {
engine_added.add(new EngineFolder(remote, local,
(LocalFolder) yield local.fetch_folder_async(parent, remote_folder.get_name())));
LocalFolder local_folder = (LocalFolder) yield local.fetch_folder_async(
remote_folder.get_path());
engine_added.add(new EngineFolder(remote, local, local_folder));
} catch (Error convert_err) {
error("Unable to fetch local folder: %s", convert_err.message);
}
@ -118,13 +117,5 @@ private class Geary.ImapEngine : Object, Geary.Account {
if (engine_added != null)
notify_folders_added_removed(engine_added, null);
}
public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null)
throws Error {
}
public async void remove_many_folders_async(Gee.Set<Geary.Folder> folders,
Cancellable? cancellable = null) throws Error {
}
}

View file

@ -5,6 +5,12 @@
*/
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

@ -5,6 +5,8 @@
*/
public interface Geary.RemoteAccount : Object, Geary.Account {
public abstract async string? get_folder_delimiter_async(string toplevel,
Cancellable? cancellable = null) throws Error;
}
public interface Geary.RemoteFolder : Object, Geary.Folder {

View file

@ -5,18 +5,26 @@
*/
public interface Geary.Comparable {
public abstract bool equals(Comparable other);
public abstract int compare(Comparable other);
public static int compare_func(void *a, void *b) {
return ((Comparable *) a)->compare((Comparable *) b);
}
}
public interface Geary.Equalable {
public abstract bool equals(Equalable other);
public static bool equal_func(void *a, void *b) {
return ((Comparable *) a)->equals((Comparable *) b);
return ((Equalable *) a)->equals((Equalable *) b);
}
}
public interface Geary.Hashable {
public abstract uint get_hash();
public abstract uint to_hash();
public static uint hash_func(void *ptr) {
return ((Hashable *) ptr)->get_hash();
return ((Hashable *) ptr)->to_hash();
}
}

View file

@ -0,0 +1,31 @@
/* 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.Common.NonblockingMailbox<G> : Object {
public int size { get { return queue.size; } }
private Gee.List<G> queue;
private NonblockingSemaphore spinlock = new NonblockingSemaphore(false);
public NonblockingMailbox() {
queue = new Gee.LinkedList<G>();
}
public void send(G msg) throws Error {
queue.add(msg);
spinlock.notify();
}
public async G recv_async(Cancellable? cancellable = null) throws Error {
for (;;) {
if (queue.size > 0)
return queue.remove_at(0);
yield spinlock.wait_async(cancellable);
}
}
}

View file

@ -0,0 +1,39 @@
/* 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.Common.NonblockingMutex {
private NonblockingSemaphore spinlock = new NonblockingSemaphore(false);
private bool locked = false;
private int next_token = 0;
private int locked_token = -1;
public NonblockingMutex() {
}
public async int claim_async(Cancellable? cancellable = null) throws Error {
for (;;) {
if (!locked) {
locked = true;
locked_token = next_token++;
return locked_token;
}
yield spinlock.wait_async(cancellable);
}
}
public void release(int token) throws Error {
if (token != locked_token)
throw new IOError.INVALID_ARGUMENT("Token %d is not the lock token", token);
locked = false;
locked_token = -1;
spinlock.notify();
}
}

View file

@ -7,17 +7,35 @@
public class Geary.Common.NonblockingSemaphore {
private class Pending {
public SourceFunc cb;
public Cancellable? cancellable;
public Pending(SourceFunc cb) {
public signal void cancelled();
public Pending(SourceFunc cb, Cancellable? cancellable) {
this.cb = cb;
this.cancellable = cancellable;
if (cancellable != null)
cancellable.cancelled.connect(on_cancelled);
}
~Pending() {
if (cancellable != null)
cancellable.cancelled.disconnect(on_cancelled);
}
private void on_cancelled() {
cancelled();
}
}
private bool broadcast;
private Cancellable? cancellable;
private bool passed = false;
private Gee.List<Pending> pending_queue = new Gee.LinkedList<Pending>();
public NonblockingSemaphore(Cancellable? cancellable = null) {
public NonblockingSemaphore(bool broadcast, Cancellable? cancellable = null) {
this.broadcast = broadcast;
this.cancellable = cancellable;
if (cancellable != null)
@ -29,34 +47,53 @@ public class Geary.Common.NonblockingSemaphore {
warning("Nonblocking semaphore destroyed with %d pending callers", pending_queue.size);
}
private void trigger_all() {
foreach (Pending pending in pending_queue)
Idle.add(pending.cb);
private void trigger(bool all) {
if (pending_queue.size == 0)
return;
pending_queue.clear();
if (all) {
foreach (Pending pending in pending_queue)
Idle.add(pending.cb);
pending_queue.clear();
} else {
Pending pending = pending_queue.remove_at(0);
Idle.add(pending.cb);
}
}
public void notify() throws Error {
check_cancelled();
passed = true;
trigger_all();
trigger(broadcast);
}
// TODO: Allow the caller to pass their own cancellable in if they want to be able to cancel
// this particular wait (and not all waiting threads of execution)
public async void wait_async() throws Error {
public async void wait_async(Cancellable? cancellable = null) throws Error {
for (;;) {
check_user_cancelled(cancellable);
check_cancelled();
if (passed)
return;
pending_queue.add(new Pending(wait_async.callback));
Pending pending = new Pending(wait_async.callback, cancellable);
pending.cancelled.connect(on_pending_cancelled);
pending_queue.add(pending);
yield;
pending.cancelled.disconnect(on_pending_cancelled);
}
}
public void reset() {
passed = false;
}
public bool is_cancelled() {
return (cancellable != null) ? cancellable.is_cancelled() : false;
}
@ -66,8 +103,20 @@ public class Geary.Common.NonblockingSemaphore {
throw new IOError.CANCELLED("Semaphore cancelled");
}
private static void check_user_cancelled(Cancellable? cancellable) throws Error {
if (cancellable != null && cancellable.is_cancelled())
throw new IOError.CANCELLED("User cancelled operation");
}
private void on_pending_cancelled(Pending pending) {
bool removed = pending_queue.remove(pending);
assert(removed);
Idle.add(pending.cb);
}
private void on_cancelled() {
trigger_all();
trigger(true);
}
}

View file

@ -4,57 +4,87 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Imap.Account : Object, Geary.Account, Geary.RemoteAccount {
public class Geary.Imap.Account : Geary.AbstractAccount, Geary.RemoteAccount {
private ClientSessionManager session_mgr;
private Gee.HashMap<string, string?> delims = new Gee.HashMap<string, string?>();
public Account(Credentials cred, uint default_port) {
base ("IMAP Account for %s".printf(cred.to_string()));
session_mgr = new ClientSessionManager(cred, default_port);
}
public Geary.Email.Field get_required_fields_for_writing() {
public override Geary.Email.Field get_required_fields_for_writing() {
return Geary.Email.Field.HEADER | Geary.Email.Field.BODY;
}
public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
public async string? get_folder_delimiter_async(string toplevel,
Cancellable? cancellable = null) throws Error {
throw new EngineError.READONLY("IMAP readonly");
if (delims.has_key(toplevel))
return delims.get(toplevel);
MailboxInformation? mbox = yield session_mgr.fetch_async(toplevel, cancellable);
if (mbox == null) {
throw new EngineError.NOT_FOUND("Toplevel folder %s not found on %s", toplevel,
session_mgr.to_string());
}
delims.set(toplevel, mbox.delim);
return mbox.delim;
}
public async void create_many_folders_async(Geary.Folder? parent, Gee.Collection<Geary.Folder> folders,
public override async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error {
throw new EngineError.READONLY("IMAP readonly");
}
public async Gee.Collection<Geary.Folder> list_folders_async(Geary.Folder? parent,
Cancellable? cancellable = null) throws Error {
Gee.Collection<MailboxInformation> mboxes = yield session_mgr.list(
(parent != null) ? parent.get_name() : null, cancellable);
Gee.Collection<MailboxInformation> mboxes;
try {
mboxes = (parent == null)
? yield session_mgr.list_roots(cancellable)
: yield session_mgr.list(parent.get_fullpath(), parent.get_root().default_separator,
cancellable);
} catch (Error err) {
if (err is ImapError.SERVER_ERROR)
throw_not_found(parent);
else
throw err;
}
Gee.Collection<Geary.Folder> folders = new Gee.ArrayList<Geary.Folder>();
foreach (MailboxInformation mbox in mboxes)
folders.add(new Geary.Imap.Folder(session_mgr, mbox));
foreach (MailboxInformation mbox in mboxes) {
if (parent == null)
delims.set(mbox.name, mbox.delim);
string basename = mbox.get_path().last();
Geary.FolderPath path = (parent != null)
? parent.get_child(basename)
: new Geary.FolderRoot(basename, mbox.delim, Folder.CASE_SENSITIVE);
folders.add(new Geary.Imap.Folder(session_mgr, path, mbox));
}
return folders;
}
public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name,
public override async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error {
MailboxInformation? mbox = yield session_mgr.fetch_async(
(parent != null) ? parent.get_name() : null, folder_name, cancellable);
if (mbox == null)
throw new EngineError.NOT_FOUND("Folder %s not found on server", folder_name);
try {
MailboxInformation? mbox = yield session_mgr.fetch_async(path.get_fullpath(), cancellable);
if (mbox == null)
throw_not_found(path);
return new Geary.Imap.Folder(session_mgr, mbox);
return new Geary.Imap.Folder(session_mgr, path, mbox);
} catch (ImapError err) {
if (err is ImapError.SERVER_ERROR)
throw_not_found(path);
else
throw err;
}
}
public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null)
throws Error {
throw new EngineError.READONLY("IMAP readonly");
}
public async void remove_many_folders_async(Gee.Set<Geary.Folder> folders,
Cancellable? cancellable = null) throws Error {
throw new EngineError.READONLY("IMAP readonly");
[NoReturn]
private void throw_not_found(Geary.FolderPath? path) throws EngineError {
throw new EngineError.NOT_FOUND("Folder %s not found on %s",
(path != null) ? path.to_string() : "root", session_mgr.to_string());
}
}

View file

@ -5,24 +5,26 @@
*/
public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
public const bool CASE_SENSITIVE = true;
private ClientSessionManager session_mgr;
private MailboxInformation info;
private string name;
private Geary.FolderPath path;
private Trillian readonly;
private Imap.FolderProperties properties;
private Mailbox? mailbox = null;
internal Folder(ClientSessionManager session_mgr, MailboxInformation info) {
internal Folder(ClientSessionManager session_mgr, Geary.FolderPath path, MailboxInformation info) {
this.session_mgr = session_mgr;
this.info = info;
this.path = path;
name = info.name;
readonly = Trillian.UNKNOWN;
properties = new Imap.FolderProperties(null, info.attrs);
}
public override string get_name() {
return name;
public override Geary.FolderPath get_path() {
return path;
}
public Trillian is_readonly() {
@ -37,8 +39,9 @@ public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder {
if (mailbox != null)
throw new EngineError.ALREADY_OPEN("%s already open", to_string());
mailbox = yield session_mgr.select_examine_mailbox(name, !readonly, cancellable);
// hook up signals
mailbox = yield session_mgr.select_examine_mailbox(path.get_fullpath(info.delim), !readonly,
cancellable);
// TODO: hook up signals
this.readonly = Trillian.from_boolean(readonly);
properties.uid_validity = mailbox.uid_validity;

View file

@ -10,5 +10,9 @@ public abstract class Geary.Imap.CommandResults {
public CommandResults(StatusResponse status_response) {
this.status_response = status_response;
}
public string to_string() {
return status_response.to_string();
}
}

View file

@ -6,14 +6,34 @@
public class Geary.Imap.MailboxInformation {
public string name { get; private set; }
public string delim { get; private set; }
public string? delim { get; private set; }
public MailboxAttributes attrs { get; private set; }
public MailboxInformation(string name, string delim, MailboxAttributes attrs) {
public MailboxInformation(string name, string? delim, MailboxAttributes attrs) {
this.name = name;
this.delim = delim;
this.attrs = attrs;
}
/**
* Will always return a list with at least one element in it.
*/
public Gee.List<string> get_path() {
Gee.List<string> path = new Gee.ArrayList<string>();
if (delim != null) {
string[] split = name.split(delim);
foreach (string str in split) {
if (!String.is_empty(str))
path.add(str);
}
}
if (path.size == 0)
path.add(name);
return path;
}
}
public class Geary.Imap.ListResults : Geary.Imap.CommandResults {
@ -37,7 +57,7 @@ public class Geary.Imap.ListResults : Geary.Imap.CommandResults {
try {
StringParameter cmd = data.get_as_string(1);
ListParameter attrs = data.get_as_list(2);
StringParameter delim = data.get_as_string(3);
StringParameter? delim = data.get_as_nullable_string(3);
StringParameter mailbox = data.get_as_string(4);
if (!cmd.equals_ci(ListCommand.NAME) && !cmd.equals_ci(XListCommand.NAME)) {
@ -60,7 +80,7 @@ public class Geary.Imap.ListResults : Geary.Imap.CommandResults {
attrlist.add(new MailboxAttribute(stringp.value));
}
MailboxInformation info = new MailboxInformation(mailbox.value, delim.value,
MailboxInformation info = new MailboxInformation(mailbox.value, delim.nullable_value,
new MailboxAttributes(attrlist));
map.set(mailbox.value, info);

View file

@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public abstract class Geary.Imap.Flag : Comparable, Hashable {
public abstract class Geary.Imap.Flag : Equalable, Hashable {
public string value { get; private set; }
public Flag(string value) {
@ -19,7 +19,7 @@ public abstract class Geary.Imap.Flag : Comparable, Hashable {
return this.value.down() == value.down();
}
public bool equals(Comparable b) {
public bool equals(Equalable b) {
Flag? flag = b as Flag;
if (flag == null)
return false;
@ -27,7 +27,7 @@ public abstract class Geary.Imap.Flag : Comparable, Hashable {
return (flag == this) ? true : flag.equals_string(value);
}
public uint get_hash() {
public uint to_hash() {
return str_hash(value.down());
}

View file

@ -35,7 +35,7 @@ public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.Me
private Gee.Set<Flag> list;
public Flags(Gee.Collection<Flag> flags) {
list = new Gee.HashSet<Flag>(Hashable.hash_func, Comparable.equal_func);
list = new Gee.HashSet<Flag>(Hashable.hash_func, Equalable.equal_func);
list.add_all(flags);
}

View file

@ -8,14 +8,18 @@ public class Geary.Imap.ClientConnection {
public const uint16 DEFAULT_PORT = 143;
public const uint16 DEFAULT_PORT_TLS = 993;
private const int FLUSH_TIMEOUT_MSEC = 100;
private string host_specifier;
private uint16 default_port;
private SocketClient socket_client = new SocketClient();
private SocketConnection? cx = null;
private Serializer? ser = null;
private Deserializer? des = null;
private Geary.Common.NonblockingMutex send_mutex = new Geary.Common.NonblockingMutex();
private int tag_counter = 0;
private char tag_prefix = 'a';
private uint flush_timeout_id = 0;
public virtual signal void connected() {
}
@ -26,6 +30,9 @@ public class Geary.Imap.ClientConnection {
public virtual signal void sent_command(Command cmd) {
}
public virtual signal void flush_failure(Error err) {
}
public virtual signal void received_status_response(StatusResponse status_response) {
}
@ -41,7 +48,7 @@ public class Geary.Imap.ClientConnection {
public virtual signal void receive_failure(Error err) {
}
public virtual signal void deserialize_failure() {
public virtual signal void deserialize_failure(Error err) {
}
public ClientConnection(string host_specifier, uint16 default_port) {
@ -54,6 +61,8 @@ public class Geary.Imap.ClientConnection {
~ClientConnection() {
// TODO: Close connection as gracefully as possible
if (flush_timeout_id != 0)
Source.remove(flush_timeout_id);
}
/**
@ -106,6 +115,8 @@ public class Geary.Imap.ClientConnection {
ser = null;
des = null;
des.xoff();
disconnected();
}
@ -128,43 +139,47 @@ public class Geary.Imap.ClientConnection {
}
private void on_deserialize_failure() {
deserialize_failure();
deserialize_failure(new ImapError.PARSE_ERROR("Unable to deserialize from %s", to_string()));
}
private void on_eos() {
recv_closed();
}
/**
* Convenience method for send_async.begin().
*/
public void post(Command cmd, AsyncReadyCallback cb, int priority = Priority.DEFAULT,
Cancellable? cancellable = null) {
send_async.begin(cmd, priority, cancellable, cb);
}
/**
* Convenience method for sync_async.end(). This is largely provided for symmetry with
* post_send().
*/
public void finish_post(AsyncResult result) throws Error {
send_async.end(result);
}
public async void send_async(Command cmd, int priority = Priority.DEFAULT,
Cancellable? cancellable = null) throws Error {
public async void send_async(Command cmd, Cancellable? cancellable = null) throws Error {
check_for_connection();
cmd.serialize(ser);
// need to run this in critical section because OutputStreams can only be written to
// serially
int token = yield send_mutex.claim_async(cancellable);
// TODO: At this point, we flush each command as it's written; at some point we'll have
// a queuing strategy that means serialized data is pushed out to the wire only at certain
// times
yield ser.flush_async(priority, cancellable);
yield cmd.serialize(ser);
send_mutex.release(token);
if (flush_timeout_id == 0)
flush_timeout_id = Timeout.add(FLUSH_TIMEOUT_MSEC, on_flush_timeout);
sent_command(cmd);
}
private bool on_flush_timeout() {
do_flush_async.begin();
flush_timeout_id = 0;
return false;
}
private async void do_flush_async() {
try {
if (ser != null)
yield ser.flush_async();
} catch (Error err) {
flush_failure(err);
}
}
private void check_for_connection() throws Error {
if (cx == null)
throw new ImapError.NOT_CONNECTED("Not connected to %s", to_string());

View file

@ -10,6 +10,7 @@ public class Geary.Imap.ClientSessionManager {
private Credentials cred;
private uint default_port;
private Gee.HashSet<ClientSession> sessions = new Gee.HashSet<ClientSession>();
private Geary.Common.NonblockingMutex sessions_mutex = new Geary.Common.NonblockingMutex();
private Gee.HashSet<SelectedContext> examined_contexts = new Gee.HashSet<SelectedContext>();
private Gee.HashSet<SelectedContext> selected_contexts = new Gee.HashSet<SelectedContext>();
private int keepalive_sec = ClientSession.DEFAULT_KEEPALIVE_SEC;
@ -45,31 +46,46 @@ public class Geary.Imap.ClientSessionManager {
session.enable_keepalives(keepalive_sec);
}
public async Gee.Collection<Geary.Imap.MailboxInformation> list(string? parent_name,
public async Gee.Collection<Geary.Imap.MailboxInformation> list_roots(
Cancellable? cancellable = null) throws Error {
// build a proper IMAP specifier
string specifier = parent_name ?? "/";
specifier += (specifier.has_suffix("/")) ? "%" : "/%";
ClientSession session = yield get_authorized_session(cancellable);
ListResults results = ListResults.decode(yield session.send_command_async(
new ListCommand(session.generate_tag(), specifier), cancellable));
new ListCommand.wildcarded(session.generate_tag(), "%", "%"), cancellable));
if (results.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
return results.get_all();
}
public async Geary.Imap.MailboxInformation? fetch_async(string? parent_name, string folder_name,
Cancellable? cancellable = null) throws Error {
public async Gee.Collection<Geary.Imap.MailboxInformation> list(string parent,
string delim, Cancellable? cancellable = null) throws Error {
// build a proper IMAP specifier
string specifier = parent_name ?? "/";
specifier += (specifier.has_suffix("/")) ? folder_name : "/%s".printf(folder_name);
string specifier = parent;
specifier += specifier.has_suffix(delim) ? "%" : (delim + "%");
ClientSession session = yield get_authorized_session(cancellable);
ListResults results = ListResults.decode(yield session.send_command_async(
new ListCommand(session.generate_tag(), specifier), cancellable));
if (results.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
return results.get_all();
}
public async Geary.Imap.MailboxInformation? fetch_async(string path,
Cancellable? cancellable = null) throws Error {
ClientSession session = yield get_authorized_session(cancellable);
ListResults results = ListResults.decode(yield session.send_command_async(
new ListCommand(session.generate_tag(), path), cancellable));
if (results.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
return (results.get_count() > 0) ? results.get_all()[0] : null;
}
@ -93,6 +109,9 @@ public class Geary.Imap.ClientSessionManager {
SelectExamineResults results;
ClientSession session = yield select_examine_async(path, is_select, out results, cancellable);
if (results.status_response.status != Status.OK)
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
SelectedContext new_context = new SelectedContext(session, results);
// Can't use the ternary operator due to this bug:
@ -131,6 +150,7 @@ public class Geary.Imap.ClientSessionManager {
context.session.close_mailbox_async.begin();
}
// This should only be called when sessions_mutex is locked.
private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error {
debug("Creating new session to %s", cred.server);
@ -151,13 +171,24 @@ public class Geary.Imap.ClientSessionManager {
}
private async ClientSession get_authorized_session(Cancellable? cancellable) throws Error {
int token = yield sessions_mutex.claim_async(cancellable);
ClientSession? found_session = null;
foreach (ClientSession session in sessions) {
string? mailbox;
if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED)
return session;
if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED) {
found_session = session;
break;
}
}
return yield create_new_authorized_session(cancellable);
if (found_session == null)
found_session = yield create_new_authorized_session(cancellable);
sessions_mutex.release(token);
return found_session;
}
private async ClientSession select_examine_async(string folder, bool is_select,
@ -183,5 +214,12 @@ public class Geary.Imap.ClientSessionManager {
bool removed = sessions.remove(session);
assert(removed);
}
/**
* Use only for debugging and logging.
*/
public string to_string() {
return cred.to_string();
}
}

View file

@ -413,10 +413,12 @@ public class Geary.Imap.ClientSession {
cx.connected.connect(on_network_connected);
cx.disconnected.connect(on_network_disconnected);
cx.sent_command.connect(on_network_sent_command);
cx.flush_failure.connect(on_network_flush_error);
cx.received_status_response.connect(on_received_status_response);
cx.received_server_data.connect(on_received_server_data);
cx.received_bad_response.connect(on_received_bad_response);
cx.receive_failure.connect(on_receive_failed);
cx.receive_failure.connect(on_network_receive_failure);
cx.deserialize_failure.connect(on_network_receive_failure);
cx.connect_async.begin(connect_params.cancellable, on_connect_completed);
@ -988,7 +990,7 @@ public class Geary.Imap.ClientSession {
}
try {
yield cx.send_async(cmd, Priority.DEFAULT, cancellable);
yield cx.send_async(cmd, cancellable);
} catch (Error err) {
return new AsyncCommandResponse(null, user, err);
}
@ -1050,6 +1052,11 @@ public class Geary.Imap.ClientSession {
#endif
}
private void on_network_flush_error(Error err) {
debug("Flush error on %s: %s", to_string(), err.message);
fsm.issue(Event.SEND_ERROR, null, null, err);
}
private void on_received_status_response(StatusResponse status_response) {
assert(!current_cmd_response.is_sealed());
current_cmd_response.seal(status_response);
@ -1086,7 +1093,7 @@ public class Geary.Imap.ClientSession {
debug("Received bad response %s: %s", root.to_string(), err.message);
}
private void on_receive_failed(Error err) {
private void on_network_receive_failure(Error err) {
debug("Receive failed: %s", err.message);
fsm.issue(Event.RECV_ERROR, null, null, err);
}

View file

@ -4,12 +4,14 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount {
public class Geary.Sqlite.Account : Geary.AbstractAccount, Geary.LocalAccount {
private MailDatabase db;
private FolderTable folder_table;
private MessageTable message_table;
public Account(Geary.Credentials cred) {
base ("SQLite account for %s".printf(cred.to_string()));
try {
db = new MailDatabase(cred.user);
} catch (Error err) {
@ -20,53 +22,76 @@ public class Geary.Sqlite.Account : Object, Geary.Account, Geary.LocalAccount {
message_table = db.get_message_table();
}
public Geary.Email.Field get_required_fields_for_writing() {
public override Geary.Email.Field get_required_fields_for_writing() {
return Geary.Email.Field.NONE;
}
public async void create_folder_async(Geary.Folder? parent, Geary.Folder folder,
Cancellable? cancellable = null) throws Error {
yield folder_table.create_async(new FolderRow(folder_table, folder.get_name(), Row.INVALID_ID),
cancellable);
private async int64 fetch_id_async(Geary.FolderPath path, Cancellable? cancellable = null)
throws Error {
FolderRow? row = yield folder_table.fetch_descend_async(path.as_list(), cancellable);
if (row == null)
throw new EngineError.NOT_FOUND("Cannot find local path to %s", path.to_string());
return row.id;
}
public async void create_many_folders_async(Geary.Folder? parent, Gee.Collection<Geary.Folder> folders,
private async int64 fetch_parent_id_async(Geary.FolderPath path, Cancellable? cancellable = null)
throws Error {
return path.is_root() ? Row.INVALID_ID : yield fetch_id_async(path.get_parent(), cancellable);
}
public async void clone_folder_async(Geary.Folder folder, Cancellable? cancellable = null)
throws Error {
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)
rows.add(new FolderRow(db.get_folder_table(), folder.get_name(), Row.INVALID_ID));
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));
}
yield folder_table.create_many_async(rows, cancellable);
}
public async Gee.Collection<Geary.Folder> list_folders_async(Geary.Folder? parent,
public override async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error {
Gee.List<FolderRow> rows = yield folder_table.list_async(Row.INVALID_ID, cancellable);
int64 parent_id = (parent != null)
? yield fetch_id_async(parent, cancellable)
: Row.INVALID_ID;
if (parent != null)
assert(parent_id != Row.INVALID_ID);
Gee.List<FolderRow> rows = yield folder_table.list_async(parent_id, cancellable);
if (rows.size == 0) {
throw new EngineError.NOT_FOUND("No local folders in %s",
(parent != null) ? parent.get_fullpath() : "root");
}
Gee.Collection<Geary.Folder> folders = new Gee.ArrayList<Geary.Sqlite.Folder>();
foreach (FolderRow row in rows)
folders.add(new Geary.Sqlite.Folder(db, row));
foreach (FolderRow row in rows) {
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));
}
return folders;
}
public async Geary.Folder fetch_folder_async(Geary.Folder? parent, string folder_name,
public override async Geary.Folder fetch_folder_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error {
FolderRow? row = yield folder_table.fetch_async(Row.INVALID_ID, folder_name, cancellable);
FolderRow? row = yield folder_table.fetch_descend_async(path.as_list(), cancellable);
if (row == null)
throw new EngineError.NOT_FOUND("\"%s\" not found in local database", folder_name);
throw new EngineError.NOT_FOUND("%s not found in local database", path.to_string());
return new Geary.Sqlite.Folder(db, row);
}
public async void remove_folder_async(Geary.Folder folder, Cancellable? cancellable = null)
throws Error {
// TODO
}
public async void remove_many_folders_async(Gee.Set<Geary.Folder> folders,
Cancellable? cancellable = null) throws Error {
// TODO
return new Geary.Sqlite.Folder(db, row, path);
}
public async bool has_message_id_async(Geary.RFC822.MessageID message_id, out int count,

View file

@ -13,14 +13,13 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
private MessageTable message_table;
private MessageLocationTable location_table;
private ImapMessageLocationPropertiesTable imap_location_table;
private string name;
private Geary.FolderPath path;
private bool opened = false;
internal Folder(MailDatabase db, FolderRow folder_row) throws Error {
internal Folder(MailDatabase db, FolderRow folder_row, Geary.FolderPath path) throws Error {
this.db = db;
this.folder_row = folder_row;
name = folder_row.name;
this.path = path;
message_table = db.get_message_table();
location_table = db.get_message_location_table();
@ -32,8 +31,8 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
throw new EngineError.OPEN_REQUIRED("%s not open", to_string());
}
public override string get_name() {
return name;
public override Geary.FolderPath get_path() {
return path;
}
public override Geary.FolderProperties? get_properties() {
@ -75,7 +74,7 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
if (yield imap_location_table.search_uid_in_folder(location.uid, folder_row.id, out message_id,
cancellable)) {
throw new EngineError.ALREADY_EXISTS("Email with UID %s already exists in %s",
location.uid.to_string(), get_name());
location.uid.to_string(), to_string());
}
message_id = yield message_table.create_async(
@ -155,8 +154,10 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
MessageLocationRow? location_row = yield location_table.fetch_async(folder_row.id, position,
cancellable);
if (location_row == null)
throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name);
if (location_row == null) {
throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position,
to_string());
}
assert(location_row.position == position);
@ -164,15 +165,17 @@ public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder {
location_row.id, cancellable);
if (imap_location_row == null) {
throw new EngineError.NOT_FOUND("No IMAP location properties at position %d in %s",
position, name);
position, to_string());
}
assert(imap_location_row.location_id == location_row.id);
MessageRow? message_row = yield message_table.fetch_async(location_row.message_id,
required_fields, cancellable);
if (message_row == null)
throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position, name);
if (message_row == null) {
throw new EngineError.NOT_FOUND("No message at position %d in folder %s", position,
to_string());
}
if (!message_row.fields.is_set(required_fields)) {
throw new EngineError.INCOMPLETE_MESSAGE(

View file

@ -87,5 +87,46 @@ public class Geary.Sqlite.FolderTable : Geary.Sqlite.Table {
return (!result.finished) ? new FolderRow.from_query_result(this, result) : null;
}
public async FolderRow? fetch_descend_async(Gee.List<string> path, Cancellable? cancellable = null)
throws Error {
assert(path.size > 0);
int64 parent_id = Row.INVALID_ID;
// walk the folder tree to the final node (which is at length - 1 - 1)
int length = path.size;
for (int ctr = 0; ctr < length - 1; ctr++) {
SQLHeavy.Query query;
if (parent_id != Row.INVALID_ID) {
query = db.prepare("SELECT id FROM FolderTable WHERE parent_id=? AND name=?");
query.bind_int64(0, parent_id);
query.bind_string(1, path[ctr]);
} else {
query = db.prepare("SELECT id FROM FolderTable WHERE parent_id IS NULL AND name=?");
query.bind_string(0, path[ctr]);
}
SQLHeavy.QueryResult result = yield query.execute_async(cancellable);
if (result.finished)
return null;
int64 id = result.fetch_int64(0);
// watch for loops, real bad if it happens ... could be more thorough here, but at least
// one level of checking is better than none
if (id == parent_id) {
warning("Loop found in database: parent of %lld is %lld in FolderTable",
parent_id, id);
return null;
}
parent_id = id;
}
// do full fetch on this folder
return yield fetch_async(parent_id, path.last(), cancellable);
}
}

View file

@ -103,7 +103,7 @@ public class Geary.State.Machine {
}
public string to_string() {
return "Machine %s".printf(descriptor.name);
return "Machine %s [%s]".printf(descriptor.name, descriptor.get_state_string(state));
}
}

View file

@ -6,6 +6,7 @@
def build(bld):
bld.common_src = [
'../common/common-date.vala',
'../common/common-intl.vala',
'../common/common-yorba-application.vala'
]
@ -13,23 +14,28 @@ def build(bld):
bld.common_packages = ['glib-2.0', 'unique-1.0', 'posix' ]
bld.engine_src = [
'../engine/api/geary-abstract-account.vala',
'../engine/api/geary-abstract-folder.vala',
'../engine/api/geary-account.vala',
'../engine/api/geary-credentials.vala',
'../engine/api/geary-email-location.vala',
'../engine/api/geary-email-properties.vala',
'../engine/api/geary-email.vala',
'../engine/api/geary-engine-account.vala',
'../engine/api/geary-engine-error.vala',
'../engine/api/geary-engine-folder.vala',
'../engine/api/geary-engine.vala',
'../engine/api/geary-folder-path.vala',
'../engine/api/geary-folder-properties.vala',
'../engine/api/geary-folder.vala',
'../engine/api/geary-imap-engine.vala',
'../engine/api/geary-generic-imap-account.vala',
'../engine/api/geary-local-interfaces.vala',
'../engine/api/geary-remote-interfaces.vala',
'../engine/common/common-interfaces.vala',
'../engine/common/common-message-data.vala',
'../engine/common/common-nonblocking-mailbox.vala',
'../engine/common/common-nonblocking-mutex.vala',
'../engine/common/common-nonblocking-semaphore.vala',
'../engine/common/common-string.vala',