From 83e2d766da2319e3d52845a513e1f4d14a3d8dc1 Mon Sep 17 00:00:00 2001 From: Eric Gregory Date: Thu, 22 Mar 2012 18:43:52 -0700 Subject: [PATCH] Initial commit of Geary DBus server --- .gitignore | 1 + src/dbusservice/controller.vala | 79 ++++++++++++++++ src/dbusservice/database.vala | 110 ++++++++++++++++++++++ src/dbusservice/dbus-conversation.vala | 41 +++++++++ src/dbusservice/dbus-conversations.vala | 117 ++++++++++++++++++++++++ src/dbusservice/dbus-email.vala | 72 +++++++++++++++ src/dbusservice/geary.xml | 76 +++++++++++++++ src/dbusservice/main.vala | 23 +++++ src/dbusservice/wscript_build | 26 ++++++ src/wscript | 1 + wscript | 10 ++ 11 files changed, 556 insertions(+) create mode 100644 src/dbusservice/controller.vala create mode 100644 src/dbusservice/database.vala create mode 100644 src/dbusservice/dbus-conversation.vala create mode 100644 src/dbusservice/dbus-conversations.vala create mode 100644 src/dbusservice/dbus-email.vala create mode 100644 src/dbusservice/geary.xml create mode 100644 src/dbusservice/main.vala create mode 100644 src/dbusservice/wscript_build diff --git a/.gitignore b/.gitignore index 33fc4a55..9ef0650b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ /geary /norman /theseus +/gearyd diff --git a/src/dbusservice/controller.vala b/src/dbusservice/controller.vala new file mode 100644 index 00000000..7877a9eb --- /dev/null +++ b/src/dbusservice/controller.vala @@ -0,0 +1,79 @@ +/* Copyright 2011-2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class Geary.DBus.Controller { + public static const string CONVERSATION_PATH_PREFIX = "/org/yorba/geary/conversation/"; + public static const string EMAIL_PATH_PREFIX = "/org/yorba/geary/email/"; + + public static Geary.DBus.Controller instance { get; private set; } + + public DBusConnection connection { get; private set; } + + private string[] args; + private Geary.EngineAccount account; + private Geary.DBus.Conversations conversations; + + public static void init(string[] args) { + instance = new Geary.DBus.Controller(args); + Database.init(); + } + + protected Controller(string[] args) { + this.args = args; + } + + public async void start() { + try { + Geary.Engine.init(get_user_data_directory(), get_resource_directory()); + + connection = yield Bus.get(GLib.BusType.SESSION); + + // Open the account. + account = Geary.Engine.open(new Geary.Credentials(args[1], args[2])); + account.report_problem.connect(on_report_problem); + + // Open the Inbox folder. + Geary.SpecialFolderMap? special_folders = account.get_special_folder_map(); + Geary.Folder folder = yield account.fetch_folder_async(special_folders.get_folder( + Geary.SpecialFolderType.INBOX).path); + + yield folder.open_async(false, null); + + conversations = new Geary.DBus.Conversations(folder); + + // Register interfaces. + Bus.own_name(BusType.SESSION, Geary.DBus.Conversations.INTERFACE_NAME, BusNameOwnerFlags.NONE, + on_conversations_aquired); + Bus.own_name(BusType.SESSION, Geary.DBus.Conversation.INTERFACE_NAME, BusNameOwnerFlags.NONE); + Bus.own_name(BusType.SESSION, Geary.DBus.Email.INTERFACE_NAME, BusNameOwnerFlags.NONE); + } catch (Error e) { + debug("Startup error: %s", e.message); + return; + } + } + + public File get_user_data_directory() { + return File.new_for_path(Environment.get_user_data_dir()).get_child("geary"); + } + + public File get_resource_directory() { + return File.new_for_path(Environment.get_current_dir()); + } + + private void on_report_problem(Geary.Account.Problem problem, Geary.Credentials? credentials, + Error? err) { + debug("Reported problem: %s Error: %s", problem.to_string(), err != null ? err.message : "(N/A)"); + } + + private void on_conversations_aquired(DBusConnection c) { + try { + connection.register_object("/org/yorba/geary/conversations", conversations); + } catch (IOError e) { + debug("Error: %s", e.message); + } + } +} + diff --git a/src/dbusservice/database.vala b/src/dbusservice/database.vala new file mode 100644 index 00000000..eab5bf67 --- /dev/null +++ b/src/dbusservice/database.vala @@ -0,0 +1,110 @@ +/* Copyright 2011-2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Geary.DBus { + +public uint db_email_hash(Geary.Email key) { + return key.id.to_hash(); +} + +public bool db_email_equal(Geary.Email a, Geary.Email b) { + return a.id.equals(b.id); +} + +} + +public class Geary.DBus.Database : Object { + + public static Database instance { get; private set; } + + private const string DBUS_PATH_PROP = "dbus-path"; + private const string DBUS_REG_PROP = "dbus-registration"; + + private Gee.HashMap path_to_object = + new Gee.HashMap(); + + // This is for tracking conversations. + // TODO: find a better way to give conversations unique IDs. + private int counter = 0; + + private Database() { + } + + // Must call this to init the database. + public static void init() { + instance = new Geary.DBus.Database(); + } + + // Finds the conversation and returns the path. If the conversation does not have + // a path, it will be assigned one. + public ObjectPath get_conversation_path(Geary.Conversation c, Geary.Folder folder) { + ObjectPath? path = c.get_data(DBUS_PATH_PROP); + + if (path == null) { + // Assign new path based on counter. + path = new ObjectPath(Controller.CONVERSATION_PATH_PREFIX + + (counter++).to_string()); + + register_object(c, new Geary.DBus.Conversation(c, folder), path); + } + + return path; + } + + // Gets a path for an email object. If one does not exist, it will be created. + public ObjectPath get_email_path(Geary.Email e, Geary.Folder folder) { + ObjectPath? path = e.get_data(DBUS_PATH_PROP); + if (path == null) { + // Generate a path and save it. + path = new ObjectPath(Controller.EMAIL_PATH_PREFIX + e.id.to_string()); + register_object(e, new Geary.DBus.Email(folder, e), path); + } + + return path; + } + + // Registers an object with DBus and saves the path. + // Uses generics to bypass Vala's dbus codegen warning. + private void register_object(Object object, T dbus_object, ObjectPath path) { + try { + uint reg_id = Controller.instance.connection.register_object(path, dbus_object); + object.set_data(DBUS_PATH_PROP, path); + object.set_data(DBUS_REG_PROP, reg_id.to_string()); + path_to_object.set(path, object); + } catch (Error e) { + warning("Could not register path: %s", path); + debug("Error: %s", e.message); + } + } + + public void remove_by_path(ObjectPath path) { + debug("Removing path: %s", path); + Object? o = path_to_object.get(path); + if (o == null) { + warning("Unable to remove object: %s", path); + return; + } + + remove(o); + } + + // Removes an object + private void remove(Object object) { + debug("removing object"); + assert(object != null); + ObjectPath? path = object.get_data(DBUS_PATH_PROP); + string? reg_id_s = object.get_data(DBUS_REG_PROP); + if (path == null || reg_id_s == null) { + warning("Unable to remove object"); + return; + } + + uint reg_id = (uint) long.parse(reg_id_s); + path_to_object.unset(path); + Controller.instance.connection.unregister_object(reg_id); + } +} + diff --git a/src/dbusservice/dbus-conversation.vala b/src/dbusservice/dbus-conversation.vala new file mode 100644 index 00000000..a79b3a7b --- /dev/null +++ b/src/dbusservice/dbus-conversation.vala @@ -0,0 +1,41 @@ +/* Copyright 2011-2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +[DBus (name = "org.yorba.Geary.Conversation", timeout = 120000)] +public class Geary.DBus.Conversation : Object { + + public static const string INTERFACE_NAME = "org.yorba.Geary.Conversation"; + + private Geary.Conversation conversation; + private Geary.Folder folder; + + public Conversation(Geary.Conversation c, Geary.Folder f) { + conversation = c; + folder = f; + } + + public async ObjectPath[] get_emails() throws IOError { + Gee.SortedSet? pool = conversation.get_pool_sorted(compare_email); + if (pool == null) + return new ObjectPath[0]; + + ObjectPath[] paths = new ObjectPath[0]; + + foreach (Geary.Email e in pool) { + paths += new ObjectPath(Database.instance.get_email_path(e, folder)); + } + + return paths; + } + + private static int compare_email(Geary.Email aenvelope, Geary.Email benvelope) { + int diff = aenvelope.date.value.compare(benvelope.date.value); + + // stabilize sort by using the mail's ordering, which is always unique in a folder + return (diff != 0) ? diff : aenvelope.id.compare(benvelope.id); + } + +} diff --git a/src/dbusservice/dbus-conversations.vala b/src/dbusservice/dbus-conversations.vala new file mode 100644 index 00000000..15efa7e2 --- /dev/null +++ b/src/dbusservice/dbus-conversations.vala @@ -0,0 +1,117 @@ +/* Copyright 2011-2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +[DBus (name = "org.yorba.Geary.Conversations", timeout = 120000)] +public class Geary.DBus.Conversations : Object { + public static const string INTERFACE_NAME = "org.yorba.Geary.Conversations"; + + public signal void scan_started(); + + public signal void scan_error(); + + public signal void scan_completed(); + + public signal void conversations_added(ObjectPath[] conversations); + + public signal void conversation_removed(ObjectPath conversation); + + public signal void conversation_appended(ObjectPath conversation, ObjectPath[] emails); + + public signal void conversation_trimmed(ObjectPath conversation, ObjectPath email); + + private Geary.Folder folder; + private Geary.Conversations conversations; + + public Conversations(Geary.Folder folder) { + this.folder = folder; + conversations = new Geary.Conversations(folder, Geary.Email.Field.ENVELOPE | + Geary.Email.Field.PROPERTIES); + + conversations.monitor_new_messages(); + + conversations.scan_started.connect(on_scan_started); + conversations.scan_error.connect(on_scan_error); + conversations.scan_completed.connect(on_scan_completed); + conversations.conversations_added.connect(on_conversations_added); + conversations.conversation_appended.connect(on_conversation_appended); + conversations.conversation_trimmed.connect(on_conversation_trimmed); + conversations.conversation_removed.connect(on_conversation_removed); + conversations.updated_placeholders.connect(on_updated_placeholders); + + folder.email_flags_changed.connect(on_email_flags_changed); + } + + public void fetch_messages(int num_messages) throws IOError { + conversations.lazy_load(-1, num_messages, Geary.Folder.ListFlags.NONE, null); + } + + private void on_scan_started(Geary.EmailIdentifier? id, int low, int count) { + debug("scan started"); + scan_started(); + } + + private void on_scan_error(Error err) { + debug("scan error"); + scan_error(); + } + + private void on_scan_completed() { + debug("scan completed"); + scan_completed(); + } + + private void on_conversations_added(Gee.Collection conversations) { + debug("Conversation added: %d conversations", conversations.size); + ObjectPath[] paths = new ObjectPath[0]; + + foreach (Geary.Conversation c in conversations) { + paths += new ObjectPath(Database.instance.get_conversation_path(c, folder)); + } + + conversations_added(paths); + } + + private void on_conversation_removed(Geary.Conversation conversation) { + debug("conversation removed"); + // Fire signal, then delete. + ObjectPath path = Database.instance.get_conversation_path(conversation, folder); + + conversation_removed(path); + Database.instance.remove_by_path(path); + } + + private void on_conversation_appended(Geary.Conversation conversation, + Gee.Collection email_list) { + debug("conversation appended"); + + ObjectPath[] email_paths = new ObjectPath[0]; + foreach (Geary.Email e in email_list) + email_paths += Database.instance.get_email_path(e, folder); + + conversation_appended(Database.instance.get_conversation_path(conversation, folder), + email_paths); + } + + private void on_conversation_trimmed(Geary.Conversation conversation, Geary.Email email) { + debug("conversation trimmed"); + // Fire signal, then delete. + ObjectPath email_path = Database.instance.get_email_path(email, folder); + conversation_trimmed(Database.instance.get_conversation_path(conversation, folder), + email_path); + Database.instance.remove_by_path(email_path); + } + + private void on_updated_placeholders(Geary.Conversation conversation, + Gee.Collection email) { + // TODO + } + + private void on_email_flags_changed(Gee.Map flag_map) { + //TODO + } +} + diff --git a/src/dbusservice/dbus-email.vala b/src/dbusservice/dbus-email.vala new file mode 100644 index 00000000..316c4fa0 --- /dev/null +++ b/src/dbusservice/dbus-email.vala @@ -0,0 +1,72 @@ +/* Copyright 2011-2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +[DBus (name = "org.yorba.Geary.Email", timeout = 120000)] +public class Geary.DBus.Email : Object { + public static const string INTERFACE_NAME = "org.yorba.Geary.Email"; + + public string to { get; private set; } + public string from { get; private set; } + public string cc { get; private set; } + public string subject { get; private set; } + public int64 date { get; private set; } + public bool read { get; private set; } + + private Geary.Folder folder; + private Geary.Email email; + + public Email(Geary.Folder f, Geary.Email e) { + folder = f; + email = e; + + to = email.to != null ? email.to.to_string() : ""; + from = email.from != null ? email.from.to_string() : ""; + cc = email.cc != null ? email.cc.to_string() : ""; + subject = email.subject != null ? email.subject.to_string() : ""; + date = email.date != null ? email.date.as_time_t : 0; + read = email.properties != null ? email.properties.email_flags.contains(EmailFlags.UNREAD) + : true; + } + + public async string get_body() throws IOError { + string body_text = ""; + Geary.Email full_email; + + try { + full_email = yield folder.fetch_email_async(email.id, Geary.Email.Field.ALL, + Geary.Folder.ListFlags.NONE); + } catch (Error err) { + warning("Could not load email: %s", err.message); + return ""; + } + + try { + body_text = full_email.get_message().get_first_mime_part_of_content_type("text/html"). + to_utf8(); + } catch (Error err) { + try { + body_text = full_email.get_message().get_first_mime_part_of_content_type("text/plain"). + to_utf8(); + } catch (Error err2) { + debug("Could not get message text. %s", err2.message); + } + } + + return body_text; + } + + public async void remove() throws IOError { + try { + yield folder.remove_single_email_async(email.id); + } catch (Error e) { + if (e is IOError) + throw (IOError) e; + else + warning("Unexpected error removing email: %s", e.message); + } + } +} + diff --git a/src/dbusservice/geary.xml b/src/dbusservice/geary.xml new file mode 100644 index 00000000..a9c284c1 --- /dev/null +++ b/src/dbusservice/geary.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbusservice/main.vala b/src/dbusservice/main.vala new file mode 100644 index 00000000..dad9230f --- /dev/null +++ b/src/dbusservice/main.vala @@ -0,0 +1,23 @@ +/* Copyright 2011-2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +int main(string[] args) { + if (args.length != 3) { + stderr.printf("Geary D-Bus daemon\n"); + stderr.printf("Usage: gearyd \n"); + + return 1; + } + + debug("Starting Gearyd..."); + Geary.DBus.Controller.init(args); + Geary.DBus.Controller.instance.start.begin(); + + debug("Entering main loop"); + new MainLoop().run(); + + return 0; +} diff --git a/src/dbusservice/wscript_build b/src/dbusservice/wscript_build new file mode 100644 index 00000000..b66ed0fb --- /dev/null +++ b/src/dbusservice/wscript_build @@ -0,0 +1,26 @@ +#! /usr/bin/env python +# encoding: utf-8 +# +# Copyright 2011-2012 Yorba Foundation + +dbusservice_src = [ +'controller.vala', +'database.vala', +'dbus-conversation.vala', +'dbus-conversations.vala', +'dbus-email.vala', +'main.vala' +] + +dbusservice_uselib = 'GEE GLIB' +dbusservice_packages = [ 'gee-1.0', 'glib-2.0' ] + +bld.program( + target = 'gearyd', + vapi_dirs = '../../vapi', + threading = True, + uselib = dbusservice_uselib + ' ' + bld.engine_uselib + ' ' + bld.common_uselib, + packages = dbusservice_packages + bld.engine_packages + bld.common_packages, + source = dbusservice_src + bld.common_src + bld.engine_src +) + diff --git a/src/wscript b/src/wscript index f15902c0..ef601749 100644 --- a/src/wscript +++ b/src/wscript @@ -169,4 +169,5 @@ def build(bld): bld.recurse('console') bld.recurse('norman') bld.recurse('theseus') + bld.recurse('dbusservice') diff --git a/wscript b/wscript index 7412cf46..f511ea36 100644 --- a/wscript +++ b/wscript @@ -121,6 +121,12 @@ def build(bld): if os.path.isfile('norman') : os.remove('norman') + + if os.path.isfile('theseus') : + os.remove('theseus') + + if os.path.isfile('gearyd') : + os.remove('gearyd') def post_build(bld): # Copy executables to root folder. @@ -128,6 +134,7 @@ def post_build(bld): console_path = 'build/src/console/console' norman_path = 'build/src/norman/norman' theseus_path = 'build/src/theseus/theseus' + gearyd_path = 'build/src/dbusservice/gearyd' if os.path.isfile(geary_path) : shutil.copy2(geary_path, 'geary') @@ -141,6 +148,9 @@ def post_build(bld): if os.path.isfile(theseus_path) : shutil.copy2(theseus_path, 'theseus') + if os.path.isfile(gearyd_path) : + shutil.copy2(gearyd_path, 'gearyd') + # Compile schemas for local (non-intall) build. client_build_path = 'build/src/client' shutil.copy2('src/client/org.yorba.geary.gschema.xml', client_build_path)