Initial commit of Geary DBus server

This commit is contained in:
Eric Gregory 2012-03-22 18:43:52 -07:00
parent 3bff675c3f
commit 83e2d766da
11 changed files with 556 additions and 0 deletions

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ build/
/geary
/norman
/theseus
/gearyd

View file

@ -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);
}
}
}

View file

@ -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<ObjectPath, Object> path_to_object =
new Gee.HashMap<ObjectPath, Object>();
// 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<T>(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);
}
}

View file

@ -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<Geary.Email>? 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);
}
}

View file

@ -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<Geary.Conversation> 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<Geary.Email> 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<Geary.Email> email) {
// TODO
}
private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier,
Geary.EmailFlags> flag_map) {
//TODO
}
}

View file

@ -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);
}
}
}

76
src/dbusservice/geary.xml Normal file
View file

@ -0,0 +1,76 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<!-- The main API. Eventually we'll need an Engine interface and a Folder
interface, but for now we're only concerned with fetching conversations
from whatever account and inbox the D-Bus server is providing. -->
<interface name="org.yorba.Geary.Conversations">
<signal name="ScanStarted">
</signal>
<signal name="ScanError">
</signal>
<signal name="ScanCompleted">
</signal>
<signal name="ConversationsAdded">
<!-- Type: array of Conversation objects -->
<arg name="conversations" type="ao" direction="out"/>
</signal>
<signal name="ConversationRemoved">
<!-- Type: a Conversation object -->
<arg name="conversation" type="o" direction="out"/>
</signal>
<signal name="ConversationAppended">
<!-- Type: a Conversation object -->
<arg name="conversation" type="o" direction="out"/>
<!-- Type: array of email objects -->
<arg name="emails" type="ao" direction="out"/>
</signal>
<signal name="ConversationTrimmed">
<!-- Type: a Conversation object -->
<arg name="conversation" type="o" direction="out"/>
<!-- Type: an email object -->
<arg name="email" type="o" direction="out"/>
</signal>
<!-- Begins the scan, fetching the number of messages provided.
-->
<method name="FetchMessages">
<arg name="num_messages" type="i" direction="in"/>
</method>
</interface>
<!-- Represents a single conversation, used for managing groups of emails.
-->
<interface name="org.yorba.Geary.Conversation">
<method name="GetEmails">
<annotation name="org.freedesktop.DBus.GLib.Async" value="true"/>
<!-- Type: array of emails -->
<arg type="ao" direction="out"/>
</method>
</interface>
<!-- This represents a single email. The properties may be squished into
a D-Bus struct if they turns out to be expensive. Additionally, we may
want to add functions for marking an email read, adding/removing a flag,
and perhaps other feature for the demo -->
<interface name="org.yorba.Geary.Email">
<property name="To" type="s" access="read"/>
<property name="From" type="s" access="read"/>
<property name="Cc" type="s" access="read"/>
<property name="Subject" type="s" access="read"/>
<property name="Date" type="x" access="read"/>
<property name="Read" type="b" access="read"/>
<method name="GetBody">
<annotation name="org.freedesktop.DBus.GLib.Async" value="true"/>
<arg type="s" direction="out"/>
</method>
<method name="Remove">
<annotation name="org.freedesktop.DBus.GLib.Async" value="true"/>
</method>
</interface>
</node>

23
src/dbusservice/main.vala Normal file
View file

@ -0,0 +1,23 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
int main(string[] args) {
if (args.length != 3) {
stderr.printf("Geary D-Bus daemon\n");
stderr.printf("Usage: gearyd <username> <password>\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;
}

View file

@ -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
)

View file

@ -169,4 +169,5 @@ def build(bld):
bld.recurse('console')
bld.recurse('norman')
bld.recurse('theseus')
bld.recurse('dbusservice')

10
wscript
View file

@ -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)