[Xfce4-commits] <postler:master> Initial Index and Message classes to store messages

Christian Dywan noreply at xfce.org
Fri May 27 20:32:01 CEST 2011


Updating branch refs/heads/master
         to 87496d12d29a2e7feed193412e7b382c0ac6b25b (commit)
       from 44fa0c77b8bd4d2ac5a017c7a3e9d64448fcb381 (commit)

commit 87496d12d29a2e7feed193412e7b382c0ac6b25b
Author: Christian Dywan <christian at twotoasts.de>
Date:   Mon May 23 23:25:40 2011 +0200

    Initial Index and Message classes to store messages
    
    Index is backed by sqlite, messages can be inserted and
    retrieved by URI.
    
    Messgage is the new abtraction for messages, coming from
    files or database or DBus.

 postler/postler-index.vala   |  151 ++++++++++++++++++++++++++++++++++++++++++
 postler/postler-message.vala |  129 +++++++++++++++++++++++++++++++++++
 postler/postler-service.vala |   83 ++++++++++++++++++++----
 postler/wscript_build        |    4 +-
 wscript                      |   14 ++++
 5 files changed, 367 insertions(+), 14 deletions(-)

diff --git a/postler/postler-index.vala b/postler/postler-index.vala
new file mode 100644
index 0000000..0e786bc
--- /dev/null
+++ b/postler/postler-index.vala
@@ -0,0 +1,151 @@
+/*
+ Copyright (C) 2011 Christian Dywan <christian at twotoasts.de>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ See the file COPYING for the full license text.
+*/
+
+namespace Postler {
+    public class Index : GLib.Object, GLib.Initable {
+        Sqlite.Database database;
+        Sqlite.Statement? statement_insert = null;
+        Sqlite.Statement? statement_get = null;
+        Sqlite.Statement? statement_list = null;
+        Sqlite.Statement? statement_unread = null;
+
+        bool init (GLib.Cancellable? cancellable = null) throws GLib.Error {
+            return true;
+        }
+
+        public Index () throws GLib.Error {
+            unowned string data_dir = Environment.get_user_data_dir ();
+            string data_path = data_dir + "/" + Config.PACKAGE_NAME + "/mail/";
+            if (Sqlite.Database.open (data_path + "index.db", out database) != Sqlite.OK)
+                throw new GLib.FileError.FAILED (_("Failed to open database: %s"), database.errmsg ());
+            if (database.exec ("PRAGMA journal_mode = TRUNCATE;") != Sqlite.OK)
+                throw new GLib.FileError.FAILED (_("Failed to open database: %s"), database.errmsg ());
+            if (database.exec ("PRAGMA synchronous = OFF;") != Sqlite.OK)
+                throw new GLib.FileError.FAILED (_("Failed to open database: %s"), database.errmsg ());
+            if (database.exec (
+                """
+                CREATE TABLE IF NOT EXISTS
+                messages (uri TEXT UNIQUE, subject TEXT, sender TEXT, recipients TEXT,
+                          unread BOOLEAN, flagged BOOLEAN, priority BOOLEAN,
+                          date INTEGER, excerpt TEXT, zeitgeist BOOLEAN);
+                """
+                ) != Sqlite.OK)
+                throw new GLib.FileError.FAILED (_("Failed to open database: %s"), database.errmsg ());
+        }
+
+        public void insert_received_message (Message message) throws GLib.Error {
+            if (statement_insert == null) {
+                /* TODO: REPLACE or IGNORE? */
+                if (database.prepare_v2 ("""
+                    INSERT OR REPLACE INTO messages
+                    (uri, subject, sender, recipients, unread, flagged, priority, date)
+                    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
+                    """,
+                    -1, out statement_insert) != Sqlite.OK)
+                    throw new GLib.FileError.FAILED (_("Failed to open database: %s"), database.errmsg ());
+            }
+            bool success =
+                statement_insert.bind_text (1, message.uri, -1) == Sqlite.OK
+             && statement_insert.bind_text (2, message.subject, -1) == Sqlite.OK
+             && statement_insert.bind_text (3, message.sender, -1) == Sqlite.OK
+             && statement_insert.bind_text (4, message.recipients, -1) == Sqlite.OK
+             && statement_insert.bind_int64 (5, message.unread ? 1 : 0) == Sqlite.OK
+             && statement_insert.bind_int64 (6, message.flagged ? 1 : 0) == Sqlite.OK
+             && statement_insert.bind_int64 (7, message.flagged ? 1 : 0) == Sqlite.OK
+             && statement_insert.bind_int64 (8, message.get_timestamp ()) == Sqlite.OK
+             && statement_insert.step () == Sqlite.DONE;
+            statement_insert.reset ();
+            if (!success)
+                throw new GLib.FileError.FAILED (_("Failed to index message: %s"), database.errmsg ());
+            /* TODO: Move file from new to cur */
+#if HAVE_ZEITGEIST
+            var log = new Zeitgeist.Log ();
+            var event = new Zeitgeist.Event.full (Zeitgeist.ZG_RECEIVE_EVENT,
+                Zeitgeist.ZG_SYSTEM_NOTIFICATION, "app://postler.desktop",
+                new Zeitgeist.Subject.full (message.uri,
+                Zeitgeist.NMO_EMAIL, Zeitgeist.NMO_MAILBOX_DATA_OBJECT,
+                "text/plain", GLib.File.new_for_uri (message.uri).get_parent ().get_uri (),
+                message.sender, ""));
+            log.insert_events_no_reply (event);
+            /*  TODO: use async call and set 'zeitgeist' boolean if succeeded */
+#endif
+        }
+
+        public int64 unread_messages (string folder) throws GLib.Error {
+            if (statement_unread == null) {
+                if (database.prepare_v2 ("""
+                    SELECT COUNT (*) FROM messages WHERE uri LIKE ?1 AND unread != 0
+                    """,
+                    -1, out statement_unread) != Sqlite.OK)
+                    throw new GLib.FileError.FAILED (_("Failed to count unread messages: %s"), database.errmsg ());
+            }
+            bool success =
+                statement_unread.bind_text (1, folder, -1) == Sqlite.OK
+             && statement_unread.step () == Sqlite.ROW;
+            if (!success) {
+                statement_unread.reset ();
+                throw new GLib.FileError.FAILED (_("Failed to count unread messages: %s"), database.errmsg ());
+            }
+            int64 count = statement_unread.column_int64 (0);
+            statement_unread.reset ();
+            return count;
+        }
+
+        public Message get_message (string uri) throws GLib.Error {
+            if (statement_get == null) {
+                if (database.prepare_v2 ("""
+                    SELECT subject, sender, recipients,
+                    unread, flagged, priority, date FROM messages WHERE uri LIKE ?1
+                    """,
+                    -1, out statement_get) != Sqlite.OK)
+                    throw new GLib.FileError.FAILED (_("Failed to list messages: %s"), database.errmsg ());
+            }
+            int result = Sqlite.ERROR;
+            bool success =
+                statement_get.bind_text (1, uri + "%", -1) == Sqlite.OK
+             && ((result = statement_get.step ())) == Sqlite.ROW;
+            if (!success) {
+                statement_get.reset ();
+                throw new GLib.FileError.FAILED (_("Failed to get message: %s"), database.errmsg ());
+            }
+            var message = new Message.from_statement (uri, statement_get);
+            statement_get.reset ();
+            return message;
+        }
+
+        public GLib.List<Message> get_messages (string folder) throws GLib.Error {
+            if (statement_list == null) {
+                if (database.prepare_v2 ("""
+                    SELECT uri FROM messages WHERE uri LIKE ?1 ORDER BY date DESC
+                    """,
+                    -1, out statement_list) != Sqlite.OK)
+                    throw new GLib.FileError.FAILED (_("Failed to list messages: %s"), database.errmsg ());
+            }
+            int result = Sqlite.ERROR;
+            bool success = statement_list.bind_text (1, folder, -1) == Sqlite.OK;
+            result = statement_list.step ();
+            success = success && (result == Sqlite.ROW || result == Sqlite.DONE);
+            if (!success) {
+                statement_list.reset ();
+                throw new GLib.FileError.FAILED (_("Failed to list messages: %s"), database.errmsg ());
+            }
+            var messages = new GLib.List<Message> ();
+            while (result == Sqlite.ROW) {
+                var message = new Message.from_uri (statement_list.column_text (0));
+                messages.append (message);
+                result = statement_list.step ();
+            }
+            statement_list.reset ();
+            return messages;
+        }
+    }
+}
+
diff --git a/postler/postler-message.vala b/postler/postler-message.vala
new file mode 100644
index 0000000..9c27a90
--- /dev/null
+++ b/postler/postler-message.vala
@@ -0,0 +1,129 @@
+/*
+ Copyright (C) 2011 Christian Dywan <christian at twotoasts.de>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ See the file COPYING for the full license text.
+*/
+
+namespace Postler {
+    public class Message : GLib.Object, GLib.Initable {
+        public string uri { public get; set; } /* TODO: uri without status flags */
+        public string get_path () { return GLib.File.new_for_uri (uri).get_path (); }
+        public string? id { public get; set; }
+        string? charset;
+        public GLib.DateTime? date { public get; set; }
+        public int64 get_timestamp () { return date != null ? date.to_unix () : 0; }
+        public string? subject { public get; set; }
+        public string? sender { public get; set; }
+        public string? recipients { public get; set; }
+        public bool unread { public get; set; } /* TODO: should be writable */
+        public bool flagged { public get; set; } /* TODO: should be writable */
+        public bool priority { public get; set; }
+
+        bool init (GLib.Cancellable? cancellable = null) throws GLib.Error {
+            return true;
+        }
+
+        static string parse_encoded (string quoted, out string charset) {
+            return Postler.Messages.parse_encoded (quoted, out charset);
+        }
+
+        public GLib.HashTable<string,Variant> to_hash_table () {
+            var message_data = new GLib.HashTable<string,Variant> (str_hash, str_equal);
+            message_data.insert ("uri", this.uri ?? "");
+            message_data.insert ("subject", this.subject ?? "");
+            message_data.insert ("sender", this.sender ?? "");
+            message_data.insert ("recipients", this.recipients ?? "");
+            message_data.insert ("unread", this.unread);
+            message_data.insert ("flagged", this.flagged);
+            message_data.insert ("priority", this.priority);
+            message_data.insert ("date", this.get_timestamp ());
+            return message_data;
+        }
+
+        public Message.from_uri (string uri) {
+            this.uri = uri;
+        }
+
+        public void from_hash_table (GLib.HashTable<string,Variant> message_data) {
+            this.subject = message_data.lookup ("subject").get_string ();
+            this.sender = message_data.lookup ("sender").get_string ();
+            this.recipients = message_data.lookup ("recipients").get_string ();
+            this.unread = message_data.lookup ("unread").get_boolean ();
+            this.flagged = message_data.lookup ("flagged").get_boolean ();
+            this.priority = message_data.lookup ("priority").get_boolean ();
+            this.date = new GLib.DateTime.from_unix_utc (message_data.lookup ("date").get_int64 ());
+        }
+
+        public Message.from_statement (string uri, Sqlite.Statement statement) {
+            this.uri = uri;
+            this.subject = statement.column_text (0);
+            this.sender = statement.column_text (1);
+            this.recipients = statement.column_text (2);
+            this.unread = statement.column_int64 (3) != 0;
+            this.flagged = statement.column_int64 (4) != 0;
+            this.priority = statement.column_int64 (5) != 0;
+            this.date = new GLib.DateTime.from_unix_utc (statement.column_int64 (6));
+        }
+
+        void read_flags (string path) {
+            /* format "unique:2,DFPRST", ordered alphabetically */
+            unowned string? last_colon = path.rstr (":2,");
+            if (last_colon != null) {
+                this.unread = last_colon.chr (-1, 'S') == null;
+                this.flagged = last_colon.chr (-1, 'F') != null;
+            }
+            else {
+                this.unread = true;
+            }
+        }
+
+        public Message.from_file (GLib.File file,
+            GLib.Cancellable? cancellable = null) throws GLib.Error {
+
+            uri = file.get_uri ();
+            read_flags (file.get_path ());
+
+            var stream = new DataInputStream (file.read (cancellable));
+            string line;
+            string previous_line = "";
+            while ((line = stream.read_line (null, cancellable)) != null) {
+                if (line == "")
+                    break;
+                if (line[0] == '\t' || line[0] == ' ')
+                    line = previous_line + " " + line.chug ();
+                previous_line = line;
+
+                string[] parts = line.split (":", 2);
+                if (parts == null || parts[0] == null)
+                    continue;
+
+                string field = ascii_strdown (parts[0]);
+                if (field == "message-id")
+                    id = parts[1].strip ();
+                else if (field == "subject")
+                    subject = parse_encoded (parts[1], out charset);
+                else if (field == "from") {
+                    string sender_charset = null;
+                    sender = parse_encoded (parts[1], out sender_charset);
+                }
+                else if (field == "date") {
+                    date = Postler.Content.date_from_string (parts[1]);}
+                else if (field == "to" || field == "cc" || field == "bcc")
+                    recipients = (recipients ?? "") + parts[1].strip () + ",";
+                else if (field == "x-priority" || field == "importance")
+                    priority = parts[1][0] == '1' || parts[1][0] == '2'
+                            || parts[1] == "High";
+            }
+
+            if (charset == null)
+                charset = "ISO-8859-1";
+            recipients = parse_encoded (recipients, out charset);
+        }
+    }
+}
+
diff --git a/postler/postler-service.vala b/postler/postler-service.vala
index c363369..685dac5 100644
--- a/postler/postler-service.vala
+++ b/postler/postler-service.vala
@@ -103,6 +103,7 @@ namespace Postler {
         double total = 0;
         int unread = 0;
         Dock.Item dockitem;
+        Index? index = null;
 
 #if HAVE_INDICATE
         Indicate.Server indicator;
@@ -112,9 +113,7 @@ namespace Postler {
             string path = item.get_property ("url") + "/INBOX/new";
             uint new_messages = 0;
             try {
-                var inbox = Dir.open (path, 0);
-                while (inbox.read_name () != null)
-                    new_messages++;
+                new_messages = (uint)unread_messages ("file://" + path);
                 var folder = File.new_for_path (path);
                 var monitor = folder.monitor_directory (0, null);
                 monitor.changed.connect ((monitor, file, other, event) => {
@@ -159,22 +158,61 @@ namespace Postler {
         bool badge_timer () {
             var accounts = new Accounts ();
             uint new_messages = 0;
-            foreach (var info in accounts.get_infos ()) {
-                try {
-                    var inbox = Dir.open (info.path + "/INBOX/new", 0);
-                    while (inbox.read_name () != null)
-                        new_messages++;
-                }
-                catch (GLib.Error error) { }
+            try {
+                new_messages = (uint)unread_messages ("%/INBOX/%");
             }
+            catch (GLib.Error error) { }
             dockitem.set_badge (new_messages);
             return true;
         }
 
+        void setup_index (Index index, Accounts accounts) {
+            stdout.printf (_("Updating index...\n"));
+            foreach (var account_info in accounts.get_infos ()) {
+                if (account_info.type == AccountType.SEARCH)
+                    continue;
+                var folder = File.new_for_path (account_info.path);
+                try {
+                    var monitor = folder.monitor_directory (0, null);
+                    monitor.changed.connect ((monitor, file, other, event) => {            
+                        /* TODO */
+                    });
+                    var enumerator = folder.enumerate_children (
+                        FILE_ATTRIBUTE_STANDARD_NAME, 0, null);
+                    FileInfo info;
+                    while ((info = enumerator.next_file (null)) != null) {
+                        var cur = folder.resolve_relative_path (info.get_name () + "/new");
+                        var file_enumerator = cur.enumerate_children (
+                            FILE_ATTRIBUTE_STANDARD_NAME, 0, null);
+                        FileInfo file_info;
+                        while ((file_info = file_enumerator.next_file (null)) != null) {
+                            var file = cur.resolve_relative_path (file_info.get_name ());
+                            var message = new Message.from_file (file);
+                            index.insert_received_message (message);
+                        }
+                    }
+                    stdout.printf ("    ✓ %s\n", account_info.name);
+                }
+                catch (GLib.Error error) {
+                    stdout.printf ("    ✗ %s\n", account_info.name);
+                    GLib.warning (_("Failed to index account \"%s\": %s"),
+                        account_info.path, error.message);
+                }
+            }
+        }
+
         public PostlerService () {
             GLib.Timeout.add_seconds (600, new_message_timer); /* 10 minutes */
-            GLib.Timeout.add_seconds (30, badge_timer);
 
+            var accounts = new Accounts ();
+            try {
+                setup_index (get_index (), accounts);
+            }
+            catch (GLib.Error error) {
+                GLib.warning (_("Index can't be setup: %s"), error.message);
+            }
+
+            GLib.Timeout.add_seconds (30, badge_timer);
             dockitem = new Dock.Item.for_name (_("Postler"));
 
 #if HAVE_INDICATE
@@ -204,7 +242,6 @@ namespace Postler {
                 items.append (item);
             }
 
-            var accounts = new Accounts ();
             foreach (var info in accounts.get_infos ())
                 add_inbox_indicator (info);
             indicator.show ();
@@ -261,6 +298,28 @@ namespace Postler {
             }
         }
 
+        Index get_index () throws GLib.Error {
+            if (index == null)
+                index = new Index ();
+            return index;
+        }
+
+        public int64 unread_messages (string uri) throws GLib.Error {
+             return get_index ().unread_messages (uri);
+        }
+
+        public GLib.HashTable<string,Variant> get_message (string uri) throws GLib.Error {
+            return get_index ().get_message (uri).to_hash_table ();
+        }
+
+        public string[] get_messages (string uri) throws GLib.Error {
+            var messages = get_index ().get_messages (uri);
+            string[] uris = {};
+            foreach (var message in messages)
+                uris += message.uri;
+            return uris;
+        }
+
         public signal void progress (string account, string text, double fraction);
 
         public void receive (string account) {
diff --git a/postler/wscript_build b/postler/wscript_build
index 7d9331b..9501644 100644
--- a/postler/wscript_build
+++ b/postler/wscript_build
@@ -13,9 +13,9 @@ obj.name = 'postler'
 obj.target = 'postler'
 obj.includes = '. ..'
 obj.find_sources_in_dirs ('.')
-obj.uselib = 'GIO GTHREAD GTK LIBNOTIFY LIBCANBERRA INDICATE UNIQUE WEBKIT'
+obj.uselib = 'GIO GTHREAD GTK LIBNOTIFY LIBCANBERRA INDICATE UNIQUE WEBKIT SQLITE3 ZEITGEIST'
 obj.packages = 'config postler posix gio-2.0 gtk+-2.0 libnotify libcanberra ' \
-               'unique-1.0 webkit-1.0'
+               'unique-1.0 webkit-1.0 sqlite3 zeitgeist-1.0'
 obj.vapi_dirs = '.'
 
 if bld.env['HAVE_INDICATE']:
diff --git a/wscript b/wscript
index f4b4d13..82e2513 100644
--- a/wscript
+++ b/wscript
@@ -114,9 +114,22 @@ def configure (conf):
     check_pkg ('unique-1.0', '0.9')
     check_pkg ('gtk+-2.0', '2.18.0', var='GTK')
     check_pkg ('webkit-1.0', '1.1.18')
+    check_pkg ('sqlite3', '3.0')
     check_pkg ('libnotify', var='LIBNOTIFY')
     check_pkg ('libcanberra', var='LIBCANBERRA')
 
+    if option_enabled ('zeitgeist'):
+        check_pkg ('zeitgeist-1.0', '0.3.10', mandatory=False)
+        if conf.env['HAVE_ZEITGEIST']:
+            conf.env.append_value ('VALAFLAGS', '-D HAVE_ZEITGEIST')
+        else:
+            Utils.pprint ('RED', 'zeitgeist is needed for Postler to show ' \
+                'up in Synapse, the Unity dash or the Activity log.\n' \
+                'If you want to build without it, pass --disable-zeitgeist.')
+            sys.exit (1)
+    else:
+        Utils.pprint ('YELLOW', 'Building without zeitgeist.')
+
     if option_enabled ('libindicate'):
         check_pkg ('indicate-0.5', mandatory=False)
         if not conf.env['HAVE_INDICATE']:
@@ -254,6 +267,7 @@ def set_options (opt):
         help='Update localization files', dest='update_po')
     add_enable_option ('docs', 'informational text files', group)
 
+    add_enable_option ('zeitgeist', 'Zeitgeist support (Synapse, Unity)')
     add_enable_option ('libindicate', 'Messaging Menu support (Ayatana)')
 
 # Taken from Geany's wscript, modified to support LINGUAS variable



More information about the Xfce4-commits mailing list