diff options
author | Nicolas Dufresne <nicolas.dufresne@collabora.co.uk> | 2010-02-25 16:17:46 -0500 |
---|---|---|
committer | Nicolas Dufresne <nicolas.dufresne@collabora.co.uk> | 2010-02-25 16:17:46 -0500 |
commit | 8d43e64804d43aafe3ab6db69ecdd47395638955 (patch) | |
tree | f919157e63239ee3a32b7bf8eaf6cec45c6dbd1b | |
parent | 8d48b82762275383debd8197e63f3c78e05c2f06 (diff) | |
parent | a88baea0dd8f3144678175cb871c84576a511c77 (diff) | |
download | telepathy-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.xml | 653 | ||||
-rw-r--r-- | extensions/all.xml | 1 | ||||
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/conn-mail-notif.c | 757 | ||||
-rw-r--r-- | src/conn-mail-notif.h | 38 | ||||
-rw-r--r-- | src/connection.c | 46 | ||||
-rw-r--r-- | src/connection.h | 11 | ||||
-rw-r--r-- | src/debug.c | 1 | ||||
-rw-r--r-- | src/debug.h | 3 | ||||
-rw-r--r-- | src/ft-channel.c | 2 | ||||
-rw-r--r-- | src/jingle-session.c | 2 | ||||
-rw-r--r-- | src/namespaces.h | 1 | ||||
-rw-r--r-- | src/util.h | 2 | ||||
-rw-r--r-- | src/vcard-manager.c | 2 | ||||
-rw-r--r-- | tests/twisted/Makefile.am | 3 | ||||
-rw-r--r-- | tests/twisted/constants.py | 1 | ||||
-rw-r--r-- | tests/twisted/gabbletest.py | 6 | ||||
-rw-r--r-- | tests/twisted/mail-notification.py | 358 | ||||
-rw-r--r-- | tests/twisted/ns.py | 1 | ||||
-rw-r--r-- | tests/twisted/servicetest.py | 1 |
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 + "<", and a key "percent" with value "%", this should be represented as + two HTTP_Post_Data structures, ("less-than", "<") and ("percent", "%"), + resulting in a POST request whose request body is "less-than=&lt;&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> + <input type="hidden" name="less-than">&lt;</input> + <input type="hidden" name="percent">%</input> + </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 — 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 — 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 — 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 — 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 — 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 — 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 — 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 — b</dt> + <dd>If true, this mail has attachments.</dd> + + <dt>subject — s</dt> + <dd> + The subject of the message. This MUST be encoded in UTF-8. + </dd> + + <dt>content-type — 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 — 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 — s</dt> + <dd> + The body of the message, possibly truncated, encoded as appropriate + for "content-type". + </dd> + + <dt>folder — 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): |