diff --git a/Makefile b/Makefile index 160429aa..56244fd4 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,23 @@ BUILD_ROOT = 1 VALAC := valac VALAFLAGS := -g --enable-checking --fatal-warnings --vapidir=vapi -APPS := geary console syntax lsmbox readmail watchmbox +APPS := geary console watchmbox ENGINE_SRC := \ src/engine/Engine.vala \ - src/engine/Interfaces.vala \ - src/engine/Email.vala \ + src/engine/api/Account.vala \ + src/engine/api/Email.vala \ + src/engine/api/Folder.vala \ + src/engine/api/Credentials.vala \ + src/engine/api/EngineError.vala \ + src/engine/sqlite/Database.vala \ + src/engine/sqlite/Table.vala \ + src/engine/sqlite/Row.vala \ + src/engine/sqlite/MailDatabase.vala \ + src/engine/sqlite/FolderTable.vala \ + src/engine/sqlite/FolderRow.vala \ + src/engine/sqlite/api/Account.vala \ + src/engine/sqlite/api/Folder.vala \ src/engine/state/Machine.vala \ src/engine/state/MachineDescriptor.vala \ src/engine/state/Mapping.vala \ @@ -50,15 +61,22 @@ ENGINE_SRC := \ src/engine/imap/decoders/ListResults.vala \ src/engine/imap/decoders/SelectExamineResults.vala \ src/engine/imap/decoders/StatusResults.vala \ + src/engine/imap/api/Account.vala \ + src/engine/imap/api/Folder.vala \ src/engine/rfc822/MailboxAddress.vala \ src/engine/rfc822/MessageData.vala \ - src/engine/util/String.vala \ src/engine/util/Memory.vala \ - src/engine/util/ReferenceSemantics.vala + src/engine/util/ReferenceSemantics.vala \ + src/engine/util/Trillian.vala + +COMMON_SRC := \ + src/common/String.vala \ + src/common/Interfaces.vala \ + src/common/YorbaApplication.vala \ + src/common/Date.vala CLIENT_SRC := \ src/client/main.vala \ - src/client/YorbaApplication.vala \ src/client/GearyApplication.vala \ src/client/ui/MainWindow.vala \ src/client/ui/MessageListView.vala \ @@ -67,32 +85,23 @@ CLIENT_SRC := \ src/client/ui/FolderListStore.vala \ src/client/ui/MessageViewer.vala \ src/client/ui/MessageBuffer.vala \ - src/client/util/Intl.vala \ - src/client/util/Date.vala + src/client/util/Intl.vala CONSOLE_SRC := \ src/console/main.vala -SYNTAX_SRC := \ - src/tests/syntax.vala - -LSMBOX_SRC := \ - src/tests/lsmbox.vala - -READMAIL_SRC := \ - src/tests/readmail.vala - WATCHMBOX_SRC := \ src/tests/watchmbox.vala -ALL_SRC := $(ENGINE_SRC) $(CLIENT_SRC) $(CONSOLE_SRC) $(SYNTAX_SRC) $(LSMBOX_SRC) $(READMAIL_SRC) $(WATCHMBOX_SRC) +ALL_SRC := $(ENGINE_SRC) $(COMMON_SRC) $(CLIENT_SRC) $(CONSOLE_SRC) $(WATCHMBOX_SRC) EXTERNAL_PKGS := \ gio-2.0 >= 2.26.1 \ gee-1.0 >= 0.6.1 \ gtk+-2.0 >= 2.22.1 \ unique-1.0 >= 1.0.0 \ - gmime-2.4 >= 2.4.14 + gmime-2.4 >= 2.4.14 \ + sqlheavy-0.1 >= 0.0.1 EXTERNAL_BINDINGS := \ gio-2.0 \ @@ -100,15 +109,16 @@ EXTERNAL_BINDINGS := \ gtk+-2.0 \ unique-1.0 \ posix \ - gmime-2.4 + gmime-2.4 \ + sqlheavy-0.1 VAPI_FILES := \ vapi/gmime-2.4.vapi -geary: $(ENGINE_SRC) $(CLIENT_SRC) Makefile $(VAPI_FILES) +geary: $(ENGINE_SRC) $(COMMON_SRC) $(CLIENT_SRC) Makefile $(VAPI_FILES) pkg-config --exists --print-errors '$(EXTERNAL_PKGS)' $(VALAC) $(VALAFLAGS) $(foreach binding,$(EXTERNAL_BINDINGS),--pkg=$(binding)) \ - $(ENGINE_SRC) $(CLIENT_SRC) \ + $(ENGINE_SRC) $(COMMON_SRC) $(CLIENT_SRC) \ -o $@ .PHONY: all @@ -120,28 +130,13 @@ clean: rm -f $(ALL_SRC:.vala=.vala.c) rm -f $(APPS) -console: $(ENGINE_SRC) $(CONSOLE_SRC) Makefile +console: $(ENGINE_SRC) $(COMMON_SRC) $(CONSOLE_SRC) Makefile $(VALAC) $(VALAFLAGS) $(foreach binding,$(EXTERNAL_BINDINGS),--pkg=$(binding)) \ - $(ENGINE_SRC) $(CONSOLE_SRC) \ + $(ENGINE_SRC) $(COMMON_SRC) $(CONSOLE_SRC) \ -o $@ -syntax: $(ENGINE_SRC) $(SYNTAX_SRC) Makefile +watchmbox: $(ENGINE_SRC) $(COMMON_SRC) $(WATCHMBOX_SRC) Makefile $(VALAC) $(VALAFLAGS) $(foreach binding,$(EXTERNAL_BINDINGS),--pkg=$(binding)) \ - $(ENGINE_SRC) $(SYNTAX_SRC) \ - -o $@ - -lsmbox: $(ENGINE_SRC) $(LSMBOX_SRC) Makefile - $(VALAC) $(VALAFLAGS) $(foreach binding,$(EXTERNAL_BINDINGS),--pkg=$(binding)) \ - $(ENGINE_SRC) $(LSMBOX_SRC) \ - -o $@ - -readmail: $(ENGINE_SRC) $(READMAIL_SRC) Makefile - $(VALAC) $(VALAFLAGS) $(foreach binding,$(EXTERNAL_BINDINGS),--pkg=$(binding)) \ - $(ENGINE_SRC) $(READMAIL_SRC) \ - -o $@ - -watchmbox: $(ENGINE_SRC) $(WATCHMBOX_SRC) Makefile - $(VALAC) $(VALAFLAGS) $(foreach binding,$(EXTERNAL_BINDINGS),--pkg=$(binding)) \ - $(ENGINE_SRC) $(WATCHMBOX_SRC) \ + $(ENGINE_SRC) $(COMMON_SRC) $(WATCHMBOX_SRC) \ -o $@ diff --git a/sql/Create.sql b/sql/Create.sql new file mode 100644 index 00000000..3b587228 --- /dev/null +++ b/sql/Create.sql @@ -0,0 +1,12 @@ + +CREATE TABLE FolderTable ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + supports_children INTEGER, + is_openable INTEGER, + parent_id INTEGER +); + +CREATE INDEX FolderTableNameIndex ON FolderTable (name); +CREATE INDEX FolderTableParentIndex ON FolderTable (parent_id); + diff --git a/src/client/GearyApplication.vala b/src/client/GearyApplication.vala index a10f68af..326a4b0d 100644 --- a/src/client/GearyApplication.vala +++ b/src/client/GearyApplication.vala @@ -6,9 +6,9 @@ public class GearyApplication : YorbaApplication { // TODO: replace static strings with const strings when gettext is integrated properly - public const string PROGRAM_NAME = "Geary"; - public static string PROGRAM_DESCRIPTION = _("Email Client"); - public const string VERSION = "0.0.1"; + public const string NAME = "Geary"; + public static string DESCRIPTION = _("Email Client"); + public const string VERSION = "0.0.0+trunk"; public const string COPYRIGHT = "Copyright 2011 Yorba Foundation"; public const string WEBSITE = "http://www.yorba.org"; public static string WEBSITE_LABEL = _("Visit the Yorba web site"); @@ -33,7 +33,7 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """; - public static GearyApplication instance { + public new static GearyApplication instance { get { if (_instance == null) _instance = new GearyApplication(); @@ -45,15 +45,23 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc., private static GearyApplication? _instance = null; private MainWindow main_window = new MainWindow(); - private Geary.Engine engine = new Geary.Engine(); + private Geary.Account? account = null; private GearyApplication() { - base ("org.yorba.geary"); + base (NAME, "geary", "org.yorba.geary"); } public override void startup() { + Geary.Credentials cred = new Geary.Credentials("imap.gmail.com", args[1], args[2]); + + try { + account = Geary.Engine.open(cred); + } catch (Error err) { + error("Unable to open mail database for %s: %s", cred.user, err.message); + } + main_window.show_all(); - main_window.login(engine, args[1], args[2]); + main_window.start(account); } public override void activate() { diff --git a/src/client/ui/FolderListStore.vala b/src/client/ui/FolderListStore.vala index 675c32b5..070b5f53 100644 --- a/src/client/ui/FolderListStore.vala +++ b/src/client/ui/FolderListStore.vala @@ -7,15 +7,20 @@ public class FolderListStore : Gtk.TreeStore { public enum Column { NAME, + FOLDER_OBJECT, N_COLUMNS; public static Column[] all() { - return { NAME }; + return { + NAME, + FOLDER_OBJECT + }; } public static Type[] get_types() { return { - typeof (string) + typeof (string), + typeof (Geary.Folder) }; } @@ -24,6 +29,9 @@ public class FolderListStore : Gtk.TreeStore { case NAME: return _("Name"); + case FOLDER_OBJECT: + return "(hidden)"; + default: assert_not_reached(); } @@ -34,25 +42,28 @@ public class FolderListStore : Gtk.TreeStore { set_column_types(Column.get_types()); } - public void add_folder(Geary.FolderDetail folder) { + public void add_folder(Geary.Folder folder) { Gtk.TreeIter iter; append(out iter, null); - set(iter, Column.NAME, folder.name); + set(iter, + Column.NAME, folder.name, + Column.FOLDER_OBJECT, folder + ); } - public void add_folders(Gee.Collection folders) { - foreach (Geary.FolderDetail folder in folders) + public void add_folders(Gee.Collection folders) { + foreach (Geary.Folder folder in folders) add_folder(folder); } - public string? get_folder_at(Gtk.TreePath path) { + public Geary.Folder? get_folder_at(Gtk.TreePath path) { Gtk.TreeIter iter; if (!get_iter(out iter, path)) return null; - string folder; - get(iter, Column.NAME, out folder); + Geary.Folder folder; + get(iter, Column.FOLDER_OBJECT, out folder); return folder; } diff --git a/src/client/ui/FolderListView.vala b/src/client/ui/FolderListView.vala index 4906f568..17ca605d 100644 --- a/src/client/ui/FolderListView.vala +++ b/src/client/ui/FolderListView.vala @@ -5,7 +5,7 @@ */ public class FolderListView : Gtk.TreeView { - public signal void folder_selected(string? folder); + public signal void folder_selected(Geary.Folder? folder); public FolderListView(FolderListStore store) { set_model(store); @@ -31,7 +31,7 @@ public class FolderListView : Gtk.TreeView { return; } - string? folder = get_store().get_folder_at(path); + Geary.Folder? folder = get_store().get_folder_at(path); if (folder != null) folder_selected(folder); } diff --git a/src/client/ui/MainWindow.vala b/src/client/ui/MainWindow.vala index 1abc843d..5ea68388 100644 --- a/src/client/ui/MainWindow.vala +++ b/src/client/ui/MainWindow.vala @@ -26,12 +26,11 @@ 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.Engine? engine = null; private Geary.Account? account = null; private Geary.Folder? current_folder = null; public MainWindow() { - title = GearyApplication.PROGRAM_NAME; + title = GearyApplication.NAME; set_default_size(800, 600); try { @@ -57,26 +56,34 @@ public class MainWindow : Gtk.Window { create_layout(); } - public void login(Geary.Engine engine, string user, string pass) { - this.engine = engine; - - do_login.begin(user, pass); + ~MainWindow() { + if (account != null) + account.folders_added_removed.disconnect(on_folders_added_removed); } - private async void do_login(string user, string pass) { + public void start(Geary.Account account) { + this.account = account; + account.folders_added_removed.connect(on_folders_added_removed); + + do_start.begin(); + } + + private void on_folders_added_removed(Gee.Collection? added, + Gee.Collection? removed) { + if (added != null) { + folder_list_store.add_folders(added); + debug("%d folders added", added.size); + } + } + + private async void do_start() { try { - account = yield engine.login("imap.gmail.com", user, pass); - if (account == null) - error("Unable to login"); - // pull down the root-level folders - Gee.Collection folders = yield account.list(null); - if (folders != null) { - debug("%d folders found", folders.size); - folder_list_store.add_folders(folders); - } else { + Gee.Collection folders = yield account.list_async(null); + if (folders != null) + on_folders_added_removed(folders, null); + else debug("no folders"); - } } catch (Error err) { error("%s", err.message); } @@ -162,7 +169,8 @@ public class MainWindow : Gtk.Window { private void on_about() { Gtk.show_about_dialog(this, - "program-name", GearyApplication.PROGRAM_NAME, + "program-name", GearyApplication.NAME, + "comments", GearyApplication.DESCRIPTION, "authors", GearyApplication.AUTHORS, "copyright", GearyApplication.COPYRIGHT, "license", GearyApplication.LICENSE, @@ -172,22 +180,30 @@ public class MainWindow : Gtk.Window { ); } - private void on_folder_selected(string? folder) { + private void on_folder_selected(Geary.Folder? folder) { if (folder == null) { + debug("no folder selected"); message_list_store.clear(); return; } + debug("Folder %s selected", folder.name); + do_select_folder.begin(folder, on_select_folder_completed); } - private async void do_select_folder(string folder_name) throws Error { + private async void do_select_folder(Geary.Folder folder) throws Error { message_list_store.clear(); - current_folder = yield account.open(folder_name); + if (current_folder != null) + yield current_folder.close_async(); - Gee.List? headers = yield current_folder.read(1, 100); + current_folder = folder; + + yield current_folder.open_async(true); + + Gee.List? headers = yield current_folder.read_async(1, 100); if (headers != null && headers.size > 0) { foreach (Geary.EmailHeader header in headers) message_list_store.append_header(header); @@ -219,8 +235,8 @@ public class MainWindow : Gtk.Window { return; } - Geary.EmailBody body = yield current_folder.fetch_body(header); - message_buffer.set_text(body.full); + Geary.Email email = yield current_folder.fetch_async(header); + message_buffer.set_text(email.full); } private void on_select_message_completed(Object? source, AsyncResult result) { diff --git a/src/client/util/Date.vala b/src/common/Date.vala similarity index 100% rename from src/client/util/Date.vala rename to src/common/Date.vala diff --git a/src/common/Interfaces.vala b/src/common/Interfaces.vala new file mode 100644 index 00000000..f07f03ef --- /dev/null +++ b/src/common/Interfaces.vala @@ -0,0 +1,22 @@ +/* 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 interface Geary.Comparable { + public abstract bool equals(Comparable other); + + public static bool equal_func(void *a, void *b) { + return ((Comparable *) a)->equals((Comparable *) b); + } +} + +public interface Geary.Hashable { + public abstract uint get_hash(); + + public static uint hash_func(void *ptr) { + return ((Hashable *) ptr)->get_hash(); + } +} + diff --git a/src/engine/util/String.vala b/src/common/String.vala similarity index 100% rename from src/engine/util/String.vala rename to src/common/String.vala diff --git a/src/client/YorbaApplication.vala b/src/common/YorbaApplication.vala similarity index 59% rename from src/client/YorbaApplication.vala rename to src/common/YorbaApplication.vala index 681850f0..9149f92e 100644 --- a/src/client/YorbaApplication.vala +++ b/src/common/YorbaApplication.vala @@ -4,16 +4,18 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -// -// YorbaApplication is a poor man's lookalike of GNOME 3's GApplication, with a couple of additions. -// It's only here to give some of GApplication's functionality in a GTK+ 2 environment. The idea -// is to ease a future migration to GTK 3. -// -// YorbaApplication specifically expects to be run in a GTK environment, and Gtk.init() *must* be -// called prior to invoking YorbaApplication. -// +/** + * YorbaApplication is a poor man's lookalike of GNOME 3's GApplication, with a couple of additions. + * It's only here to give some of GApplication's functionality in a GTK+ 2 environment. The idea + * is to ease a future migration to GTK 3. + * + * YorbaApplication specifically expects to be run in a GTK environment, and Gtk.init() *must* be + * called prior to invoking YorbaApplication. + */ public abstract class YorbaApplication { + public static YorbaApplication? instance { get; private set; default = null; } + public bool registered { get; private set; } public string[]? args { get; private set; } @@ -23,6 +25,12 @@ public abstract class YorbaApplication { private int exitcode = 0; private Unique.App? unique_app = null; + /** + * This signal is fired only when the application is starting the first time, not on + * subsequent activations (i.e. the application is launched while running by the user). + * + * The args[] array will be available when this signal is fired. + */ public virtual signal void startup() { } @@ -32,8 +40,20 @@ public abstract class YorbaApplication { public virtual signal void exiting(bool panicked) { } - protected YorbaApplication(string app_id) { + /** + * application_title is a localized name of the application. program_name is non-localized + * and used by the system. app_id is a CORBA-esque program identifier. + * + * Only one YorbaApplication instance may be created in an program. + */ + protected YorbaApplication(string application_title, string program_name, string app_id) { this.app_id = app_id; + + Environment.set_application_name(application_title); + Environment.set_prgname(program_name); + + assert(instance == null); + instance = this; } public bool register(Cancellable? cancellable = null) throws Error { @@ -110,5 +130,22 @@ public abstract class YorbaApplication { Posix.exit(1); } + + public File get_user_data_directory() { + return File.new_for_path(Environment.get_user_data_dir()).get_child(Environment.get_prgname()); + } + + /** + * Returns the base directory that the application's various resource files are stored. If the + * application is running from its installed directory, this will point to + * $(BASEDIR)/share/. If it's running from the build directory, this points to + * that. + * + * TODO: Implement. This is placeholder code for build environments and assumes you're running + * the program in the build directory. + */ + public File get_resource_directory() { + return File.new_for_path(Environment.get_current_dir()); + } } diff --git a/src/engine/Engine.vala b/src/engine/Engine.vala index 3d28e6a8..0c01376b 100644 --- a/src/engine/Engine.vala +++ b/src/engine/Engine.vala @@ -4,10 +4,99 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Engine : Object { - public static async Account? login(string server, string user, string pass) throws Error { - return new Imap.ClientSessionManager(server, Imap.ClientConnection.DEFAULT_PORT_TLS, user, - pass); +public class Geary.Engine : Object, Geary.Account { + private NetworkAccount net; + private LocalAccount local; + + private Engine(NetworkAccount net, LocalAccount local) { + this.net = net; + this.local = local; + } + + public static Account open(Geary.Credentials cred) throws Error { + return new Engine( + new Geary.Imap.Account(cred, Imap.ClientConnection.DEFAULT_PORT_TLS), + new Geary.Sqlite.Account(cred)); + } + + public async Gee.Collection list_async(string? parent_folder, + Cancellable? cancellable = null) throws Error { + Gee.Collection list = yield local.list_async(parent_folder, cancellable); + + background_update_folders.begin(parent_folder, list); + + debug("Reporting %d folders", list.size); + + return list; + } + + private Gee.Set get_folder_names(Gee.Collection folders) { + Gee.Set names = new Gee.HashSet(); + foreach (Geary.Folder folder in folders) + names.add(folder.name); + + return names; + } + + private Gee.List get_excluded_folders(Gee.Collection folders, + Gee.Set names) { + Gee.List excluded = new Gee.ArrayList(); + foreach (Geary.Folder folder in folders) { + if (!names.contains(folder.name)) + excluded.add(folder); + } + + return excluded; + } + + private async void background_update_folders(string? parent_folder, + Gee.Collection local_folders) { + Gee.Collection net_folders; + try { + net_folders = yield net.list_async(parent_folder); + } catch (Error neterror) { + error("Unable to retrieve folder list from server: %s", neterror.message); + } + + Gee.Set local_names = get_folder_names(local_folders); + Gee.Set net_names = get_folder_names(net_folders); + + debug("%d local names, %d net names", local_names.size, net_names.size); + + Gee.List? to_add = get_excluded_folders(net_folders, local_names); + Gee.List? to_remove = get_excluded_folders(local_folders, net_names); + + debug("Adding %d, removing %d to/from local store", to_add.size, to_remove.size); + + if (to_add.size == 0) + to_add = null; + + if (to_remove.size == 0) + to_remove = null; + + if (to_add != null || to_remove != null) + notify_folders_added_removed(to_add, null); + + try { + if (to_add != null) + yield local.create_many_async(to_add); + } catch (Error err) { + error("Unable to add/remove folders: %s", err.message); + } + } + + public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { + } + + public async void create_many_async(Gee.Collection folders, + Cancellable? cancellable = null) throws Error { + } + + public async void remove_async(string folder, Cancellable? cancellable = null) throws Error { + } + + public async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) + throws Error { } } diff --git a/src/engine/Interfaces.vala b/src/engine/Interfaces.vala deleted file mode 100644 index a2ee654a..00000000 --- a/src/engine/Interfaces.vala +++ /dev/null @@ -1,39 +0,0 @@ -/* 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 interface Geary.FolderDetail : Object { - public abstract string name { get; protected set; } -} - -public interface Geary.Account : Object { - public abstract async Gee.Collection list(FolderDetail? parent, - Cancellable? cancellable = null) throws Error; - - public abstract async Folder open(string folder, Cancellable? cancellable = null) throws Error; -} - -public interface Geary.Folder : Object { - public enum CloseReason { - LOCAL_CLOSE, - REMOTE_CLOSE, - FOLDER_CLOSED - } - - public abstract string name { get; protected set; } - - public abstract int count { get; protected set; } - - public abstract bool is_readonly { get; protected set; } - - public signal void closed(CloseReason reason); - - public abstract async Gee.List? read(int low, int count, Cancellable? cancellable = null) - throws Error; - - public abstract async EmailBody fetch_body(EmailHeader header, Cancellable? cancellable = null) - throws Error; -} - diff --git a/src/engine/api/Account.vala b/src/engine/api/Account.vala new file mode 100644 index 00000000..f6f9a22d --- /dev/null +++ b/src/engine/api/Account.vala @@ -0,0 +1,40 @@ +/* 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 interface Geary.Account : Object { + public signal void folders_added_removed(Gee.Collection? added, + Gee.Collection? removed); + + protected virtual void notify_folders_added_removed(Gee.Collection? added, + Gee.Collection? removed) { + folders_added_removed(added, removed); + } + + public abstract async Gee.Collection list_async(string? parent_folder, + Cancellable? cancellable = null) throws Error; + + public abstract async void create_async(Geary.Folder folder, Cancellable? cancellable = null) + throws Error; + + public abstract async void create_many_async(Gee.Collection folders, + Cancellable? cancellable = null) throws Error; + + public abstract async void remove_async(string folder, Cancellable? cancellable = null) + throws Error; + + public abstract async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) + throws Error; +} + +public interface Geary.NetworkAccount : Object, Geary.Account { + public signal void connectivity_changed(bool online); + + public abstract bool is_online(); +} + +public interface Geary.LocalAccount : Object, Geary.Account { +} + diff --git a/src/engine/api/Credentials.vala b/src/engine/api/Credentials.vala new file mode 100644 index 00000000..d865b493 --- /dev/null +++ b/src/engine/api/Credentials.vala @@ -0,0 +1,18 @@ +/* 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.Credentials { + public string server { get; private set; } + public string user { get; private set; } + public string pass { get; private set; } + + public Credentials(string server, string user, string pass) { + this.server = server; + this.user = user; + this.pass = pass; + } +} + diff --git a/src/engine/Email.vala b/src/engine/api/Email.vala similarity index 87% rename from src/engine/Email.vala rename to src/engine/api/Email.vala index 5b86568a..d6e22e9f 100644 --- a/src/engine/Email.vala +++ b/src/engine/api/Email.vala @@ -23,11 +23,11 @@ public class Geary.EmailHeader : Object { } } -public class Geary.EmailBody : Object { +public class Geary.Email : Object { public EmailHeader header { get; private set; } public string full { get; private set; } - public EmailBody(EmailHeader header, string full) { + public Email(EmailHeader header, string full) { this.header = header; this.full = full; } @@ -36,7 +36,7 @@ public class Geary.EmailBody : Object { * This does not return the full body or any portion of it. It's intended only for debugging. */ public string to_string() { - return "email body (%d bytes)".printf(full.data.length); + return "%s (%d bytes)".printf(header.to_string(), full.data.length); } } diff --git a/src/engine/api/EngineError.vala b/src/engine/api/EngineError.vala new file mode 100644 index 00000000..573d725f --- /dev/null +++ b/src/engine/api/EngineError.vala @@ -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. + */ + +public errordomain Geary.EngineError { + OPEN_REQUIRED, + ALREADY_OPEN +} + diff --git a/src/engine/api/Folder.vala b/src/engine/api/Folder.vala new file mode 100644 index 00000000..1255146e --- /dev/null +++ b/src/engine/api/Folder.vala @@ -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 interface Geary.Folder : Object { + public enum CloseReason { + LOCAL_CLOSE, + REMOTE_CLOSE, + FOLDER_CLOSED + } + + public abstract string name { get; protected set; } + public abstract Trillian is_readonly { get; protected set; } + public abstract Trillian supports_children { get; protected set; } + public abstract Trillian has_children { get; protected set; } + public abstract Trillian is_openable { get; protected set; } + + public signal void opened(); + + public signal void closed(CloseReason reason); + + public signal void updated(); + + public abstract async void open_async(bool readonly, Cancellable? cancellable = null) throws Error; + + public abstract async void close_async(Cancellable? cancellable = null) throws Error; + + public abstract int get_message_count() throws Error; + + public abstract async Gee.List? read_async(int low, int count, + Cancellable? cancellable = null) throws Error; + + public abstract async Geary.Email fetch_async(Geary.EmailHeader header, + Cancellable? cancellable = null) throws Error; +} + diff --git a/src/engine/imap/ClientSession.vala b/src/engine/imap/ClientSession.vala index 0b83a525..1bf4c8ed 100644 --- a/src/engine/imap/ClientSession.vala +++ b/src/engine/imap/ClientSession.vala @@ -5,9 +5,9 @@ */ public class Geary.Imap.ClientSession { - // 30 min keepalive required to maintain session; back off by 30 sec for breathing room - public const int MIN_KEEPALIVE_SEC = (30 * 60) - 30; - public const int DEFAULT_KEEPALIVE_SEC = 60; + // 30 min keepalive required to maintain session; back off by 5 min for breathing room + public const int MIN_KEEPALIVE_SEC = 25 * 60; + public const int DEFAULT_KEEPALIVE_SEC = 3 * 60; public enum Context { UNCONNECTED, diff --git a/src/engine/imap/ClientSessionManager.vala b/src/engine/imap/ClientSessionManager.vala index 87f42a25..90e38670 100644 --- a/src/engine/imap/ClientSessionManager.vala +++ b/src/engine/imap/ClientSessionManager.vala @@ -4,21 +4,17 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.ClientSessionManager : Object, Geary.Account { - private string server; +public class Geary.Imap.ClientSessionManager { + private Credentials cred; private uint default_port; - private string user; - private string pass; private Gee.HashSet sessions = new Gee.HashSet(); - private Gee.HashSet examined_contexts = new Gee.HashSet(); - private Gee.HashSet selected_contexts = new Gee.HashSet(); + private Gee.HashSet examined_contexts = new Gee.HashSet(); + private Gee.HashSet selected_contexts = new Gee.HashSet(); private int keepalive_sec = ClientSession.DEFAULT_KEEPALIVE_SEC; - public ClientSessionManager(string server, uint default_port, string user, string pass) { - this.server = server; + public ClientSessionManager(Credentials cred, uint default_port) { + this.cred = cred; this.default_port = default_port; - this.user = user; - this.pass = pass; } /** @@ -33,9 +29,10 @@ public class Geary.Imap.ClientSessionManager : Object, Geary.Account { session.enable_keepalives(keepalive_sec); } - public async Gee.Collection list(Geary.FolderDetail? parent, + public async Gee.Collection list(string? parent_name, Cancellable? cancellable = null) throws Error { - string specifier = (parent != null) ? parent.name : "/"; + // build a proper IMAP specifier + string specifier = parent_name ?? "/"; specifier += (specifier.has_suffix("/")) ? "%" : "/%"; ClientSession session = yield get_authorized_session(cancellable); @@ -54,31 +51,31 @@ public class Geary.Imap.ClientSessionManager : Object, Geary.Account { return yield select_examine_mailbox(path, false, cancellable); } - private async Mailbox select_examine_mailbox(string path, bool is_select, + public async Mailbox select_examine_mailbox(string path, bool is_select, Cancellable? cancellable = null) throws Error { - Gee.HashSet contexts = is_select ? selected_contexts : examined_contexts; + Gee.HashSet contexts = is_select ? selected_contexts : examined_contexts; - foreach (MailboxContext mailbox_context in contexts) { - if (mailbox_context.name == path) - return new Mailbox(mailbox_context); + foreach (SelectedContext context in contexts) { + if (context.name == path) + return new Mailbox(context); } SelectExamineResults results; ClientSession session = yield select_examine_async(path, is_select, out results, cancellable); - MailboxContext new_mailbox_context = new MailboxContext(session, results); + SelectedContext new_context = new SelectedContext(session, results); // Can't use the ternary operator due to this bug: // https://bugzilla.gnome.org/show_bug.cgi?id=599349 if (is_select) - new_mailbox_context.freed.connect(on_selected_context_freed); + new_context.freed.connect(on_selected_context_freed); else - new_mailbox_context.freed.connect(on_examined_context_freed); + new_context.freed.connect(on_examined_context_freed); - bool added = contexts.add(new_mailbox_context); + bool added = contexts.add(new_context); assert(added); - return new Mailbox(new_mailbox_context); + return new Mailbox(new_context); } private void on_selected_context_freed(Geary.ReferenceSemantics semantics) { @@ -90,22 +87,18 @@ public class Geary.Imap.ClientSessionManager : Object, Geary.Account { } private void on_context_freed(Geary.ReferenceSemantics semantics, - Gee.HashSet contexts) { - MailboxContext mailbox_context = (MailboxContext) semantics; + Gee.HashSet contexts) { + SelectedContext context = (SelectedContext) semantics; - debug("Mailbox %s freed, closing select/examine", mailbox_context.name); + debug("Mailbox %s freed, closing select/examine", context.name); // last reference to the Mailbox has been dropped, so drop the mailbox and move the // ClientSession back to the authorized state - bool removed = contexts.remove(mailbox_context); + bool removed = contexts.remove(context); assert(removed); - if (mailbox_context.session != null) - mailbox_context.session.close_mailbox_async.begin(); - } - - public async Geary.Folder open(string folder, Cancellable? cancellable = null) throws Error { - return yield examine_mailbox(folder, cancellable); + if (context.session != null) + context.session.close_mailbox_async.begin(); } private async ClientSession get_authorized_session(Cancellable? cancellable = null) throws Error { @@ -115,13 +108,13 @@ public class Geary.Imap.ClientSessionManager : Object, Geary.Account { return session; } - debug("Creating new session to %s", server); + debug("Creating new session to %s", cred.server); - ClientSession new_session = new ClientSession(server, default_port); + ClientSession new_session = new ClientSession(cred.server, default_port); new_session.disconnected.connect(on_disconnected); yield new_session.connect_async(cancellable); - yield new_session.login_async(user, pass, cancellable); + yield new_session.login_async(cred.user, cred.pass, cancellable); // do this after logging in new_session.enable_keepalives(keepalive_sec); diff --git a/src/engine/imap/Email.vala b/src/engine/imap/Email.vala index f48bcc2c..2449366c 100644 --- a/src/engine/imap/Email.vala +++ b/src/engine/imap/Email.vala @@ -10,8 +10,8 @@ public class Geary.Imap.EmailHeader : Geary.EmailHeader { } } -public class Geary.Imap.EmailBody : Geary.EmailBody { - public EmailBody(EmailHeader header, string full) { +public class Geary.Imap.Email : Geary.Email { + public Email(EmailHeader header, string full) { base (header, full); } } diff --git a/src/engine/imap/Flag.vala b/src/engine/imap/Flag.vala index fc002091..49480658 100644 --- a/src/engine/imap/Flag.vala +++ b/src/engine/imap/Flag.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public abstract class Geary.Imap.Flag { +public abstract class Geary.Imap.Flag : Comparable, Hashable { public string value { get; private set; } public Flag(string value) { @@ -19,21 +19,21 @@ public abstract class Geary.Imap.Flag { return this.value.down() == value.down(); } - public bool equals(Flag flag) { + public bool equals(Comparable b) { + Flag? flag = b as Flag; + if (flag == null) + return false; + return (flag == this) ? true : flag.equals_string(value); } + public uint get_hash() { + return str_hash(value.down()); + } + public string to_string() { return value; } - - public static uint hash_func(void *flag) { - return str_hash(((Flag *) flag)->value); - } - - public static bool equal_func(void *a, void *b) { - return ((Flag *) a)->equals((Flag *) b); - } } public class Geary.Imap.MessageFlag : Geary.Imap.Flag { diff --git a/src/engine/imap/Mailbox.vala b/src/engine/imap/Mailbox.vala index f37089d9..a3f04c7a 100644 --- a/src/engine/imap/Mailbox.vala +++ b/src/engine/imap/Mailbox.vala @@ -4,33 +4,37 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.Mailbox : Geary.SmartReference, Geary.Folder { +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; } - private MailboxContext mailbox; + private SelectedContext context; - internal Mailbox(MailboxContext mailbox) { - base (mailbox); + public signal void closed(); + + public signal void disconnected(bool local); + + internal Mailbox(SelectedContext context) { + base (context); - this.mailbox = mailbox; - mailbox.exists_changed.connect(on_exists_changed); - mailbox.closed.connect(on_closed); - mailbox.disconnected.connect(on_disconnected); + this.context = context; + context.exists_changed.connect(on_exists_changed); + context.closed.connect(on_closed); + context.disconnected.connect(on_disconnected); - name = mailbox.name; - count = mailbox.exists; - is_readonly = mailbox.is_readonly; + name = context.name; + count = context.exists; + is_readonly = context.is_readonly; } public async Gee.List? read(int low, int count, Cancellable? cancellable = null) throws Error { - if (mailbox.is_closed()) - throw new ImapError.NOT_SELECTED("Mailbox %s closed", mailbox.to_string()); + if (context.is_closed()) + throw new ImapError.NOT_SELECTED("Mailbox %s closed", name); - CommandResponse resp = yield mailbox.session.send_command_async( - new FetchCommand(mailbox.session.generate_tag(), new MessageSet.range(low, count), + CommandResponse resp = yield context.session.send_command_async( + new FetchCommand(context.session.generate_tag(), new MessageSet.range(low, count), { FetchDataType.ENVELOPE }), cancellable); if (resp.status_response.status != Status.OK) @@ -47,16 +51,16 @@ public class Geary.Imap.Mailbox : Geary.SmartReference, Geary.Folder { return msgs; } - public async Geary.EmailBody fetch_body(Geary.EmailHeader hdr, Cancellable? cancellable = null) + public async Geary.Email fetch(Geary.EmailHeader hdr, Cancellable? cancellable = null) throws Error { Geary.Imap.EmailHeader? header = hdr as Geary.Imap.EmailHeader; assert(header != null); - if (mailbox.is_closed()) - throw new ImapError.NOT_SELECTED("Folder closed"); + if (context.is_closed()) + throw new ImapError.NOT_SELECTED("Mailbox %s closed", name); - CommandResponse resp = yield mailbox.session.send_command_async( - new FetchCommand(mailbox.session.generate_tag(), new MessageSet(hdr.msg_num), + CommandResponse resp = yield context.session.send_command_async( + new FetchCommand(context.session.generate_tag(), new MessageSet(hdr.msg_num), { FetchDataType.RFC822_TEXT }), cancellable); if (resp.status_response.status != Status.OK) @@ -68,7 +72,7 @@ public class Geary.Imap.Mailbox : Geary.SmartReference, Geary.Folder { Geary.RFC822.Text text = (Geary.RFC822.Text) results[0].get_data(FetchDataType.RFC822_TEXT); - return new EmailBody(header, text.buffer.to_ascii_string()); + return new Email(header, text.buffer.to_ascii_string()); } private void on_exists_changed(int exists) { @@ -76,15 +80,15 @@ public class Geary.Imap.Mailbox : Geary.SmartReference, Geary.Folder { } private void on_closed() { - closed(CloseReason.FOLDER_CLOSED); + closed(); } private void on_disconnected(bool local) { - closed(local ? CloseReason.LOCAL_CLOSE : CloseReason.REMOTE_CLOSE); + disconnected(local); } } -internal class Geary.Imap.MailboxContext : Object, Geary.ReferenceSemantics { +internal class Geary.Imap.SelectedContext : Object, Geary.ReferenceSemantics { public ClientSession? session { get; private set; } protected int manual_ref_count { get; protected set; } @@ -102,7 +106,7 @@ internal class Geary.Imap.MailboxContext : Object, Geary.ReferenceSemantics { public signal void disconnected(bool local); - internal MailboxContext(ClientSession session, SelectExamineResults results) { + internal SelectedContext(ClientSession session, SelectExamineResults results) { this.session = session; name = session.get_current_mailbox(); @@ -117,7 +121,7 @@ internal class Geary.Imap.MailboxContext : Object, Geary.ReferenceSemantics { session.disconnected.connect(on_session_disconnected); } - ~MailboxSession() { + ~SelectedContext() { if (session != null) { session.current_mailbox_changed.disconnect(on_session_mailbox_changed); session.unsolicited_exists.disconnect(on_unsolicited_exists); @@ -169,9 +173,5 @@ internal class Geary.Imap.MailboxContext : Object, Geary.ReferenceSemantics { assert_not_reached(); } } - - public string to_string() { - return "Mailbox %s".printf(name); - } } diff --git a/src/engine/imap/MessageData.vala b/src/engine/imap/MessageData.vala index d51e23fb..1fe54011 100644 --- a/src/engine/imap/MessageData.vala +++ b/src/engine/imap/MessageData.vala @@ -36,7 +36,7 @@ public abstract class Geary.Imap.Flags : Geary.Common.MessageData, Geary.Imap.Me private Gee.Set list; public Flags(Gee.Collection flags) { - list = new Gee.HashSet(Flag.hash_func, Flag.equal_func); + list = new Gee.HashSet(Hashable.hash_func, Comparable.equal_func); list.add_all(flags); } diff --git a/src/engine/imap/api/Account.vala b/src/engine/imap/api/Account.vala new file mode 100644 index 00000000..9f99fb3e --- /dev/null +++ b/src/engine/imap/api/Account.vala @@ -0,0 +1,47 @@ +/* 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.Imap.Account : Object, Geary.Account, Geary.NetworkAccount { + private ClientSessionManager session_mgr; + + public Account(Credentials cred, uint default_port) { + session_mgr = new ClientSessionManager(cred, default_port); + } + + public bool is_online() { + return true; + } + + public async Gee.Collection list_async(string? parent_folder, + Cancellable? cancellable = null) throws Error { + Gee.Collection mboxes = yield session_mgr.list(parent_folder, cancellable); + + Gee.Collection folders = new Gee.ArrayList(); + foreach (MailboxInformation mbox in mboxes) + folders.add(new Geary.Imap.Folder(session_mgr, mbox)); + + return folders; + } + + public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { + // TODO + } + + public async void create_many_async(Gee.Collection folders, + Cancellable? cancellable = null) throws Error { + // TODO + } + + public async void remove_async(string folder, Cancellable? cancellable = null) throws Error { + // TODO + } + + public async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) + throws Error { + // TODO + } +} + diff --git a/src/engine/imap/api/Folder.vala b/src/engine/imap/api/Folder.vala new file mode 100644 index 00000000..d12e70d8 --- /dev/null +++ b/src/engine/imap/api/Folder.vala @@ -0,0 +1,74 @@ +/* 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.Imap.Folder : Object, Geary.Folder { + public string name { get; protected set; } + // This is only for when a context has been selected + public Trillian is_readonly { get; protected set; } + public Trillian supports_children { get; protected set; } + public Trillian has_children { get; protected set; } + public Trillian is_openable { get; protected set; } + + private ClientSessionManager session_mgr; + private MailboxInformation info; + private Mailbox? mailbox = null; + + internal Folder(ClientSessionManager session_mgr, MailboxInformation info) { + this.session_mgr = session_mgr; + this.info = info; + + name = info.name; + is_readonly = Trillian.UNKNOWN; + supports_children = Trillian.from_boolean(!info.attrs.contains(MailboxAttribute.NO_INFERIORS)); + // \HasNoChildren is an optional attribute and lack of presence doesn't indiciate anything + has_children = info.attrs.contains(MailboxAttribute.HAS_NO_CHILDREN) ? Trillian.TRUE + : Trillian.UNKNOWN; + is_openable = Trillian.from_boolean(!info.attrs.contains(MailboxAttribute.NO_SELECT)); + } + + public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + 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 + + this.is_readonly = Trillian.from_boolean(readonly); + } + + public async void close_async(Cancellable? cancellable = null) throws Error { + mailbox = null; + is_readonly = Trillian.UNKNOWN; + } + + public int get_message_count() throws Error { + if (mailbox == null) + throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); + + return mailbox.count; + } + + public async Gee.List? read_async(int low, int count, + Cancellable? cancellable = null) throws Error { + if (mailbox == null) + throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); + + return yield mailbox.read(low, count, cancellable); + } + + public async Geary.Email fetch_async(Geary.EmailHeader header, + Cancellable? cancellable = null) throws Error { + if (mailbox == null) + throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); + + return yield mailbox.fetch(header, cancellable); + } + + public string to_string() { + return name; + } +} + diff --git a/src/engine/imap/decoders/ListResults.vala b/src/engine/imap/decoders/ListResults.vala index 1bd7f19e..c2d20b7c 100644 --- a/src/engine/imap/decoders/ListResults.vala +++ b/src/engine/imap/decoders/ListResults.vala @@ -4,12 +4,12 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.FolderDetail : Object, Geary.FolderDetail { - public string name { get; protected set; } +public class Geary.Imap.MailboxInformation { + public string name { get; private set; } public string delim { get; private set; } public MailboxAttributes attrs { get; private set; } - public FolderDetail(string name, string delim, MailboxAttributes attrs) { + public MailboxInformation(string name, string delim, MailboxAttributes attrs) { this.name = name; this.delim = delim; this.attrs = attrs; @@ -17,19 +17,18 @@ public class Geary.Imap.FolderDetail : Object, Geary.FolderDetail { } public class Geary.Imap.ListResults : Geary.Imap.CommandResults { - private Gee.HashMap map = new Gee.HashMap(); + private Gee.Map map; - public ListResults(StatusResponse status_response, Gee.Collection details) { + public ListResults(StatusResponse status_response, Gee.Map map) { base (status_response); - foreach (FolderDetail detail in details) - map.set(detail.name, detail); + this.map = map; } public static ListResults decode(CommandResponse response) { assert(response.is_sealed()); - Gee.List details = new Gee.ArrayList(); + Gee.Map map = new Gee.HashMap(); foreach (ServerData data in response.server_data) { try { StringParameter cmd = data.get_as_string(1); @@ -57,24 +56,25 @@ public class Geary.Imap.ListResults : Geary.Imap.CommandResults { list.add(new MailboxAttribute(stringp.value)); } - details.add(new FolderDetail(mailbox.value, delim.value, new MailboxAttributes(list))); + map.set(mailbox.value, + new MailboxInformation(mailbox.value, delim.value, new MailboxAttributes(list))); } catch (ImapError ierr) { debug("Unable to decode \"%s\": %s", data.to_string(), ierr.message); } } - return new ListResults(response.status_response, details); + return new ListResults(response.status_response, map); } public Gee.Collection get_names() { return map.keys; } - public Gee.Collection get_all() { + public Gee.Collection get_all() { return map.values; } - public FolderDetail? get_detail(string name) { + public MailboxInformation? get_info(string name) { return map.get(name); } } diff --git a/src/engine/sqlite/Database.vala b/src/engine/sqlite/Database.vala new file mode 100644 index 00000000..859088df --- /dev/null +++ b/src/engine/sqlite/Database.vala @@ -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 abstract class Geary.Sqlite.Database { + internal SQLHeavy.VersionedDatabase db; + + private Gee.HashMap table_map = new Gee.HashMap< + SQLHeavy.Table, Geary.Sqlite.Table>(); + + public Database(File db_file, File schema_dir) throws Error { + if (!db_file.get_parent().query_exists()) + db_file.get_parent().make_directory_with_parents(); + + db = new SQLHeavy.VersionedDatabase(db_file.get_path(), schema_dir.get_path()); + } + + protected Geary.Sqlite.Table? get_table(string name, out SQLHeavy.Table heavy_table) { + try { + heavy_table = db.get_table(name); + } catch (SQLHeavy.Error err) { + error("Unable to load %s: %s", name, err.message); + } + + return table_map.get(heavy_table); + } + + protected void add_table(Geary.Sqlite.Table table) { + table_map.set(table.table, table); + } +} + diff --git a/src/engine/sqlite/FolderRow.vala b/src/engine/sqlite/FolderRow.vala new file mode 100644 index 00000000..f5b8ee99 --- /dev/null +++ b/src/engine/sqlite/FolderRow.vala @@ -0,0 +1,33 @@ +/* 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.FolderRow : Geary.Sqlite.Row { + public int64 id { get; private set; } + public string name { get; private set; } + public Trillian supports_children { get; private set; } + public Trillian is_openable { get; private set; } + public int64 parent_id { get; private set; } + + public FolderRow(string name, Trillian supports_children, Trillian is_openable, + int64 parent_id = INVALID_ID) { + this.id = -1; + this.name = name; + this.supports_children = supports_children; + this.is_openable = is_openable; + this.parent_id = parent_id; + } + + public FolderRow.from_query_result(SQLHeavy.QueryResult result) throws Error { + id = fetch_int64_for(result, FolderTable.Column.ID.colname()); + name = fetch_string_for(result, FolderTable.Column.NAME.colname()); + supports_children = Trillian.from_int(fetch_int_for(result, + FolderTable.Column.SUPPORTS_CHILDREN.colname())); + is_openable = Trillian.from_int(fetch_int_for(result, + FolderTable.Column.IS_OPENABLE.colname())); + parent_id = fetch_int64_for(result, FolderTable.Column.PARENT_ID.colname()); + } +} + diff --git a/src/engine/sqlite/FolderTable.vala b/src/engine/sqlite/FolderTable.vala new file mode 100644 index 00000000..b8320d0e --- /dev/null +++ b/src/engine/sqlite/FolderTable.vala @@ -0,0 +1,97 @@ +/* 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.FolderTable : Geary.Sqlite.Table { + // This *must* match the column order in the database + public enum Column { + ID, + NAME, + SUPPORTS_CHILDREN, + IS_OPENABLE, + PARENT_ID; + + public string colname() { + switch (this) { + case ID: + return "id"; + + case NAME: + return "name"; + + case SUPPORTS_CHILDREN: + return "supports_children"; + + case IS_OPENABLE: + return "is_openable"; + + case PARENT_ID: + return "parent_id"; + + default: + assert_not_reached(); + } + } + } + + internal FolderTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { + base (gdb, table); + } + + public async Gee.List list_async(int64 parent_id, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Query query = db.prepare("SELECT * FROM FolderTable WHERE parent_id=?"); + query.bind_int64(0, parent_id); + + SQLHeavy.QueryResult result = yield query.execute_async(cancellable); + + Gee.List rows = new Gee.ArrayList(); + while (!result.finished) { + rows.add(new FolderRow.from_query_result(result)); + + yield result.next_async(cancellable); + } + + return rows; + } + + private SQLHeavy.Query create_query(SQLHeavy.Queryable? queryable = null) throws SQLHeavy.Error { + SQLHeavy.Queryable q = queryable ?? db; + SQLHeavy.Query query = q.prepare( + "INSERT INTO FolderTable (name, supports_children, is_openable, parent_id) VALUES (?, ?, ?, ?)"); + + return query; + } + + private void create_binding(SQLHeavy.Query query, FolderRow row) throws SQLHeavy.Error { + query.clear(); + query.bind_string(0, row.name); + query.bind_int(1, row.supports_children.to_int()); + query.bind_int(2, row.is_openable.to_int()); + query.bind_int64(3, row.parent_id); + } + + public async void 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 rows, Cancellable? cancellable = null) + throws Error { + SQLHeavy.Transaction transaction = db.begin_transaction(); + + SQLHeavy.Query query = create_query(transaction); + foreach (FolderRow row in rows) { + create_binding(query, row); + query.execute_insert(); + } + + // TODO: Need an async transaction commit + transaction.commit(); + } +} + diff --git a/src/engine/sqlite/MailDatabase.vala b/src/engine/sqlite/MailDatabase.vala new file mode 100644 index 00000000..9b06375c --- /dev/null +++ b/src/engine/sqlite/MailDatabase.vala @@ -0,0 +1,27 @@ +/* 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.MailDatabase : Geary.Sqlite.Database { + public const string FILENAME = "geary.db"; + + public MailDatabase(string user) throws Error { + base (YorbaApplication.instance.get_user_data_directory().get_child(user).get_child(FILENAME), + YorbaApplication.instance.get_resource_directory().get_child("sql")); + } + + public Geary.Sqlite.FolderTable get_folder_table() { + SQLHeavy.Table heavy_table; + FolderTable? folder_table = get_table("FolderTable", out heavy_table) as FolderTable; + if (folder_table != null) + return folder_table; + + folder_table = new FolderTable(this, heavy_table); + add_table(folder_table); + + return folder_table; + } +} + diff --git a/src/engine/sqlite/Row.vala b/src/engine/sqlite/Row.vala new file mode 100644 index 00000000..e2c92ec0 --- /dev/null +++ b/src/engine/sqlite/Row.vala @@ -0,0 +1,25 @@ +/* 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.Sqlite.Row { + public const int64 INVALID_ID = -1; + + public static int fetch_int_for(SQLHeavy.QueryResult result, string name) + throws SQLHeavy.Error { + return result.fetch_int(result.field_index(name)); + } + + public static int64 fetch_int64_for(SQLHeavy.QueryResult result, string name) + throws SQLHeavy.Error { + return result.fetch_int64(result.field_index(name)); + } + + public static string fetch_string_for(SQLHeavy.QueryResult result, string name) + throws SQLHeavy.Error { + return result.fetch_string(result.field_index(name)); + } +} + diff --git a/src/engine/sqlite/Table.vala b/src/engine/sqlite/Table.vala new file mode 100644 index 00000000..462f42e4 --- /dev/null +++ b/src/engine/sqlite/Table.vala @@ -0,0 +1,22 @@ +/* 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.Sqlite.Table { + internal SQLHeavy.Database db { + get { + return gdb.db; + } + } + + internal weak Geary.Sqlite.Database gdb; + internal SQLHeavy.Table table; + + internal Table(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { + this.gdb = gdb; + this.table = table; + } +} + diff --git a/src/engine/sqlite/api/Account.vala b/src/engine/sqlite/api/Account.vala new file mode 100644 index 00000000..d8dfdb95 --- /dev/null +++ b/src/engine/sqlite/api/Account.vala @@ -0,0 +1,56 @@ +/* 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.Account : Object, Geary.Account, Geary.LocalAccount { + private MailDatabase db; + private FolderTable folder_table; + + public Account(Geary.Credentials cred) { + try { + db = new MailDatabase(cred.user); + } catch (Error err) { + error("Unable to open database: %s", err.message); + } + + folder_table = db.get_folder_table(); + } + + public async Gee.Collection list_async(string? parent_folder, + Cancellable? cancellable = null) throws Error { + Gee.List rows = yield folder_table.list_async(Row.INVALID_ID, cancellable); + + Gee.Collection folders = new Gee.ArrayList(); + foreach (FolderRow row in rows) + folders.add(new Geary.Sqlite.Folder(row)); + + return folders; + } + + public async void create_async(Geary.Folder folder, Cancellable? cancellable = null) throws Error { + yield folder_table.create_async( + new FolderRow(folder.name, folder.supports_children, folder.is_openable), + cancellable); + } + + public async void create_many_async(Gee.Collection folders, + Cancellable? cancellable = null) throws Error { + Gee.List rows = new Gee.ArrayList(); + foreach (Geary.Folder folder in folders) + rows.add(new FolderRow(folder.name, folder.supports_children, folder.is_openable)); + + yield folder_table.create_many_async(rows, cancellable); + } + + public async void remove_async(string folder, Cancellable? cancellable = null) throws Error { + // TODO + } + + public async void remove_many_async(Gee.Set folders, Cancellable? cancellable = null) + throws Error { + // TODO + } +} + diff --git a/src/engine/sqlite/api/Folder.vala b/src/engine/sqlite/api/Folder.vala new file mode 100644 index 00000000..38bd4413 --- /dev/null +++ b/src/engine/sqlite/api/Folder.vala @@ -0,0 +1,48 @@ +/* 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.Folder : Object, Geary.Folder { + private FolderRow row; + + public string name { get; protected set; } + public Trillian is_readonly { get; protected set; } + public Trillian supports_children { get; protected set; } + public Trillian has_children { get; protected set; } + public Trillian is_openable { get; protected set; } + + internal Folder(FolderRow row) throws Error { + this.row = row; + + name = row.name; + is_readonly = Trillian.UNKNOWN; + supports_children = row.supports_children; + has_children = Trillian.UNKNOWN; + is_openable = row.is_openable; + } + + public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + is_readonly = Trillian.TRUE; + } + + public async void close_async(Cancellable? cancellable = null) throws Error { + is_readonly = Trillian.UNKNOWN; + } + + public int get_message_count() throws Error { + return 0; + } + + public async Gee.List? read_async(int low, int count, + Cancellable? cancellable = null) throws Error { + return null; + } + + public async Geary.Email fetch_async(Geary.EmailHeader header, + Cancellable? cancellable = null) throws Error { + throw new EngineError.OPEN_REQUIRED("Not implemented"); + } +} + diff --git a/src/engine/util/Trillian.vala b/src/engine/util/Trillian.vala new file mode 100644 index 00000000..f96719b0 --- /dev/null +++ b/src/engine/util/Trillian.vala @@ -0,0 +1,69 @@ +/* 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. + */ + +/** + * A trillian is a three-state boolean, used when the value is potentially unknown. + */ + +public enum Geary.Trillian { + UNKNOWN = -1, + FALSE = 0, + TRUE = 1; + + public bool to_boolean(bool if_unknown) { + switch (this) { + case UNKNOWN: + return if_unknown; + + case FALSE: + return false; + + case TRUE: + return true; + + default: + assert_not_reached(); + } + } + + public inline static Trillian from_boolean(bool b) { + return b ? TRUE : FALSE; + } + + public int to_int() { + return (int) this; + } + + public inline static Trillian from_int(int i) { + switch (i) { + case 0: + return FALSE; + + case 1: + return TRUE; + + default: + return UNKNOWN; + } + } + + public string to_string() { + switch (this) { + case UNKNOWN: + return "unknown"; + + case FALSE: + return "false"; + + case TRUE: + return "true"; + + default: + assert_not_reached(); + } + } +} + diff --git a/src/tests/lsmbox.vala b/src/tests/lsmbox.vala deleted file mode 100644 index 96dd83cd..00000000 --- a/src/tests/lsmbox.vala +++ /dev/null @@ -1,57 +0,0 @@ -/* 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. - */ - -MainLoop? main_loop = null; -Geary.Imap.ClientSessionManager? sess = null; -string? mailbox = null; -int start = 0; -int count = 0; - -async void async_start() { - try { - Geary.Folder folder = yield sess.open(mailbox); - - bool ok = false; - Gee.List? msgs = yield folder.read(start, count); - if (msgs != null && msgs.size > 0) { - foreach (Geary.EmailHeader msg in msgs) - stdout.printf("%s\n", msg.to_string()); - - ok = true; - } - - if (!ok) - debug("Unable to examine mailbox %s", mailbox); - } catch (Error err) { - debug("Error: %s", err.message); - } - - main_loop.quit(); -} - -int main(string[] args) { - if (args.length < 6) { - stderr.printf("usage: lsmbox \n"); - - return 1; - } - - main_loop = new MainLoop(); - - string user = args[1]; - string pass = args[2]; - mailbox = args[3]; - start = int.parse(args[4]); - count = int.parse(args[5]); - - sess = new Geary.Imap.ClientSessionManager("imap.gmail.com", 993, user, pass); - async_start.begin(); - - main_loop.run(); - - return 0; -} - diff --git a/src/tests/readmail.vala b/src/tests/readmail.vala deleted file mode 100644 index 7c46af6e..00000000 --- a/src/tests/readmail.vala +++ /dev/null @@ -1,72 +0,0 @@ -/* 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. - */ - -MainLoop? main_loop = null; -Geary.Imap.ClientSession? sess = null; -string? user = null; -string? pass = null; -string? mailbox = null; -int msg_num = 0; - -async void async_start() { - try { - yield sess.connect_async(); - yield sess.login_async(user, pass); - yield sess.examine_async(mailbox); - - Geary.Imap.FetchCommand fetch = new Geary.Imap.FetchCommand(sess.generate_tag(), - new Geary.Imap.MessageSet(msg_num), { Geary.Imap.FetchDataType.RFC822 }); - Geary.Imap.CommandResponse resp = yield sess.send_command_async(fetch); - Geary.Imap.FetchResults[] results = Geary.Imap.FetchResults.decode(resp); - - assert(results.length == 1); - Geary.RFC822.Full? full = - results[0].get_data(Geary.Imap.FetchDataType.RFC822) as Geary.RFC822.Full; - assert(full != null); - - DataInputStream dins = new DataInputStream(full.buffer.get_input_stream()); - dins.set_newline_type(DataStreamNewlineType.CR_LF); - for (;;) { - string? line = dins.read_line(null); - if (line == null) - break; - - stdout.printf("%s\n", line); - } - - yield sess.close_mailbox_async(); - - yield sess.logout_async(); - yield sess.disconnect_async(); - } catch (Error err) { - debug("Error: %s", err.message); - } - - main_loop.quit(); -} - -int main(string[] args) { - if (args.length < 5) { - stderr.printf("usage: readmail \n"); - - return 1; - } - - main_loop = new MainLoop(); - - user = args[1]; - pass = args[2]; - mailbox = args[3]; - msg_num = int.parse(args[4]); - - sess = new Geary.Imap.ClientSession("imap.gmail.com", 993); - async_start.begin(); - - main_loop.run(); - - return 0; -} - diff --git a/src/tests/syntax.vala b/src/tests/syntax.vala deleted file mode 100644 index abda8775..00000000 --- a/src/tests/syntax.vala +++ /dev/null @@ -1,65 +0,0 @@ -/* 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. - */ - -MainLoop? main_loop = null; - -void print(int depth, Gee.List params) { - string pad = string.nfill(depth * 4, ' '); - - int index = 0; - foreach (Geary.Imap.Parameter param in params) { - Geary.Imap.ListParameter? list = param as Geary.Imap.ListParameter; - if (list == null) { - stdout.printf("%s#%02d >%s<\n", pad, index++, param.to_string()); - - continue; - } - - print(depth + 1, list.get_all()); - } -} - -void on_params_ready(Geary.Imap.RootParameters root) { - print(0, root.get_all()); -} - -void on_eos() { - main_loop.quit(); -} - -int main(string[] args) { - if (args.length < 2) { - stderr.printf("usage: syntax \n"); - - return 1; - } - - main_loop = new MainLoop(); - - // turn argument into single line for deserializer - string line = ""; - for (int ctr = 1; ctr < args.length; ctr++) { - line += args[ctr]; - if (ctr < (args.length - 1)) - line += " "; - } - line += "\r\n"; - - MemoryInputStream mins = new MemoryInputStream(); - mins.add_data(line.data, null); - - Geary.Imap.Deserializer des = new Geary.Imap.Deserializer(mins); - des.parameters_ready.connect(on_params_ready); - des.eos.connect(on_eos); - - stdout.printf("INPUT: >%s<\n", line); - des.xon(); - - main_loop.run(); - - return 0; -} -