summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Dufresne <nicolas.dufresne@collabora.co.uk>2010-02-25 16:17:46 -0500
committerNicolas Dufresne <nicolas.dufresne@collabora.co.uk>2010-02-25 16:17:46 -0500
commit8d43e64804d43aafe3ab6db69ecdd47395638955 (patch)
treef919157e63239ee3a32b7bf8eaf6cec45c6dbd1b
parent8d48b82762275383debd8197e63f3c78e05c2f06 (diff)
parenta88baea0dd8f3144678175cb871c84576a511c77 (diff)
downloadtelepathy-gabble-8d43e64804d43aafe3ab6db69ecdd47395638955.tar.gz
Merge branch 'email-notification'
Reviewed-by: Simon McVittie <simon.mcvittie@collabora.co.uk>
-rw-r--r--extensions/Connection_Interface_Mail_Notification.xml653
-rw-r--r--extensions/all.xml1
-rw-r--r--src/Makefile.am2
-rw-r--r--src/conn-mail-notif.c757
-rw-r--r--src/conn-mail-notif.h38
-rw-r--r--src/connection.c46
-rw-r--r--src/connection.h11
-rw-r--r--src/debug.c1
-rw-r--r--src/debug.h3
-rw-r--r--src/ft-channel.c2
-rw-r--r--src/jingle-session.c2
-rw-r--r--src/namespaces.h1
-rw-r--r--src/util.h2
-rw-r--r--src/vcard-manager.c2
-rw-r--r--tests/twisted/Makefile.am3
-rw-r--r--tests/twisted/constants.py1
-rw-r--r--tests/twisted/gabbletest.py6
-rw-r--r--tests/twisted/mail-notification.py358
-rw-r--r--tests/twisted/ns.py1
-rw-r--r--tests/twisted/servicetest.py1
20 files changed, 1881 insertions, 10 deletions
diff --git a/extensions/Connection_Interface_Mail_Notification.xml b/extensions/Connection_Interface_Mail_Notification.xml
new file mode 100644
index 000000000..35678c2f9
--- /dev/null
+++ b/extensions/Connection_Interface_Mail_Notification.xml
@@ -0,0 +1,653 @@
+<?xml version="1.0" ?>
+<node name="/Connection_Interface_Mail_Notification"
+ xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0"
+ >
+ <tp:copyright> Copyright (C) 2007 Collabora Limited </tp:copyright>
+ <tp:license xmlns="http://www.w3.org/1999/xhtml">
+ <p>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.</p>
+
+<p>This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Library General Public License for more details.</p>
+
+<p>You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</p>
+ </tp:license>
+ <interface
+ name="org.freedesktop.Telepathy.Connection.Interface.MailNotification.DRAFT"
+ tp:causes-havoc="experimental">
+ <tp:requires interface="org.freedesktop.Telepathy.Connection"/>
+ <tp:added version="0.19.1">(as draft 1)</tp:added>
+
+ <tp:flags name="Mail_Notification_Flags" value-prefix="Mail_Notification_Flag" type="u" >
+ <tp:flag suffix="Supports_Unread_Mail_Count" value="1">
+ <tp:docstring>
+ This Connection provides the number of unread e-mails (or e-mail
+ threads) in the main folder of your e-mail account, as the
+ <tp:member-ref>UnreadMailCount</tp:member-ref> property. The
+ connection manager will update this value by emitting the
+ <tp:member-ref>UnreadMailsChanged</tp:member-ref> signal.
+ </tp:docstring>
+ </tp:flag>
+ <tp:flag suffix="Supports_Unread_Mails" value="2">
+ <tp:docstring>
+ This Connection provides a detailed list of unread e-mails, as the
+ <tp:member-ref>UnreadMails</tp:member-ref> property. If this flag
+ is set, <tt>Supports_Unread_Mail_Count</tt> MUST be set, and
+ <tt>Emits_Mails_Received</tt> MUST NOT be set.
+ The Connection will update the list by emitting the
+ <tp:member-ref>UnreadMailsChanged</tp:member-ref> signals.
+ </tp:docstring>
+ </tp:flag>
+ <tp:flag suffix="Emits_Mails_Received" value="4">
+ <tp:docstring>
+ This Connection emits the <tp:member-ref>MailsReceived</tp:member-ref>
+ signal, which provides details about newly arrived e-mails but does
+ not maintain their read/unread status afterwards. This flag MUST NOT
+ be combined with <tt>Supports_Unread_Mails</tt>.
+ </tp:docstring>
+ </tp:flag>
+ <tp:flag suffix="Supports_Request_Inbox_URL" value="8">
+ <tp:docstring>
+ This Connection can provide a URL (with optional POST data) to
+ open the the inbox of the e-mail account in a web-based client, via
+ the <tp:member-ref>RequestInboxURL</tp:member-ref> method.
+ </tp:docstring>
+ </tp:flag>
+ <tp:flag suffix="Supports_Request_Mail_URL" value="16">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>This Connection can provide a URL (with optional POST data) to open
+ a specific mail in a web-based client, via the
+ <tp:member-ref>RequestMailURL</tp:member-ref> method. This feature
+ is not useful unless either Emits_Mails_Received or
+ Supports_Unread_Mails is set.</p>
+
+ <p>If this flag is not set, clients SHOULD fall back to using
+ <tp:member-ref>RequestInboxURL</tp:member-ref> if available.</p>
+ </tp:docstring>
+ </tp:flag>
+ <tp:flag suffix="Thread_Based" value="32">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>Each <tp:type>Mail</tp:type> represents a thread of e-mails, which
+ MAY have more than one sender.</p>
+
+ <tp:rationale>
+ <p>Google Talk notifies users about new mail in terms of unread
+ threads, rather than unread e-mails.</p>
+ </tp:rationale>
+ </tp:docstring>
+ </tp:flag>
+
+ <tp:docstring>
+ <p>Flags representing capabilities provided by a connection manager.
+ Those values can be used as bitfield. Some flags depend on, or
+ conflict with, each other.</p>
+
+ <p>Connections SHOULD implement as many of these features as the
+ underlying protocol allows, preferring to implement
+ Supports_Unread_Mails instead of Emits_Mails_Received if both are
+ possible.</p>
+ </tp:docstring>
+ </tp:flags>
+
+ <tp:enum name="HTTP_Method" type="u">
+ <tp:enumvalue suffix="Get" value="0">
+ <tp:docstring>
+ Use the GET method when opening the URL.
+ </tp:docstring>
+ </tp:enumvalue>
+ <tp:enumvalue suffix="Post" value="1">
+ <tp:docstring>
+ Use the POST method when opening the URL. Refer to
+ <tp:type>HTTP_Post_Data</tp:type> for more details.
+ </tp:docstring>
+ </tp:enumvalue>
+ <tp:docstring>
+ The HTTP Method with which to request a URL.
+ </tp:docstring>
+ </tp:enum>
+
+ <tp:struct name="HTTP_Post_Data" array-name="HTTP_Post_Data_List">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>A pair (key, value) representing POST data compatible with the
+ application/x-www-form-urlencoded MIME type. The strings MUST be
+ valid UTF-8 strings, and the characters used in the key MUST obey
+ the requirements of the
+ <a href="http://www.w3.org/TR/html401/types.html#type-cdata">
+ HTML CDATA type</a>. The value MUST NOT be
+ encoded with HTML entities.</p>
+
+ <p>For example, if the POST data should contain a key "less-than" with value
+ "&lt;", and a key "percent" with value "%", this should be represented as
+ two HTTP_Post_Data structures, ("less-than", "&lt;") and ("percent", "%"),
+ resulting in a POST request whose request body is "less-than=&amp;lt;&amp;percent=%25".
+ If a client passes this to a browser by writing it into an HTML form, it
+ could do so by representing it as:</p>
+
+ <pre>
+ &lt;input type="hidden" name="less-than"&gt;&amp;lt;&lt;/input&gt;
+ &lt;input type="hidden" name="percent"&gt;%&lt;/input&gt;
+ </pre>
+
+ <tp:rationale>
+ <p>This data can be used to generate a HTML file that will
+ automatically load the URL with appropriate POST data, in which case
+ the client MUST convert any characters that are special within HTML
+ into HTML entities. Alternatively, it can be used in an API that will
+ instruct the browser how to load the URL (like the Netscape Plug-in
+ API), in which case the client MUST escape
+ <a href="http://www.ietf.org/rfc/rfc1738.txt">characters that are
+ reserved in URLs</a>, if appropriate for that API.</p>
+
+ <p>An array of pairs is used instead of a map from keys to values,
+ because it's valid to repeat keys in both HTML and
+ x-www-form-urlencoded data.</p>
+ </tp:rationale>
+ </tp:docstring>
+ <tp:member type="s" name="Key">
+ <tp:docstring>The key, corresponding to a HTML control
+ name</tp:docstring>
+ </tp:member>
+ <tp:member type="s" name="Value">
+ <tp:docstring>The value</tp:docstring>
+ </tp:member>
+ </tp:struct>
+
+ <tp:struct name="Mail_Address" array-name="Mail_Address_List">
+ <tp:docstring>
+ A pair (name, address) representing an e-mail address,
+ such as ("Nicolas Dufresne", "nicolas.dufresne@collabora.co.uk").
+ </tp:docstring>
+ <tp:member type="s" name="Name">
+ <tp:docstring>The displayed name corresponding to the e-mail
+ address</tp:docstring>
+ </tp:member>
+ <tp:member type="s" name="Address">
+ <tp:docstring>The actual e-mail address</tp:docstring>
+ </tp:member>
+ </tp:struct>
+
+ <tp:struct name="Mail_URL">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>A structure containing the required information to open a web-based
+ e-mail UI, without needing re-authentication (if possible).</p>
+
+ <p>Because the URL and POST data frequently contain short-lived
+ credential tokens, a new URL should be requested (by calling one of
+ the methods that returns a Mail_URL) for each visit to the web-based
+ UI, and the URL should be visited soon after it is returned.</p>
+ </tp:docstring>
+ <tp:member type="s" name="URL">
+ <tp:docstring>
+ The URL to which to send a request.
+ </tp:docstring>
+ </tp:member>
+ <tp:member type="u" name="Method" tp:type="HTTP_Method">
+ <tp:docstring>
+ The HTTP method of the request.
+ </tp:docstring>
+ </tp:member>
+ <tp:member type="a(ss)" name="Post_Data" tp:type="HTTP_Post_Data[]">
+ <tp:docstring>
+ An array of name-value pairs containing the POST data to use when
+ opening the URL. This MUST be an empty array if the Method is not
+ POST.
+ </tp:docstring>
+ </tp:member>
+ </tp:struct>
+
+ <tp:mapping name="Mail" array-name="Mail_List">
+ <tp:docstring>
+ An extensible map representing a mail, or (on protocols where
+ <tt>Thread_Based</tt> appears in
+ <tp:member-ref>MailNotificationFlags</tp:member-ref>) a thread of
+ mails. All keys are optional where not otherwise stated; however, at
+ least one of "senders" and "subject" must be included.
+ </tp:docstring>
+
+ <tp:member type="s" name="Key">
+ <tp:docstring>
+ <p>A key providing information about the mail or thread. Well-known
+ keys are as follows:</p>
+
+ <dl>
+ <dt>id &#8212; s</dt>
+ <dd>
+ <p>A unique ID for this e-mail. CMs with
+ <tt>Supports_Unread_Mails</tt> set in
+ <tp:member-ref>MailNotificationFlags</tp:member-ref> MUST provide
+ this key in each <tp:type>Mail</tp:type>.</p>
+
+ <p>If provided, the ID SHOULD be unique to a Mail at least until
+ that mail is removed with the
+ <tp:member-ref>UnreadMailsChanged</tp:member-ref> signal
+ (in protocols with <tt>Supports_Unread_Emails</tt>), or
+ unique for the duration of a session (otherwise).</p>
+
+ <tp:rationale>
+ <p>In protocols with Supports_Unread_Mails, this key is used to
+ indicate which mail was removed. In protocols without that
+ feature, it's impossible to tell when a mail has been removed
+ (and hence how long the identifier will remain valid for use
+ with <tp:member-ref>RequestMailURL</tp:member-ref>).</p>
+ </tp:rationale>
+ </dd>
+
+ <dt>url-data &#8212; any type</dt>
+ <dd>An opaque identifier (typically a string or list of strings)
+ provided to the Connection when calling
+ <tp:member-ref>RequestMailURL</tp:member-ref>,
+ containing information used by the Connection to build the URL.
+ </dd>
+
+ <dt>senders &#8212; a(ss) (<tp:type>Mail_Address</tp:type>)</dt>
+ <dd>
+ An array of sender display name and e-mail address pairs. Note that
+ only e-mails represented as a thread can have multiple senders.
+ </dd>
+
+ <dt>to-addresses &#8212; a(ss) (<tp:type>Mail_Address</tp:type>)</dt>
+ <dd>
+ An array of display name and e-mail address pairs representing
+ the recipients.
+ </dd>
+
+ <dt>cc-addresses &#8212; a(ss) (<tp:type>Mail_Address</tp:type>)</dt>
+ <dd>
+ An array of display name and e-mail address pairs representing
+ the carbon-copy recipients.
+ </dd>
+
+ <dt>sent-timestamp &#8212; x (<tp:type>Unix_Timestamp64</tp:type>)</dt>
+ <dd>A UNIX timestamp indicating when the message was sent, or for
+ a thread, when the most recent message was sent.
+ </dd>
+
+ <dt>received-timestamp &#8212; x (<tp:type>Unix_Timestamp64</tp:type>)</dt>
+ <dd>A UNIX timestamp indicating when the message was received, or for
+ a thread, when the most recent message was received.
+ </dd>
+
+ <dt>has-attachments &#8212; b</dt>
+ <dd>If true, this mail has attachments.</dd>
+
+ <dt>subject &#8212; s</dt>
+ <dd>
+ The subject of the message. This MUST be encoded in UTF-8.
+ </dd>
+
+ <dt>content-type &#8212; s</dt>
+ <dd>
+ <p>The MIME type of the message content. Two types are currently
+ supported: "text/plain" for plain text, and "text/html" for a
+ HTML document. If omitted, "text/plain" MUST be assumed.
+ Regardless of MIME type, the content MUST be valid UTF-8 (which
+ may require that the Connection transcodes it from a legacy
+ encoding).</p>
+
+ <tp:rationale>
+ <p>All strings on D-Bus must be UTF-8.</p>
+ </tp:rationale>
+ </dd>
+
+ <dt>truncated &#8212; b</dt>
+ <dd>
+ If true, the content is only a partial message; if false or
+ omitted, the content is the entire message.
+ </dd>
+
+ <dt>content &#8212; s</dt>
+ <dd>
+ The body of the message, possibly truncated, encoded as appropriate
+ for "content-type".
+ </dd>
+
+ <dt>folder &#8212; s</dt>
+ <dd>
+ The name of the folder containing this e-mails.
+ If omitted, the inbox SHOULD be assumed.
+ </dd>
+ </dl>
+ </tp:docstring>
+ </tp:member>
+
+ <tp:member name="Value" type="v">
+ <tp:docstring>The value, of whatever type is appropriate for the
+ key.</tp:docstring>
+ </tp:member>
+ </tp:mapping>
+
+ <property name="MailNotificationFlags" type="u" access="read"
+ tp:type="Mail_Notification_Flags"
+ tp:name-for-bindings="Mail_Notification_Flags">
+ <tp:docstring>
+ Integer representing the bitwise-OR of supported features for e-mails
+ notification on this server. This property MUST NOT change after the
+ Connection becomes CONNECTED.
+
+ <tp:rationale>
+ This property indicates the behavior and availability
+ of the other properties and signals within this interface. A
+ connection manager that cannot at least set one of the flags
+ in the <tp:type>Mail_Notification_Flags</tp:type>
+ SHOULD NOT provide this interface.
+ </tp:rationale>
+ </tp:docstring>
+ </property>
+
+ <property name="UnreadMailCount" type="u" access="read"
+ tp:name-for-bindings="Unread_Mail_Count">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>The number of unread messages in the Inbox. Change notification is
+ via <tp:member-ref>UnreadMailsChanged</tp:member-ref>.</p>
+
+ <p>This property is only useful if <tt>Supports_Unread_Mail_Count</tt>
+ is set in the <tp:member-ref>MailNotificationFlags</tp:member-ref>;
+ otherwise, it MUST be zero.</p>
+
+ <p>If <tt>Thread_Based</tt> appears in the
+ <tp:member-ref>MailNotificationFlags</tp:member-ref>, this property
+ counts the number of threads, not the number of mails.</p>
+ </tp:docstring>
+ </property>
+
+ <property name="UnreadMails" type="aa{sv}" tp:type="Mail[]"
+ tp:name-for-bindings="Unread_Mails" access="read">
+ <tp:docstring>
+ A array of unread <tp:type>Mail</tp:type>s. Change notification is via
+ <tp:member-ref>UnreadMailsChanged</tp:member-ref>. This property is
+ only useful if <tt>Supports_Unread_Mails</tt> is set in
+ <tp:member-ref>MailNotificationFlags</tp:member-ref>; otherwise, it MUST be
+ an empty list.
+ </tp:docstring>
+ </property>
+
+ <property name="MailAddress" type="s"
+ tp:name-for-bindings="Mail_Address" access="read">
+ <tp:docstring>
+ A string representing the e-mail address of the account. The CMs MUST
+ provide this information.
+ <tp:rationale>
+ In close integration of MailNotification with other e-mail services,
+ the e-mail address can be used has a unique identifier for the
+ account. Possible integration could be between Telepathy and
+ Evolution where the e-mail address is the common information in
+ both interfaces.
+ </tp:rationale>
+ </tp:docstring>
+ </property>
+
+ <signal name="MailsReceived" tp:name-for-bindings="Mails_Received">
+ <arg name="Mails" type="aa{sv}" tp:type="Mail[]">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>An array of <tp:type>Mail</tp:type>s. Those e-mail MUST NOT have
+ the "id" key.</p>
+
+ <tp:rationale>
+ <p>On connections that emit this signal, it's impossible to tell
+ when a mail has been removed, and hence when "id" has become
+ invalid.</p>
+ </tp:rationale>
+ </tp:docstring>
+ </arg>
+
+ <tp:docstring>
+ Emitted when new e-mails messages arrive to the inbox associated with
+ this connection. This signal is used for protocols that are not able
+ to maintain the <tp:member-ref>UnreadMails</tp:member-ref> list, but
+ do provide real-time notification about newly arrived e-mails. It MUST
+ NOT be emitted unless <tt>Emits_Mails_Received</tt> is set in
+ <tp:member-ref>MailNotificationFlags</tp:member-ref>.
+ </tp:docstring>
+ </signal>
+
+ <signal name="UnreadMailsChanged"
+ tp:name-for-bindings="Unread_Mails_Changed">
+ <arg name="Count" type="u">
+ <tp:docstring>
+ Number of unread messages in the inbox (the new value of
+ <tp:member-ref>UnreadMailCount</tp:member-ref>).
+ </tp:docstring>
+ </arg>
+ <arg name="Mails_Added" type="aa{sv}" tp:type="Mail[]">
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>A list of <tp:type>Mail</tp:type> that are being added or updated
+ in <tp:member-ref>UnreadMails</tp:member-ref>.</p>
+
+ <tp:rationale>
+ <p>Mails may be updated when the URL information (URL and POST data)
+ have changed, or senders were added or removed from an e-mail
+ thread.</p>
+ </tp:rationale>
+
+ <p>If the <tt>Supports_Unread_Mails</tt> flag is not set, this list
+ MUST be empty, even if Count has increased.</p>
+ </tp:docstring>
+ </arg>
+ <arg name="Mails_Removed" type="as">
+ <tp:docstring>
+ A list of e-mail IDs that are being removed from
+ <tp:member-ref>UnreadMails</tp:member-ref>.
+ If the <tt>Supports_Unread_Mails</tt> flag is not set, this list
+ MUST be empty, even if Count has decreased.
+ </tp:docstring>
+ </arg>
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>Emitted when <tp:member-ref>UnreadMails</tp:member-ref> or
+ <tp:member-ref>UnreadMailCount</tp:member-ref> have changed. It MUST
+ NOT be emited if <tt>Supports_Unread_Mail_Count</tt> flag is not set
+ in <tp:member-ref>MailNotificationFlags</tp:member-ref>.</p>
+
+ <p><tt>Mails_Added</tt> and
+ <tt>Mails_Removed</tt> MUST be empty if the
+ <tt>Supports_Unread_Mails</tt> flag is not set.</p>
+ </tp:docstring>
+ </signal>
+
+ <method name="Subscribe"
+ tp:name-for-bindings="Subscribe">
+ <tp:docstring>
+ <p>This method subscribes a client to the notification interface. This
+ MUST be called by clients before using this interface.</p>
+
+ <p>The Connection tracks a subscription count (like a refcount) for
+ each unique bus name that has called Subscribe(). When a client calls
+ Unsubscribe(), it releases one "reference". If a client exits
+ (or crashes), the Connection releases all "references" held on its
+ behalf.</p>
+
+ <tp:rationale>
+ <p>The reference count imposed on the subscription simplifies
+ implementation of client running in the same process
+ (e.g. plug-ins): two plug-ins interested in mail notification can
+ call Subscribe and Unsubscribe independently without interfering
+ with each other.</p>
+
+ <p>This method exists to reduce memory and network overhead when
+ there is no active subscription. An example of a protocol that
+ benefits from this method is the Google XMPP Mail Notification
+ extension: in this protocol, the CM receives a notification
+ that something has changed, but to get more information, the CM
+ must request this information. Knowing that nobody is currently
+ interested in this information, the CM can avoid generating
+ useless network traffic. Similarly, the CM may free
+ the list of unread messages to reduce memory overhead.</p>
+ </tp:rationale>
+
+ </tp:docstring>
+ <tp:possible-errors>
+ <tp:error name="org.freedesktop.Telepathy.Error.Disconnected"/>
+ <tp:error name="org.freedesktop.Telepathy.Error.NotImplemented"/>
+ </tp:possible-errors>
+ </method>
+
+ <method name="Unsubscribe"
+ tp:name-for-bindings="Unsubscribe">
+ <tp:docstring>
+ This method unsubscribes a client from the notification interface.
+ This SHOULD be called by each client that has successfully called
+ Subscribe when it no longer needs the mail notification interface.
+
+ <tp:rationale>
+ See <tp:member-ref>Subscribe</tp:member-ref> for rationale.
+ </tp:rationale>
+ </tp:docstring>
+ <tp:possible-errors>
+ <tp:error name="org.freedesktop.Telepathy.Error.NotAvailable">
+ <tp:docstring>
+ Raised if the client calling this method has no references to
+ release.
+ </tp:docstring>
+ </tp:error>
+ <tp:error name="org.freedesktop.Telepathy.Error.NotImplemented"/>
+ </tp:possible-errors>
+ </method>
+
+ <method name="RequestInboxURL"
+ tp:name-for-bindings="Request_Inbox_URL">
+ <arg direction="out" name="URL" type="(sua(ss))" tp:type="Mail_URL" >
+ <tp:docstring>
+ A struture containing a URL and optional additional data to open a
+ webmail client, without re-authentication if possible.
+ </tp:docstring>
+ </arg>
+ <tp:docstring>
+ This method creates and returns a URL and an optional POST data that
+ allow opening the Inbox folder of a webmail account. This URL MAY
+ contain tokens with a short lifetime, so clients SHOULD request a new
+ URL for each visit to the webmail interface. This method is implemented
+ only if the <tt>Supports_Request_Inbox_URL</tt> flag is set in
+ <tp:member-ref>MailNotificationFlags</tp:member-ref>.
+
+ <tp:rationale>
+ We are not using properties here because the tokens are unsuitable
+ for sharing between clients, and network round-trips may be required
+ to obtain the information that leads to authentication free webmail
+ access.
+ </tp:rationale>
+ </tp:docstring>
+ <tp:possible-errors>
+ <tp:error name="org.freedesktop.Telepathy.Error.Disconnected"/>
+ <tp:error name="org.freedesktop.Telepathy.Error.NetworkError"/>
+ <tp:error name="org.freedesktop.Telepathy.Error.NotImplemented"/>
+ </tp:possible-errors>
+ </method>
+
+ <method name="RequestMailURL"
+ tp:name-for-bindings="Request_Mail_URL">
+ <arg direction="in" name="ID" type="s">
+ <tp:docstring>
+ The mail's <tt>id</tt> as found in the <tp:type>Mail</tp:type>
+ structure, or the empty string if no <tt>id</tt> key was provided.
+ </tp:docstring>
+ </arg>
+ <arg direction="in" name="URL_Data" type="v">
+ <tp:docstring>
+ Whatever <tt>url-data</tt> was found in the <tp:type>Mail</tp:type>
+ structure, or the boolean value False (D-Bus type 'b') if no
+ <tt>url-data</tt> was provided in the Mail.
+ </tp:docstring>
+ </arg>
+ <arg direction="out" name="URL" type="(sua(ss))" tp:type="Mail_URL" >
+ <tp:docstring>
+ A struture that contains a URL and optional additional data to open a
+ webmail client, without re-authentication if possible.
+ </tp:docstring>
+ </arg>
+ <tp:docstring>
+ This method creates and returns a URL and optional POST data that
+ allow opening a specific mail in a webmail interface. This
+ method is implemented only if <tt>Supports_Request_Mail_URL</tt> flag
+ is set in <tp:member-ref>MailNotificationFlags</tp:member-ref>.
+ <tp:rationale>
+ See <tp:member-ref>RequestInboxURL</tp:member-ref> for design
+ rationale.
+ </tp:rationale>
+ </tp:docstring>
+ <tp:possible-errors>
+ <tp:error name="org.freedesktop.Telepathy.Error.Disconnected"/>
+ <tp:error name="org.freedesktop.Telepathy.Error.NetworkError"/>
+ <tp:error name="org.freedesktop.Telepathy.Error.NotImplemented"/>
+ <tp:error name="org.freedesktop.Telepathy.Error.InvalidArgument"/>
+ </tp:possible-errors>
+ </method>
+
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>An interface to support receiving notifications about a e-mail
+ account associated with this connection.</p>
+
+ <p>In protocols where this is possible, this interface also allows the
+ connection manager to provide the necessary information for clients
+ to open a web-based mail client without having to re-authenticate.</p>
+
+ <p>To use this interface, a client MUST first subscribe using the
+ <tp:member-ref>Subscribe</tp:member-ref> method. The subscription
+ mechanic aims at reducing network traffic and memory footprint in the
+ situation where nobody is currently interesting in provided
+ information. When done with this interface, clients SHOULD call
+ <tp:member-ref>Unsubscribe</tp:member-ref> to release resources in
+ the CM.</p>
+
+ <p>Protocols have various different levels of Mail Notification support.
+ To describe the level of support, the interface provides a property
+ called <tp:member-ref>MailNotificationFlags</tp:member-ref>.
+ Not all combinations are valid; protocols can be divided into four
+ categories as follows.</p>
+
+ <p>Connections to the most capable protocols, such as Google's XMPP Mail
+ Notification extension, have the Supports_Unread_Mails flag (this
+ implies that they must also have Supports_Unread_Mail_Count, but not
+ Emits_Mails_Received). On these connections, clients
+ requiring change notification MUST monitor the
+ <tp:member-ref>UnreadMailsChanged</tp:member-ref> signal, and
+ either recover the initial state from the
+ <tp:member-ref>UnreadMails</tp:member-ref> property (if they require
+ details other than the number of mails) or the
+ <tp:member-ref>UnreadMailCount</tp:member-ref> property (if they
+ are only interested in the number of unread mails). The
+ <tp:member-ref>MailsReceived</tp:member-ref> signal is never emitted
+ on these connections, so clients that will display a short-term
+ notification for each new mail MUST do so in response to emission of
+ the <tp:member-ref>UnreadMailsChanged</tp:member-ref> signal.</p>
+
+ <p>The most common situation, seen in protocols like MSN and Yahoo, is
+ that the number of unread mails is provided and kept up-to-date,
+ and a separate notification is emitted with some details of each new
+ mail. This is a combination of the following two features, and clients
+ SHOULD implement one or both as appropriate for their requirements.</p>
+
+ <p>On protocols that have the Emits_Mails_Received flag (which implies
+ that they do not have Supports_Unread_Mails), the CM does not keep
+ track of any mails; it simply emits a notification whenever new mail
+ arrives. Those events may be used for short term display (like a
+ notification popup) to inform the user. No protocol is known to support
+ only this feature, but it is useful for integration with libraries that
+ that do not implement tracking of the number of mails. Clients
+ requiring these notifications MUST monitor the
+ <tp:member-ref>MailsReceived</tp:member-ref> signal on any connections
+ with this flag.</p>
+
+ <p>On protocols that have the Supports_Unread_Mail_Count flag but not
+ the Supports_Unread_Mails flag, clients cannot display complete
+ details of unread email, but can display an up-to-date count of the
+ <em>number</em> of unread mails. To do this, they must monitor the
+ <tp:member-ref>UnreadMailsChanged</tp:member-ref> signal, and
+ retrieve the initial state from the
+ <tp:member-ref>UnreadMailCount</tp:member-ref> property.</p>
+
+ <p>
+ Orthogonal features described by the
+ <tp:member-ref>MailNotificationFlags</tp:member-ref> property include the
+ RequestSomethingURL methods, which are used to obtain URLs allowing
+ clients to open a webmail client. Connections SHOULD support as many
+ of these methods as possible.</p>
+ </tp:docstring>
+ </interface>
+</node>
+<!-- vim:set sw=2 sts=2 et ft=xml: -->
+
diff --git a/extensions/all.xml b/extensions/all.xml
index 81a82206f..cd6913727 100644
--- a/extensions/all.xml
+++ b/extensions/all.xml
@@ -46,6 +46,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA</p>
<xi:include href="Connection_Interface_Contact_Info.xml"/>
<xi:include href="Connection_Interface_Gabble_Decloak.xml"/>
<xi:include href="Channel_Interface_Conference.xml"/>
+<xi:include href="Connection_Interface_Mail_Notification.xml"/>
<xi:include href="Connection_Future.xml"/>
<xi:include href="Gabble_Plugin_Gateways.xml"/>
diff --git a/src/Makefile.am b/src/Makefile.am
index e0d656a74..02ef616c9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -53,6 +53,8 @@ libgabble_convenience_la_SOURCES = \
conn-presence.c \
conn-sidecars.h \
conn-sidecars.c \
+ conn-mail-notif.h \
+ conn-mail-notif.c \
connection.h \
connection.c \
connection-manager.h \
diff --git a/src/conn-mail-notif.c b/src/conn-mail-notif.c
new file mode 100644
index 000000000..072c693de
--- /dev/null
+++ b/src/conn-mail-notif.c
@@ -0,0 +1,757 @@
+/*
+ * conn-mail-notif - Gabble mail notification interface
+ * Copyright (C) 2009-2010 Collabora Ltd.
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/* This is implementation of Gmail Notification protocol as documented at
+ * http://code.google.com/apis/talk/jep_extensions/gmail.html
+ * This is the only protocol supported at the moment. To add new protocol,
+ * one would have to split the google specific parts wich are the
+ * update_unread_mails() function and the new-mail signal on google xml
+ * namespace. The data structure and suscription mechanism shall remain across
+ * protocols.
+ */
+
+#include "config.h"
+
+#include "conn-mail-notif.h"
+
+#include <string.h>
+
+#include <dbus/dbus-glib-lowlevel.h>
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/interfaces.h>
+#include <telepathy-glib/svc-connection.h>
+#include <telepathy-glib/util.h>
+
+#define DEBUG_FLAG GABBLE_DEBUG_MAIL_NOTIF
+#include "connection.h"
+#include "debug.h"
+#include "extensions/extensions.h"
+#include "namespaces.h"
+#include "util.h"
+
+
+enum
+{
+ PROP_MAIL_NOTIFICATION_FLAGS,
+ PROP_UNREAD_MAIL_COUNT,
+ PROP_UNREAD_MAILS,
+ PROP_MAIL_ADDRESS,
+ NUM_OF_PROP,
+};
+
+
+static void unsubscribe (GabbleConnection *conn, const gchar *name,
+ gboolean remove_all);
+static void update_unread_mails (GabbleConnection *conn);
+
+
+static void
+subscriber_name_owner_changed (TpDBusDaemon *dbus_daemon,
+ const gchar *name,
+ const gchar *new_owner,
+ gpointer user_data)
+{
+ GabbleConnection *conn = GABBLE_CONNECTION (user_data);
+
+ if (CHECK_STR_EMPTY (new_owner))
+ {
+ DEBUG ("Sender removed: %s", name);
+ unsubscribe (conn, name, TRUE);
+ }
+}
+
+
+static void
+unsubscribe (GabbleConnection *conn,
+ const gchar *name, gboolean remove_all)
+{
+ guint count;
+
+ g_return_if_fail (g_hash_table_size (conn->mail_subscribers) > 0);
+
+ count = GPOINTER_TO_UINT (
+ g_hash_table_lookup (conn->mail_subscribers, name));
+
+ if (count == 1 || remove_all)
+ {
+ tp_dbus_daemon_cancel_name_owner_watch (conn->daemon, name,
+ subscriber_name_owner_changed, conn);
+
+ g_hash_table_remove (conn->mail_subscribers, name);
+
+ if (g_hash_table_size (conn->mail_subscribers) == 0)
+ {
+ DEBUG ("Last subscriber unsubscribed, cleaning up!");
+ g_free (conn->inbox_url);
+ conn->inbox_url = NULL;
+
+ if (conn->unread_mails != NULL)
+ {
+ g_hash_table_unref (conn->unread_mails);
+ conn->unread_mails = NULL;
+ }
+ }
+ }
+ else
+ {
+ g_hash_table_insert (conn->mail_subscribers, g_strdup (name),
+ GUINT_TO_POINTER (--count));
+ }
+}
+
+
+static inline gboolean
+check_supported_or_dbus_return (GabbleConnection *conn,
+ DBusGMethodInvocation *context)
+{
+ if (TP_BASE_CONNECTION (conn)->status != TP_CONNECTION_STATUS_CONNECTED)
+ {
+ GError e = { TP_ERRORS, TP_ERROR_DISCONNECTED, "Not connected" };
+ dbus_g_method_return_error (context, &e);
+ return TRUE;
+ }
+
+ if (!(conn->features & GABBLE_CONNECTION_FEATURES_GOOGLE_MAIL_NOTIFY))
+ {
+ tp_dbus_g_method_return_not_implemented (context);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+static void
+gabble_mail_notification_subscribe (GabbleSvcConnectionInterfaceMailNotification *iface,
+ DBusGMethodInvocation *context)
+{
+ GabbleConnection *conn = GABBLE_CONNECTION (iface);
+ gchar *subscriber;
+ guint count;
+
+ if (check_supported_or_dbus_return (conn, context))
+ return;
+
+ subscriber = dbus_g_method_get_sender (context);
+
+ DEBUG ("Subscribe called by: %s", subscriber);
+
+ count = GPOINTER_TO_UINT (
+ g_hash_table_lookup (conn->mail_subscribers, subscriber));
+
+ /* Gives subscriber ownership to mail_subscribers hash table */
+ g_hash_table_insert (conn->mail_subscribers, subscriber,
+ GUINT_TO_POINTER (++count));
+
+ if (count == 1)
+ {
+ if (g_hash_table_size (conn->mail_subscribers) == 1)
+ update_unread_mails (conn);
+
+ tp_dbus_daemon_watch_name_owner (conn->daemon, subscriber,
+ subscriber_name_owner_changed, conn, NULL);
+ }
+
+ gabble_svc_connection_interface_mail_notification_return_from_subscribe (
+ context);
+}
+
+
+static void
+gabble_mail_notification_unsubscribe (GabbleSvcConnectionInterfaceMailNotification *iface,
+ DBusGMethodInvocation *context)
+{
+ GabbleConnection *conn = GABBLE_CONNECTION (iface);
+ gchar *subscriber;
+
+ if (check_supported_or_dbus_return (conn, context))
+ return;
+
+ subscriber = dbus_g_method_get_sender (context);
+
+ DEBUG ("Unsubscribe called by: %s", subscriber);
+
+ if (!g_hash_table_lookup_extended (conn->mail_subscribers, subscriber,
+ NULL, NULL))
+ {
+ GError e = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, "Not subscribed" };
+
+ DEBUG ("Subscriber '%s' is not subscribed!", subscriber);
+
+ dbus_g_method_return_error (context, &e);
+ g_free (subscriber);
+ return;
+ }
+
+ unsubscribe (conn, subscriber, FALSE);
+
+ g_free (subscriber);
+ gabble_svc_connection_interface_mail_notification_return_from_unsubscribe (context);
+}
+
+
+static void
+gabble_mail_notification_request_inbox_url (
+ GabbleSvcConnectionInterfaceMailNotification *iface,
+ DBusGMethodInvocation *context)
+{
+ GabbleConnection *conn = GABBLE_CONNECTION (iface);
+ GValueArray *result;
+ GPtrArray *empty_array;
+
+ if (check_supported_or_dbus_return (conn, context))
+ return;
+
+ /* TODO Make sure we don't have to authenticate again */
+
+ empty_array = g_ptr_array_new ();
+
+ result = tp_value_array_build (3,
+ G_TYPE_STRING, conn->inbox_url ? conn->inbox_url : "",
+ G_TYPE_UINT, GABBLE_HTTP_METHOD_GET,
+ GABBLE_ARRAY_TYPE_HTTP_POST_DATA_LIST, empty_array,
+ G_TYPE_INVALID);
+
+ gabble_svc_connection_interface_mail_notification_return_from_request_inbox_url (
+ context, result);
+
+ g_value_array_free (result);
+ g_ptr_array_free (empty_array, TRUE);
+}
+
+
+static void
+gabble_mail_notification_request_mail_url (
+ GabbleSvcConnectionInterfaceMailNotification *iface,
+ const gchar *in_id,
+ const GValue *in_url_data,
+ DBusGMethodInvocation *context)
+{
+ GabbleConnection *conn = GABBLE_CONNECTION (iface);
+ GValueArray *result;
+ gchar *url = NULL;
+ GPtrArray *empty_array;
+
+ if (check_supported_or_dbus_return (conn, context))
+ return;
+
+ /* TODO Make sure we don't have to authenticate again */
+
+ if (conn->inbox_url != NULL)
+ {
+ /* IDs are decimal on the XMPP side and hexadecimal on the wemail side. */
+ guint64 tid = g_ascii_strtoull (in_id, NULL, 0);
+ url = g_strdup_printf ("%s/#inbox/%" G_GINT64_MODIFIER "x",
+ conn->inbox_url, tid);
+ }
+
+ empty_array = g_ptr_array_new ();
+
+ result = tp_value_array_build (3,
+ G_TYPE_STRING, url ? url : "",
+ G_TYPE_UINT, GABBLE_HTTP_METHOD_GET,
+ GABBLE_ARRAY_TYPE_HTTP_POST_DATA_LIST, empty_array,
+ G_TYPE_INVALID);
+
+ gabble_svc_connection_interface_mail_notification_return_from_request_mail_url (
+ context, result);
+
+ g_value_array_free (result);
+ g_ptr_array_free (empty_array, TRUE);
+}
+
+
+static gboolean
+sender_each (WockyXmppNode *node,
+ gpointer user_data)
+{
+ GPtrArray *senders = user_data;
+
+ if (!tp_strdiff ("1", wocky_xmpp_node_get_attribute (node, "unread")))
+ {
+ GValueArray *sender;
+ const gchar *name;
+ const gchar *address;
+
+ name = wocky_xmpp_node_get_attribute (node, "name");
+ address = wocky_xmpp_node_get_attribute (node, "address");
+
+ sender = tp_value_array_build (2,
+ G_TYPE_STRING, name ? name : "",
+ G_TYPE_STRING, address ? address : "",
+ G_TYPE_INVALID);
+
+ g_ptr_array_add (senders, sender);
+ }
+ return TRUE;
+}
+
+
+static gboolean
+handle_senders (WockyXmppNode *parent_node,
+ GHashTable *mail)
+{
+ gboolean dirty = FALSE;
+ WockyXmppNode *node;
+
+ node = wocky_xmpp_node_get_child (parent_node, "senders");
+ if (node != NULL)
+ {
+ GType addr_list_type = GABBLE_ARRAY_TYPE_MAIL_ADDRESS_LIST;
+ GPtrArray *senders, *old_senders;
+
+ senders = g_ptr_array_new ();
+ wocky_xmpp_node_each_child (node, sender_each, senders);
+
+ old_senders = tp_asv_get_boxed (mail, "senders", addr_list_type);
+
+ if (old_senders == NULL || senders->len != old_senders->len)
+ dirty = TRUE;
+
+ tp_asv_take_boxed (mail, "senders", addr_list_type, senders);
+ }
+
+ return dirty;
+}
+
+
+static gboolean
+handle_subject (WockyXmppNode *parent_node,
+ GHashTable *mail)
+{
+ gboolean dirty = FALSE;
+ WockyXmppNode *node;
+
+ node = wocky_xmpp_node_get_child (parent_node, "subject");
+ if (node != NULL)
+ {
+ if (tp_strdiff (node->content, tp_asv_get_string (mail, "subject")))
+ {
+ dirty = TRUE;
+ tp_asv_set_string (mail, "subject", node->content);
+ }
+ }
+
+ return dirty;
+}
+
+
+static gboolean
+handle_snippet (WockyXmppNode *parent_node,
+ GHashTable *mail)
+{
+ gboolean dirty = FALSE;
+ WockyXmppNode *node;
+
+ node = wocky_xmpp_node_get_child (parent_node, "snippet");
+ if (node != NULL)
+ {
+ if (tp_strdiff (node->content, tp_asv_get_string (mail, "content")))
+ {
+ dirty = TRUE;
+ tp_asv_set_boolean (mail, "truncated", TRUE);
+ tp_asv_set_string (mail, "content", node->content);
+ }
+ }
+
+ return dirty;
+}
+
+
+/* Structure used has user_data mail_thread_info_each callback */
+typedef struct
+{
+ GabbleConnection *conn;
+ /* stolen from conn -> unread_mails, the left items in this is
+ * represent the removed emails */
+ GHashTable *old_mails;
+ GPtrArray *mails_added;
+} MailThreadCollector;
+
+static gboolean
+mail_thread_info_each (WockyXmppNode *node,
+ gpointer user_data)
+{
+ MailThreadCollector *collector = user_data;
+
+ if (!tp_strdiff (node->name, "mail-thread-info"))
+ {
+ GHashTable *mail = NULL;
+ const gchar *val_str;
+ gchar *tid;
+ gboolean dirty = FALSE;
+
+ val_str = wocky_xmpp_node_get_attribute (node, "tid");
+
+ /* We absolutly need an ID */
+ if (val_str == NULL)
+ return TRUE;
+
+ tid = g_strdup (val_str);
+
+ if (collector->old_mails != NULL)
+ {
+ mail = g_hash_table_lookup (collector->old_mails, tid);
+ g_hash_table_steal (collector->old_mails, tid);
+ }
+
+ if (mail == NULL)
+ {
+ mail = tp_asv_new ("id", G_TYPE_STRING, tid,
+ "url-data", G_TYPE_STRING, "",
+ NULL);
+ dirty = TRUE;
+ }
+
+ val_str = wocky_xmpp_node_get_attribute (node, "date");
+
+ if (val_str != NULL)
+ {
+ gint64 date;
+
+ date = (g_ascii_strtoll (val_str, NULL, 0) / 1000l);
+ if (date != tp_asv_get_int64 (mail, "received-timestamp", NULL))
+ dirty = TRUE;
+
+ tp_asv_set_int64 (mail, "received-timestamp", date);
+ }
+
+ if (handle_senders (node, mail))
+ dirty = TRUE;
+
+ if (handle_subject (node, mail))
+ dirty = TRUE;
+
+ if (handle_snippet (node, mail))
+ dirty = TRUE;
+
+ /* gives tid ownership to unread_mails hash table */
+ g_hash_table_insert (collector->conn->unread_mails, tid, mail);
+
+ if (dirty)
+ g_ptr_array_add (collector->mails_added, mail);
+ }
+
+ return TRUE;
+}
+
+
+static void
+store_unread_mails (GabbleConnection *conn,
+ WockyXmppNode *mailbox)
+{
+ GHashTableIter iter;
+ GPtrArray *mails_removed;
+ MailThreadCollector collector;
+ const gchar *url;
+
+ collector.conn = conn;
+ collector.old_mails = conn->unread_mails;
+ conn->unread_mails = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) g_hash_table_unref);
+ collector.mails_added = g_ptr_array_new ();
+
+ url = wocky_xmpp_node_get_attribute (mailbox, "url");
+ g_free (conn->inbox_url);
+ conn->inbox_url = g_strdup (url);
+
+ /* Store new mails */
+ wocky_xmpp_node_each_child (mailbox, mail_thread_info_each, &collector);
+
+ /* Generate the list of removed thread IDs */
+ mails_removed = g_ptr_array_new_with_free_func (g_free);
+
+ if (collector.old_mails != NULL)
+ {
+ gpointer key;
+
+ g_hash_table_iter_init (&iter, collector.old_mails);
+
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ {
+ gchar *tid = key;
+ g_ptr_array_add (mails_removed, g_strdup (tid));
+ }
+
+ g_hash_table_unref (collector.old_mails);
+ }
+ g_ptr_array_add (mails_removed, NULL);
+
+ if (collector.mails_added->len > 0 || mails_removed->len > 0)
+ gabble_svc_connection_interface_mail_notification_emit_unread_mails_changed (
+ conn, g_hash_table_size (conn->unread_mails), collector.mails_added,
+ (const char **)mails_removed->pdata);
+
+ g_ptr_array_free (collector.mails_added, TRUE);
+ g_ptr_array_free (mails_removed, TRUE);
+}
+
+
+static void
+query_unread_mails_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ WockyXmppNode *node;
+ WockyPorter *porter = WOCKY_PORTER (source_object);
+ WockyXmppStanza *reply = wocky_porter_send_iq_finish (porter, res, &error);
+
+ if (error != NULL)
+ {
+ DEBUG ("Failed retreive unread emails information: %s", error->message);
+ g_error_free (error);
+ goto end;
+ }
+
+ DEBUG ("Got unread mail details");
+
+ node = wocky_xmpp_node_get_child (reply->node, "mailbox");
+
+ if (node != NULL)
+ {
+ GabbleConnection *conn = GABBLE_CONNECTION (user_data);
+ store_unread_mails (conn, node);
+ }
+
+end:
+ if (reply != NULL)
+ g_object_unref (reply);
+}
+
+
+static void
+update_unread_mails (GabbleConnection *conn)
+{
+ WockyXmppStanza *query;
+ WockyPorter *porter = wocky_session_get_porter (conn->session);
+
+ DEBUG ("Updating unread mails information");
+
+ query = wocky_xmpp_stanza_build (WOCKY_STANZA_TYPE_IQ,
+ WOCKY_STANZA_SUB_TYPE_GET, NULL, NULL,
+ WOCKY_NODE, "query",
+ WOCKY_NODE_XMLNS, NS_GOOGLE_MAIL_NOTIFY,
+ WOCKY_NODE_END,
+ WOCKY_STANZA_END);
+ wocky_porter_send_iq_async (porter, query, NULL, query_unread_mails_cb, conn);
+ g_object_unref (query);
+}
+
+
+static gboolean
+new_mail_handler (WockyPorter *porter,
+ WockyXmppStanza *stanza,
+ gpointer user_data)
+{
+ GabbleConnection *conn = GABBLE_CONNECTION (user_data);
+
+ if (g_hash_table_size (conn->mail_subscribers) > 0)
+ {
+ DEBUG ("Got Google <new-mail> notification");
+ update_unread_mails (conn);
+ }
+
+ return TRUE;
+}
+
+
+static void
+connection_status_changed (GabbleConnection *conn,
+ TpConnectionStatus status,
+ TpConnectionStatusReason reason,
+ gpointer user_data)
+{
+ if (status == TP_CONNECTION_STATUS_CONNECTED
+ && conn->features & GABBLE_CONNECTION_FEATURES_GOOGLE_MAIL_NOTIFY)
+ {
+ DEBUG ("Connected, registering Google 'new-mail' notification");
+
+ conn->new_mail_handler_id =
+ wocky_porter_register_handler (wocky_session_get_porter (conn->session),
+ WOCKY_STANZA_TYPE_IQ, WOCKY_STANZA_SUB_TYPE_SET,
+ NULL, WOCKY_PORTER_HANDLER_PRIORITY_NORMAL,
+ new_mail_handler, conn,
+ WOCKY_NODE, "new-mail",
+ WOCKY_NODE_XMLNS, NS_GOOGLE_MAIL_NOTIFY,
+ WOCKY_NODE_END,
+ WOCKY_STANZA_END);
+ }
+}
+
+
+void
+conn_mail_notif_init (GabbleConnection *conn)
+{
+ conn->mail_subscribers = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, NULL);
+ conn->inbox_url = NULL;
+ conn->unread_mails = NULL;
+
+ g_signal_connect (conn, "status-changed",
+ G_CALLBACK (connection_status_changed), conn);
+}
+
+
+static gboolean
+foreach_cancel_watch (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ const gchar *subscriber = key;
+ GabbleConnection *conn = GABBLE_CONNECTION (user_data);
+
+ tp_dbus_daemon_cancel_name_owner_watch (conn->daemon,
+ subscriber, subscriber_name_owner_changed, conn);
+
+ return TRUE;
+}
+
+
+void
+conn_mail_notif_dispose (GabbleConnection *conn)
+{
+ if (conn->mail_subscribers)
+ {
+ g_hash_table_foreach_remove (conn->mail_subscribers,
+ foreach_cancel_watch, conn);
+ g_hash_table_unref (conn->mail_subscribers);
+ conn->mail_subscribers = NULL;
+ }
+
+ g_free (conn->inbox_url);
+ conn->inbox_url = NULL;
+
+ if (conn->unread_mails != NULL)
+ g_hash_table_unref (conn->unread_mails);
+
+ conn->unread_mails = NULL;
+
+ if (conn->new_mail_handler_id != 0)
+ {
+ WockyPorter *porter = wocky_session_get_porter (conn->session);
+ wocky_porter_unregister_handler (porter, conn->new_mail_handler_id);
+ conn->new_mail_handler_id = 0;
+ }
+}
+
+
+void
+conn_mail_notif_iface_init (gpointer g_iface,
+ gpointer iface_data)
+{
+ GabbleSvcConnectionInterfaceMailNotificationClass *klass = g_iface;
+
+#define IMPLEMENT(x) gabble_svc_connection_interface_mail_notification_implement_##x (\
+ klass, gabble_mail_notification_##x)
+ IMPLEMENT (subscribe);
+ IMPLEMENT (unsubscribe);
+ IMPLEMENT (request_inbox_url);
+ IMPLEMENT (request_mail_url);
+#undef IMPLEMENT
+}
+
+
+static GPtrArray *
+get_unread_mails (GabbleConnection *conn)
+{
+ GPtrArray *mails = g_ptr_array_new ();
+ GHashTableIter iter;
+ gpointer value;
+
+ if (conn->unread_mails != NULL)
+ {
+ g_hash_table_iter_init (&iter, conn->unread_mails);
+
+ while (g_hash_table_iter_next (&iter, NULL, &value))
+ {
+ GHashTable *mail = value;
+ g_ptr_array_add (mails, mail);
+ }
+ }
+
+ return mails;
+}
+
+
+void
+conn_mail_notif_properties_getter (GObject *object,
+ GQuark interface,
+ GQuark name,
+ GValue *value,
+ gpointer getter_data)
+{
+ static GQuark prop_quarks[NUM_OF_PROP] = {0};
+ GabbleConnection *conn = GABBLE_CONNECTION (object);
+
+ if (G_UNLIKELY (prop_quarks[0] == 0))
+ {
+ prop_quarks[PROP_MAIL_NOTIFICATION_FLAGS] =
+ g_quark_from_static_string ("MailNotificationFlags");
+ prop_quarks[PROP_UNREAD_MAIL_COUNT] =
+ g_quark_from_static_string ("UnreadMailCount");
+ prop_quarks[PROP_UNREAD_MAILS] =
+ g_quark_from_static_string ("UnreadMails");
+ prop_quarks[PROP_MAIL_ADDRESS] =
+ g_quark_from_static_string ("MailAddress");
+ }
+
+ DEBUG ("MailNotification get property %s", g_quark_to_string (name));
+
+ if (name == prop_quarks[PROP_MAIL_NOTIFICATION_FLAGS])
+ {
+ if (conn->features & GABBLE_CONNECTION_FEATURES_GOOGLE_MAIL_NOTIFY)
+ g_value_set_uint (value,
+ GABBLE_MAIL_NOTIFICATION_FLAG_SUPPORTS_UNREAD_MAIL_COUNT
+ | GABBLE_MAIL_NOTIFICATION_FLAG_SUPPORTS_UNREAD_MAILS
+ | GABBLE_MAIL_NOTIFICATION_FLAG_SUPPORTS_REQUEST_INBOX_URL
+ | GABBLE_MAIL_NOTIFICATION_FLAG_SUPPORTS_REQUEST_MAIL_URL
+ | GABBLE_MAIL_NOTIFICATION_FLAG_THREAD_BASED
+ );
+ else
+ g_value_set_uint (value, 0);
+ }
+ else if (name == prop_quarks[PROP_UNREAD_MAIL_COUNT])
+ {
+ g_value_set_uint (value,
+ conn->unread_mails ? g_hash_table_size (conn->unread_mails) : 0);
+ }
+ else if (name == prop_quarks[PROP_UNREAD_MAILS])
+ {
+ GPtrArray *mails = get_unread_mails (conn);
+ g_value_set_boxed (value, mails);
+ g_ptr_array_free (mails, TRUE);
+ }
+ else if (name == prop_quarks[PROP_MAIL_ADDRESS])
+ {
+ TpBaseConnection *base = TP_BASE_CONNECTION (object);
+ TpHandleRepoIface *contact_handles =
+ tp_base_connection_get_handles (base, TP_HANDLE_TYPE_CONTACT);
+ TpHandle self = tp_base_connection_get_self_handle (base);
+ const gchar *bare_jid = tp_handle_inspect (contact_handles, self);
+
+ /* After some testing I found that the bare jid (username@stream_server)
+ * always represent the e-mail address on Google account. */
+ g_value_set_string (value, bare_jid);
+ }
+ else
+ {
+ g_assert_not_reached ();
+ }
+}
diff --git a/src/conn-mail-notif.h b/src/conn-mail-notif.h
new file mode 100644
index 000000000..b1212e2bf
--- /dev/null
+++ b/src/conn-mail-notif.h
@@ -0,0 +1,38 @@
+/*
+ * conn-mail-notif.h - Header for Gabble connection mail notification interface
+ * Copyright (C) 2009 Collabora Ltd.
+ *
+ * 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.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef __CONN_EMAIL_NOTIF_H__
+#define __CONN_EMAIL_NOTIF_H__
+
+#include <glib-object.h>
+
+#include "connection.h"
+
+G_BEGIN_DECLS
+
+void conn_mail_notif_init (GabbleConnection *conn);
+void conn_mail_notif_dispose (GabbleConnection *conn);
+void conn_mail_notif_iface_init (gpointer g_iface, gpointer iface_data);
+void conn_mail_notif_properties_getter (GObject *object, GQuark interface,
+ GQuark name, GValue *value, gpointer getter_data);
+
+G_END_DECLS
+
+#endif /* __CONN_EMAIL_NOTIF_H__ */
+
diff --git a/src/connection.c b/src/connection.c
index d0643e2e1..250475d79 100644
--- a/src/connection.c
+++ b/src/connection.c
@@ -55,6 +55,7 @@
#include "conn-location.h"
#include "conn-presence.h"
#include "conn-sidecars.h"
+#include "conn-mail-notif.h"
#include "conn-olpc.h"
#include "debug.h"
#include "disco.h"
@@ -95,7 +96,7 @@ G_DEFINE_TYPE_WITH_CODE(GabbleConnection,
conn_avatars_iface_init);
G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_CAPABILITIES,
capabilities_service_iface_init);
- G_IMPLEMENT_INTERFACE(TP_TYPE_SVC_DBUS_PROPERTIES,
+ G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES,
tp_dbus_properties_mixin_iface_init);
G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_CONTACTS,
tp_contacts_mixin_iface_init);
@@ -118,6 +119,8 @@ G_DEFINE_TYPE_WITH_CODE(GabbleConnection,
olpc_gadget_iface_init);
G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SVC_CONNECTION_FUTURE,
conn_future_iface_init);
+ G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SVC_CONNECTION_INTERFACE_MAIL_NOTIFICATION,
+ conn_mail_notif_iface_init);
)
/* properties */
@@ -333,6 +336,7 @@ gabble_connection_constructor (GType type,
conn_olpc_activity_properties_init (self);
conn_location_init (self);
conn_sidecars_init (self);
+ conn_mail_notif_init (self);
tp_contacts_mixin_add_contact_attributes_iface (G_OBJECT (self),
TP_IFACE_CONNECTION_INTERFACE_CAPABILITIES,
@@ -421,11 +425,19 @@ gabble_connection_constructed (GObject *object)
static void
gabble_connection_init (GabbleConnection *self)
{
+ GError *error = NULL;
GabbleConnectionPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
GABBLE_TYPE_CONNECTION, GabbleConnectionPrivate);
DEBUG("Initializing (GabbleConnection *)%p", self);
+ self->daemon = tp_dbus_daemon_dup (&error);
+
+ if (self->daemon == NULL)
+ {
+ g_error ("Failed to connect to dbus daemon: %s", error->message);
+ }
+
self->priv = priv;
self->lmconn = lm_connection_new ();
@@ -724,6 +736,13 @@ gabble_connection_class_init (GabbleConnectionClass *gabble_connection_class)
{ "DecloakAutomatically", TWICE ("decloak-automatically") },
{ NULL }
};
+ static TpDBusPropertiesMixinPropImpl mail_notif_props[] = {
+ { "MailNotificationFlags", NULL, NULL },
+ { "UnreadMailCount", NULL, NULL },
+ { "UnreadMails", NULL, NULL },
+ { "MailAddress", NULL, NULL },
+ { NULL }
+ };
static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = {
{ GABBLE_IFACE_OLPC_GADGET,
conn_olpc_gadget_properties_getter,
@@ -745,6 +764,11 @@ gabble_connection_class_init (GabbleConnectionClass *gabble_connection_class)
tp_dbus_properties_mixin_setter_gobject_properties,
decloak_props,
},
+ { GABBLE_IFACE_CONNECTION_INTERFACE_MAIL_NOTIFICATION,
+ conn_mail_notif_properties_getter,
+ NULL,
+ mail_notif_props,
+ },
{ NULL }
};
@@ -1006,6 +1030,8 @@ gabble_connection_dispose (GObject *object)
g_hash_table_destroy (self->avatar_requests);
+ conn_mail_notif_dispose (self);
+
g_assert (priv->iq_disco_cb == NULL);
g_assert (priv->iq_unknown_cb == NULL);
g_assert (priv->olpc_msg_cb == NULL);
@@ -1079,6 +1105,12 @@ gabble_connection_dispose (GObject *object)
conn_sidecars_dispose (self);
+ if (self->daemon != NULL)
+ {
+ g_object_unref (self->daemon);
+ self->daemon = NULL;
+ }
+
if (G_OBJECT_CLASS (gabble_connection_parent_class)->dispose)
G_OBJECT_CLASS (gabble_connection_parent_class)->dispose (object);
}
@@ -2544,6 +2576,8 @@ connection_disco_cb (GabbleDisco *disco,
conn->features |= GABBLE_CONNECTION_FEATURES_PRESENCE_INVISIBLE;
else if (0 == strcmp (var, NS_PRIVACY))
conn->features |= GABBLE_CONNECTION_FEATURES_PRIVACY;
+ else if (0 == strcmp (var, NS_GOOGLE_MAIL_NOTIFY))
+ conn->features |= GABBLE_CONNECTION_FEATURES_GOOGLE_MAIL_NOTIFY;
}
}
@@ -2559,6 +2593,14 @@ connection_disco_cb (GabbleDisco *disco,
tp_base_connection_add_interfaces ((TpBaseConnection *) conn, ifaces);
}
+ if (conn->features & GABBLE_CONNECTION_FEATURES_GOOGLE_MAIL_NOTIFY)
+ {
+ const gchar *ifaces[] =
+ { GABBLE_IFACE_CONNECTION_INTERFACE_MAIL_NOTIFICATION, NULL };
+
+ tp_base_connection_add_interfaces ((TpBaseConnection *) conn, ifaces);
+ }
+
/* send presence to the server to indicate availability */
/* TODO: some way for the user to set this */
if (!_gabble_connection_signal_own_presence (conn, NULL, &error))
@@ -3287,7 +3329,7 @@ gabble_connection_send_presence (GabbleConnection *conn,
if (LM_MESSAGE_SUB_TYPE_SUBSCRIBE == sub_type)
lm_message_node_add_own_nick (message->node, conn);
- if (status != NULL && status[0] != '\0')
+ if (!CHECK_STR_EMPTY(status))
lm_message_node_add_child (message->node, "status", status);
result = _gabble_connection_send (conn, message, error);
diff --git a/src/connection.h b/src/connection.h
index 6bce3053c..f198741ec 100644
--- a/src/connection.h
+++ b/src/connection.h
@@ -28,6 +28,7 @@
#include <telepathy-glib/contacts-mixin.h>
#include <telepathy-glib/presence-mixin.h>
#include <telepathy-glib/dbus-properties-mixin.h>
+#include <telepathy-glib/dbus.h>
#include <wocky/wocky-session.h>
#include <wocky/wocky-pep-service.h>
@@ -74,6 +75,7 @@ typedef enum
GABBLE_CONNECTION_FEATURES_PRESENCE_INVISIBLE = 1 << 2,
GABBLE_CONNECTION_FEATURES_PRIVACY = 1 << 3,
GABBLE_CONNECTION_FEATURES_PEP = 1 << 4,
+ GABBLE_CONNECTION_FEATURES_GOOGLE_MAIL_NOTIFY = 1 << 5,
} GabbleConnectionFeatures;
typedef struct _GabbleConnectionPrivate GabbleConnectionPrivate;
@@ -120,6 +122,9 @@ struct _GabbleConnection {
TpPresenceMixin presence;
TpContactsMixin contacts;
+ /* DBus daemon instance */
+ TpDBusDaemon *daemon;
+
/* loudmouth connection */
LmConnection *lmconn;
WockySession *session;
@@ -187,6 +192,12 @@ struct _GabbleConnection {
/* gchar *interface → GList<DBusGMethodInvocation> */
GHashTable *pending_sidecars;
+ /* Mail Notification */
+ GHashTable *mail_subscribers;
+ gchar *inbox_url;
+ GHashTable *unread_mails;
+ guint new_mail_handler_id;
+
GabbleConnectionPrivate *priv;
};
diff --git a/src/debug.c b/src/debug.c
index ad180b556..55d0f88dd 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -39,6 +39,7 @@ static GDebugKey keys[] = {
{ "search", GABBLE_DEBUG_SEARCH },
{ "base-channel", GABBLE_DEBUG_BASE_CHANNEL },
{ "plugins", GABBLE_DEBUG_PLUGINS },
+ { "mail", GABBLE_DEBUG_MAIL_NOTIF },
{ 0, },
};
diff --git a/src/debug.h b/src/debug.h
index 5eacff62c..dcd2cb85a 100644
--- a/src/debug.h
+++ b/src/debug.h
@@ -32,7 +32,8 @@ typedef enum
GABBLE_DEBUG_FT = 1 << 18,
GABBLE_DEBUG_SEARCH = 1 << 19,
GABBLE_DEBUG_BASE_CHANNEL = 1 << 20,
- GABBLE_DEBUG_PLUGINS = 1 << 21
+ GABBLE_DEBUG_PLUGINS = 1 << 21,
+ GABBLE_DEBUG_MAIL_NOTIF = 1 << 22
} GabbleDebugFlags;
void gabble_debug_set_flags_from_env (void);
diff --git a/src/ft-channel.c b/src/ft-channel.c
index 4cf1be682..fa93abb8f 100644
--- a/src/ft-channel.c
+++ b/src/ft-channel.c
@@ -67,8 +67,6 @@ G_DEFINE_TYPE_WITH_CODE (GabbleFileTransferChannel, gabble_file_transfer_channel
file_transfer_iface_init);
);
-#define CHECK_STR_EMPTY(x) ((x) == NULL || (x)[0] == '\0')
-
#define GABBLE_UNDEFINED_FILE_SIZE G_MAXUINT64
static const char *gabble_file_transfer_channel_interfaces[] = { NULL };
diff --git a/src/jingle-session.c b/src/jingle-session.c
index d67ca6780..73f75b468 100644
--- a/src/jingle-session.c
+++ b/src/jingle-session.c
@@ -2038,7 +2038,7 @@ gabble_jingle_session_terminate (GabbleJingleSession *sess,
lm_message_node_add_child (r, reason_elt, NULL);
- if (text != NULL && *text != '\0')
+ if (!CHECK_STR_EMPTY(text))
lm_message_node_add_child (r, "text", text);
}
diff --git a/src/namespaces.h b/src/namespaces.h
index 50c6e322b..18b3feb5c 100644
--- a/src/namespaces.h
+++ b/src/namespaces.h
@@ -105,6 +105,7 @@
#define NS_X_CONFERENCE "jabber:x:conference"
#define NS_XMPP_STANZAS "urn:ietf:params:xml:ns:xmpp-stanzas"
#define NS_GEOLOC "http://jabber.org/protocol/geoloc"
+#define NS_GOOGLE_MAIL_NOTIFY "google:mail:notify"
#define NS_TEMPPRES "urn:xmpp:temppres:0"
diff --git a/src/util.h b/src/util.h
index aa6dbe67b..cd4de7b48 100644
--- a/src/util.h
+++ b/src/util.h
@@ -35,6 +35,8 @@
#include "types.h"
+#define CHECK_STR_EMPTY(x) ((x) == NULL || (x)[0] == '\0')
+
typedef GSList * NodeIter;
#define node_iter(node) (node->children)
#define node_iter_next(i) (g_slist_next (i))
diff --git a/src/vcard-manager.c b/src/vcard-manager.c
index 4353609f2..02877e040 100644
--- a/src/vcard-manager.c
+++ b/src/vcard-manager.c
@@ -810,7 +810,7 @@ observe_vcard (GabbleConnection *conn,
{
const gchar *fn = lm_message_node_get_value (fn_node);
- if (fn != NULL && *fn != '\0')
+ if (!CHECK_STR_EMPTY(fn))
{
field = "<FN>";
alias = g_strdup (fn);
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
index 767f13e13..6f8447825 100644
--- a/tests/twisted/Makefile.am
+++ b/tests/twisted/Makefile.am
@@ -160,7 +160,8 @@ TWISTED_TESTS = \
test-set-status-idempotence.py \
test-location.py \
pubsub.py \
- sidecars.py
+ sidecars.py \
+ mail-notification.py
TESTS =
diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py
index f80e7dd76..263e779f2 100644
--- a/tests/twisted/constants.py
+++ b/tests/twisted/constants.py
@@ -101,6 +101,7 @@ CONN_IFACE_SIMPLE_PRESENCE = CONN + '.Interface.SimplePresence'
CONN_IFACE_REQUESTS = CONN + '.Interface.Requests'
CONN_IFACE_LOCATION = CONN + '.Interface.Location'
CONN_IFACE_GABBLE_DECLOAK = CONN + '.Interface.Gabble.Decloak'
+CONN_IFACE_MAIL_NOTIFICATION = CONN + '.Interface.MailNotification.DRAFT'
ATTR_CONTACT_CAPABILITIES = CONN_IFACE_CONTACT_CAPS + '/capabilities'
diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py
index d6e4c343f..6bf2ef498 100644
--- a/tests/twisted/gabbletest.py
+++ b/tests/twisted/gabbletest.py
@@ -28,7 +28,7 @@ import dbus
NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'
-def make_result_iq(stream, iq):
+def make_result_iq(stream, iq, add_query_node=True):
result = IQ(stream, "result")
result["id"] = iq["id"]
to = iq.getAttribute('to')
@@ -36,7 +36,7 @@ def make_result_iq(stream, iq):
result["from"] = to
query = iq.firstChildElement()
- if query:
+ if query and add_query_node:
result.addElement((query.uri, query.name))
return result
@@ -301,6 +301,8 @@ class GoogleXmlStream(BaseXmlStream):
feature['var'] = ns.GOOGLE_ROSTER
feature = query.addElement('feature')
feature['var'] = ns.GOOGLE_JINGLE_INFO
+ feature = query.addElement('feature')
+ feature['var'] = ns.GOOGLE_MAIL_NOTIFY
iq['type'] = 'result'
iq['from'] = 'localhost'
diff --git a/tests/twisted/mail-notification.py b/tests/twisted/mail-notification.py
new file mode 100644
index 000000000..d2d9b3cb9
--- /dev/null
+++ b/tests/twisted/mail-notification.py
@@ -0,0 +1,358 @@
+"""
+Test Connection.Interface.MailNotification
+"""
+
+from twisted.words.xish import domish
+from gabbletest import exec_test, make_result_iq, GoogleXmlStream
+from servicetest import EventPattern
+
+import constants as cs
+import ns
+import dbus
+
+def check_properties_empty(conn, expected_flags=0):
+ """Check that all mail notification properties are empty and that
+ mail notification flags match the expected flags"""
+
+ flags = conn.Get(
+ cs.CONN_IFACE_MAIL_NOTIFICATION, 'MailNotificationFlags',
+ dbus_interface=cs.PROPERTIES_IFACE)
+ assert flags == expected_flags
+
+ mail_count = conn.Get(
+ cs.CONN_IFACE_MAIL_NOTIFICATION, 'UnreadMailCount',
+ dbus_interface=cs.PROPERTIES_IFACE)
+ assert mail_count == 0
+
+ unread_mails = conn.Get(
+ cs.CONN_IFACE_MAIL_NOTIFICATION, 'UnreadMails',
+ dbus_interface=cs.PROPERTIES_IFACE)
+ assert len(unread_mails) == 0
+
+
+def test_google_featured(q, bus, conn, stream):
+ """Test functionnality when google mail notification is supported"""
+
+ inbox_url = 'http://mail.google.com/mail'
+
+ # E-mail thread 1 data
+ thread1_id = "1"
+ # Dates are 32bit unsigned integers, let's use the biggest possible value
+ thread1_date = (pow(2,32) - 1) * 1000L
+ thread1_url = 'http://mail.google.com/mail/#inbox/%x' % long(thread1_id)
+ thread1_senders = [('John Smith', 'john@smith.com'),
+ ('Denis Tremblay', 'denis@tremblay.qc.ca')]
+ thread1_subject = "subject1"
+ thread1_snippet = "body1"
+
+ # Email thread 2 data
+ thread2_id = "2"
+ thread2_date = 1234L
+ thread2_url = 'http://mail.google.com/mail/#inbox/%x' % long(thread2_id)
+ thread2_senders = [('Sam Gratte', 'sam@gratte.edu'),]
+ thread2_subject = "subject2"
+ thread2_snippet = "body2"
+
+ # Email thread 3 data
+ thread3_id = "3"
+ thread3_date = 1235L
+ thread3_url = 'http://mail.google.com/mail/#inbox/%x' % long(thread3_id)
+ thread3_senders = [('Le Chat', 'le@chat.fr'),]
+ thread3_subject = "subject3"
+ thread3_snippet = "body3"
+
+ # Supported mail notification flags
+ Supports_Unread_Mail_Count = 1
+ Supports_Unread_Mails = 2
+ Supports_Request_Inbox_URL = 8
+ Supports_Request_Mail_URL = 16
+ Thread_Based = 32
+ expected_flags = Supports_Unread_Mail_Count\
+ | Supports_Unread_Mails\
+ | Supports_Request_Inbox_URL\
+ | Supports_Request_Mail_URL\
+ | Thread_Based
+
+ # Nobody is subscribed yet, attributes should all be empty, and
+ # mail notification flags are set properly.
+ check_properties_empty(conn, expected_flags)
+
+ # Check that Gabble queries mail data on initial call to Subscribe().
+ conn.MailNotification.Subscribe()
+ event = q.expect('stream-iq', query_ns=ns.GOOGLE_MAIL_NOTIFY)
+
+ result = make_result_iq(stream, event.stanza, False)
+ mailbox = result.addElement('mailbox')
+ mailbox['xmlns'] = ns.GOOGLE_MAIL_NOTIFY
+ mailbox['url'] = inbox_url
+
+ # Set e-mail thread 1
+ mail = mailbox.addElement('mail-thread-info')
+ mail['tid'] = thread1_id
+ mail['date'] = str(thread1_date)
+ senders = mail.addElement('senders')
+ for t1_sender in thread1_senders:
+ sender = senders.addElement('sender')
+ sender['name'] = t1_sender[0]
+ sender['address'] = t1_sender[1]
+ sender['unread'] = '1'
+ mail.addElement('subject', content=thread1_subject)
+ mail.addElement('snippet', content=thread1_snippet)
+
+ # Set e-mail thread 2
+ mail = mailbox.addElement('mail-thread-info')
+ mail['tid'] = thread2_id
+ mail['date'] = str(thread2_date)
+ senders = mail.addElement('senders')
+ for t2_sender in thread2_senders:
+ sender = senders.addElement('sender')
+ sender['name'] = t2_sender[0]
+ sender['address'] = t2_sender[1]
+ sender['unread'] = '1'
+ sender = senders.addElement('sender')
+ sender['name'] = 'Read Sender'
+ sender['address'] = 'read@sender.net'
+ mail.addElement('subject', content=thread2_subject)
+ mail.addElement('snippet', content=thread2_snippet)
+
+ stream.send(result)
+
+ # Then we expect UnreadMailsChanged with all the mail information.
+ event = q.expect('dbus-signal', signal="UnreadMailsChanged")
+
+ # Check that inbox URL is correct
+ stored_url = conn.MailNotification.RequestInboxURL()
+ assert stored_url[0] == inbox_url
+ assert stored_url[1] == 0 # HTTP GET
+ assert len(stored_url[2]) == 0
+
+ # UnreadMailsChanged(u: count, aa{sv}: mails_added, ax: mails_removed)
+ unread_count = event.args[0]
+ mails_added = event.args[1]
+ mails_removed = event.args[2]
+
+ # Get stored data to check we have same thing
+ stored_unread_count = conn.Get(
+ cs.CONN_IFACE_MAIL_NOTIFICATION, 'UnreadMailCount',
+ dbus_interface=cs.PROPERTIES_IFACE)
+ stored_unread_mails = conn.Get(
+ cs.CONN_IFACE_MAIL_NOTIFICATION, 'UnreadMails',
+ dbus_interface=cs.PROPERTIES_IFACE)
+
+ assert unread_count == 2
+ assert stored_unread_count == unread_count
+ assert len(stored_unread_mails) == unread_count
+ assert len(mails_added) == unread_count
+ assert len(mails_removed) == 0
+
+ # Extract mails from signal, order is unknown
+ mail1 = None
+ mail2 = None
+ for mail in mails_added:
+ if mail['id'] == thread1_id:
+ mail1 = mail
+ elif mail['id'] == thread2_id:
+ mail2 = mail
+ else:
+ assert False, "Gabble sent an unknown mail id=" + str(mail['id'])
+
+ # Validate added e-mails with original data.
+ assert mail1 != None
+ # While date is in millisecond, the received timestamp is in seconds thus
+ # we need to divided by 1000
+ assert mail1['received-timestamp'] == thread1_date / 1000
+ assert mail1['subject'] == thread1_subject
+ assert mail1['truncated'] == True
+ assert mail1['content'] == thread1_snippet
+ assert mail1['senders'] == thread1_senders
+
+ assert mail2 != None
+ assert mail2['received-timestamp'] == thread2_date / 1000
+ assert mail2['subject'] == thread2_subject
+ assert mail2['truncated'] == True
+ assert mail2['content'] == thread2_snippet
+ assert mail2['senders'] == thread2_senders
+
+ # Extract mails from stored mails, order is unkown
+ stored_mail1 = None
+ stored_mail2 = None
+ for mail in stored_unread_mails:
+ if mail['id'] == thread1_id:
+ stored_mail1 = mail
+ elif mail['id'] == thread2_id:
+ stored_mail2 = mail
+ else:
+ assert False, "Gabble stored an unkown mail id=" + str(mail['id'])
+
+ # Validate stored e-mails with original data
+ assert stored_mail1 != None
+ assert stored_mail1['received-timestamp'] == thread1_date / 1000
+ assert stored_mail1['subject'] == thread1_subject
+ assert stored_mail1['truncated'] == True
+ assert stored_mail1['content'] == thread1_snippet
+ assert stored_mail1['senders'] == thread1_senders
+
+ assert stored_mail2 != None
+ assert stored_mail2['received-timestamp'] == thread2_date / 1000
+ assert stored_mail2['subject'] == thread2_subject
+ assert stored_mail2['truncated'] == True
+ assert stored_mail2['content'] == thread2_snippet
+ assert stored_mail2['senders'] == thread2_senders
+
+ # Check the we can get an URL for a specific mail
+ mail_url1 = conn.MailNotification.RequestMailURL(
+ stored_mail1['id'],
+ stored_mail1['url-data']);
+
+ mail_url2 = conn.MailNotification.RequestMailURL(
+ stored_mail2['id'],
+ stored_mail2['url-data']);
+
+ assert mail_url1[0] == thread1_url
+ assert mail_url1[1] == 0
+ assert len(mail_url1[2]) == 0
+
+ assert mail_url2[0] == thread2_url
+ assert mail_url2[1] == 0
+ assert len(mail_url2[2]) == 0
+
+ # Now we want to validate the update mechanism. Thus we wil send an
+ # new-mail event, wait for gabble to query the latest mail and reply
+ # a different list.
+ m = domish.Element((None, 'iq'))
+ m['type'] = 'set'
+ m['from'] = 'alice@foo.com'
+ m['id'] = '3'
+ m.addElement((ns.GOOGLE_MAIL_NOTIFY, 'new-mail'))
+ stream.send(m)
+
+ # Wait for mail information request
+ event = q.expect('stream-iq', query_ns=ns.GOOGLE_MAIL_NOTIFY)
+
+ result = make_result_iq(stream, event.stanza, False)
+ mailbox = result.addElement('mailbox')
+ mailbox['xmlns'] = ns.GOOGLE_MAIL_NOTIFY
+ # We alter the URL to see if it gets detected
+ mailbox['url'] = inbox_url + 'diff'
+
+ # Set e-mail thread 1 and change snippet to see if it's detected
+ mail = mailbox.addElement('mail-thread-info')
+ mail['tid'] = str(thread1_id)
+ mail['date'] = str(thread1_date)
+ senders = mail.addElement('senders')
+ for t1_sender in thread1_senders:
+ sender = senders.addElement('sender')
+ sender['name'] = t1_sender[0]
+ sender['address'] = t1_sender[1]
+ sender['unread'] = '1'
+ mail.addElement('subject', content=thread1_subject)
+ mail.addElement('snippet', content=thread1_snippet + 'diff')
+
+ # We don't set the thread 2, as if it was removed
+
+ # Set e-mail thread 3
+ mail = mailbox.addElement('mail-thread-info')
+ mail['tid'] = str(thread3_id)
+ mail['date'] = str(thread3_date)
+ senders = mail.addElement('senders')
+ for t3_sender in thread3_senders:
+ sender = senders.addElement('sender')
+ sender['name'] = t3_sender[0]
+ sender['address'] = t3_sender[1]
+ sender['unread'] = '1'
+ mail.addElement('subject', content=thread3_subject)
+ mail.addElement('snippet', content=thread3_snippet)
+
+ stream.send(result)
+
+ event = q.expect('dbus-signal', signal='UnreadMailsChanged')
+ unread_count = event.args[0]
+ mails_added = event.args[1]
+ mails_removed = event.args[2]
+
+ # Validate that changed is set for correct items
+ assert unread_count == 2
+ assert len(mails_added) == 2
+ assert mails_added[0]['id'] in (thread1_id, thread3_id)
+ assert mails_added[1]['id'] in (thread1_id, thread3_id)
+ assert mails_added[0]['id'] != mails_added[1]['id']
+ assert len(mails_removed) == 1
+ assert mails_removed[0] == thread2_id
+
+ # Check attribue MailAddres
+ mail_address = conn.Get(
+ cs.CONN_IFACE_MAIL_NOTIFICATION, 'MailAddress',
+ dbus_interface=cs.PROPERTIES_IFACE)
+
+ assert mail_address == "test@localhost"
+
+ # Unsubscribe and check that all data has been dropped
+ conn.MailNotification.Unsubscribe()
+ check_properties_empty(conn, expected_flags)
+
+
+def test_no_google_featured(q, bus, conn, stream):
+ """Check that Gabble reacts correctly when called on MailNotification
+ while the feature is not supported."""
+
+ # Google mail notification is not supported, gabble should not emit any
+ # signals.
+ forbidden = [EventPattern('dbus-signal', signal='MailsReceived'),
+ EventPattern('dbus-signal', signal='UnreadMailsChanged'),
+ EventPattern('stream-iq', query_ns=ns.GOOGLE_MAIL_NOTIFY)]
+ q.forbid_events(forbidden)
+
+ # Make sure gabble does not query mail data on an unexpected new-mail
+ # notification.
+ m = domish.Element((None, 'iq'))
+ m['type'] = 'set'
+ m['from'] = 'alice@foo.com'
+ m['id'] = '2'
+ m.addElement((ns.GOOGLE_MAIL_NOTIFY, 'new-mail'))
+ stream.send(m)
+
+ # Make sure method returns not implemented exception
+ try:
+ conn.MailNotification.Subscribe()
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_IMPLEMENTED
+
+ try:
+ conn.MailNotification.Unsubscribe()
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_IMPLEMENTED
+
+ try:
+ conn.MailNotification.RequestInboxURL()
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_IMPLEMENTED
+
+ try:
+ conn.MailNotification.RequestMailURL("1", "http://test.com/mail")
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_IMPLEMENTED
+
+ # Make sure all properties return with empty or 0 data including
+ # MailNotificationFlags
+ check_properties_empty(conn)
+
+ q.unforbid_events(forbidden)
+
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ interfaces = conn.GetInterfaces()
+
+ if stream.__class__ is GoogleXmlStream:
+ assert cs.CONN_IFACE_MAIL_NOTIFICATION in interfaces
+ test_google_featured(q, bus, conn, stream)
+ else:
+ assert cs.CONN_IFACE_MAIL_NOTIFICATION not in interfaces
+ test_no_google_featured(q, bus, conn, stream)
+
+if __name__ == '__main__':
+ exec_test(test, protocol=GoogleXmlStream)
+ exec_test(test)
diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py
index 219125933..97f2625ed 100644
--- a/tests/twisted/ns.py
+++ b/tests/twisted/ns.py
@@ -16,6 +16,7 @@ GOOGLE_ROSTER = 'google:roster'
GOOGLE_SESSION = "http://www.google.com/session"
GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone"
GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video"
+GOOGLE_MAIL_NOTIFY = "google:mail:notify"
IBB = 'http://jabber.org/protocol/ibb'
JINGLE_015 = "http://jabber.org/protocol/jingle"
JINGLE_015_AUDIO = "http://jabber.org/protocol/jingle/description/audio"
diff --git a/tests/twisted/servicetest.py b/tests/twisted/servicetest.py
index 0634678e6..c40193827 100644
--- a/tests/twisted/servicetest.py
+++ b/tests/twisted/servicetest.py
@@ -339,6 +339,7 @@ def wrap_connection(conn):
('ContactCapabilities', cs.CONN_IFACE_CONTACT_CAPS),
('Location', cs.CONN_IFACE_LOCATION),
('Future', tp_name_prefix + '.Connection.FUTURE'),
+ ('MailNotification', cs.CONN_IFACE_MAIL_NOTIFICATION),
]))
def wrap_channel(chan, type_, extra=None):